Première version fonctionnelle qui gère les événements récurrents.
Fix #7
This commit is contained in:
		@@ -10,10 +10,11 @@ from selenium import webdriver
 | 
			
		||||
from selenium.webdriver.chrome.service import Service
 | 
			
		||||
from selenium.webdriver.chrome.options import Options
 | 
			
		||||
import icalendar
 | 
			
		||||
from icalendar import vDatetime
 | 
			
		||||
from datetime import datetime, date
 | 
			
		||||
import json
 | 
			
		||||
from bs4 import BeautifulSoup
 | 
			
		||||
 | 
			
		||||
import pickle
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Downloader(ABC):
 | 
			
		||||
@@ -77,7 +78,7 @@ class Extractor(ABC):
 | 
			
		||||
    def clear_events(self):
 | 
			
		||||
        self.events = []
 | 
			
		||||
 | 
			
		||||
    def add_event(self, title, category, start_day, location, description, tags, uuid, url_human=None, start_time=None, end_day=None, end_time=None, last_modified=None, published=False):
 | 
			
		||||
    def add_event(self, title, category, start_day, location, description, tags, uuid, recurrences=None, url_human=None, start_time=None, end_day=None, end_time=None, last_modified=None, published=False):
 | 
			
		||||
        if title is None:
 | 
			
		||||
            print("ERROR: cannot import an event without name")
 | 
			
		||||
            return
 | 
			
		||||
@@ -107,6 +108,9 @@ class Extractor(ABC):
 | 
			
		||||
        if last_modified is not None:
 | 
			
		||||
            event["last_modified"] = last_modified
 | 
			
		||||
 | 
			
		||||
        if recurrences is not None:
 | 
			
		||||
            event["recurrences"] = recurrences
 | 
			
		||||
 | 
			
		||||
        self.events.append(event)
 | 
			
		||||
 | 
			
		||||
    def default_value_if_exists(self, default_values, key):
 | 
			
		||||
@@ -191,11 +195,25 @@ class ICALExtractor(Extractor):
 | 
			
		||||
 | 
			
		||||
            last_modified = self.get_item_from_vevent(event, "LAST-MODIFIED", raw = True)
 | 
			
		||||
 | 
			
		||||
            rrule = self.get_item_from_vevent(event, "RRULE", raw = True)
 | 
			
		||||
            if rrule is not None:
 | 
			
		||||
                print("Recurrent event not yet supported", rrule)
 | 
			
		||||
            recurrence_entries = {}
 | 
			
		||||
            for e in ["RRULE", "EXRULE", "EXDATE", "RDATE"]:
 | 
			
		||||
                i = self.get_item_from_vevent(event, e, raw = True)
 | 
			
		||||
                if i is not None:
 | 
			
		||||
                    recurrence_entries[e] = i
 | 
			
		||||
 | 
			
		||||
            self.add_event(title, category, start_day, location, description, tags, uuid=event_url, url_human=url_human, start_time=start_time, end_day=end_day, end_time=end_time, last_modified=last_modified, published=published)
 | 
			
		||||
            if start_day is not None and len(recurrence_entries) != 0:
 | 
			
		||||
                recurrences = ""
 | 
			
		||||
 | 
			
		||||
                for k, r in recurrence_entries.items():
 | 
			
		||||
                    if isinstance(r, list):
 | 
			
		||||
                        recurrences += "\n".join([k + ":" + e.to_ical().decode() for e in r]) + "\n"
 | 
			
		||||
                    else:
 | 
			
		||||
                        recurrences += k + ":" + r.to_ical().decode() + "\n"
 | 
			
		||||
            else:
 | 
			
		||||
                recurrences = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            self.add_event(title, category, start_day, location, description, tags, recurrences=recurrences, uuid=event_url, url_human=url_human, start_time=start_time, end_day=end_day, end_time=end_time, last_modified=last_modified, published=published)
 | 
			
		||||
 | 
			
		||||
        return self.get_structure()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
from datetime import datetime, timedelta, date, time
 | 
			
		||||
import calendar
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
@@ -16,7 +18,7 @@ def daterange(start, end, step=timedelta(1)):
 | 
			
		||||
            curr += step
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CalendarDay:
 | 
			
		||||
class DayInCalendar:
 | 
			
		||||
    midnight = time(23, 59, 59)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, d, on_requested_interval = True):
 | 
			
		||||
@@ -36,8 +38,40 @@ class CalendarDay:
 | 
			
		||||
    def is_today(self):
 | 
			
		||||
        return self.today
 | 
			
		||||
 | 
			
		||||
    def is_generic_uuid_event_from_other(self, event):    
 | 
			
		||||
        for e in self.events:
 | 
			
		||||
            if event.is_generic_by_uuid(e):
 | 
			
		||||
                return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def remove_event_with_generic_uuid_if_exists(self, event):
 | 
			
		||||
        removed = False
 | 
			
		||||
        for i, e in enumerate(self.events):
 | 
			
		||||
            if e.is_generic_by_uuid(event):
 | 
			
		||||
                # remove e from events_by_category
 | 
			
		||||
                for k, v in self.events_by_category.items():
 | 
			
		||||
                    if e in v:
 | 
			
		||||
                        self.events_by_category[k].remove(e)
 | 
			
		||||
                # remove e from the events
 | 
			
		||||
                del self.events[i]
 | 
			
		||||
                removed = True
 | 
			
		||||
 | 
			
		||||
        if removed:
 | 
			
		||||
            # remove empty events_by_category
 | 
			
		||||
            self.events_by_category = dict([(k, v) for k, v in self.events_by_category.items() if len(v) > 0])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def add_event(self, event):
 | 
			
		||||
        if event.contains_date(self.date):
 | 
			
		||||
            if self.is_generic_uuid_event_from_other(event):
 | 
			
		||||
                # we do not add a generic event if a specific is already present
 | 
			
		||||
                pass
 | 
			
		||||
            else:
 | 
			
		||||
                self.remove_event_with_generic_uuid_if_exists(event)
 | 
			
		||||
                self._add_event_internal(event)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _add_event_internal(self, event):
 | 
			
		||||
            self.events.append(event)
 | 
			
		||||
            if event.category is None:
 | 
			
		||||
                if not "" in self.events_by_category:
 | 
			
		||||
@@ -49,7 +83,7 @@ class CalendarDay:
 | 
			
		||||
                self.events_by_category[event.category.name].append(event)
 | 
			
		||||
 | 
			
		||||
    def filter_events(self):
 | 
			
		||||
        self.events.sort(key=lambda e: CalendarDay.midnight if e.start_time is None else e.start_time)
 | 
			
		||||
        self.events.sort(key=lambda e: DayInCalendar.midnight if e.start_time is None else e.start_time)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CalendarList:
 | 
			
		||||
@@ -70,13 +104,13 @@ class CalendarList:
 | 
			
		||||
            self.c_lastdate = lastdate + timedelta(days=6-lastdate.weekday())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        # create a list of CalendarDays
 | 
			
		||||
        # create a list of DayInCalendars
 | 
			
		||||
        self.create_calendar_days()
 | 
			
		||||
 | 
			
		||||
        # fill CalendarDays with events
 | 
			
		||||
        # fill DayInCalendars with events
 | 
			
		||||
        self.fill_calendar_days()
 | 
			
		||||
 | 
			
		||||
        # finally, sort each CalendarDay
 | 
			
		||||
        # finally, sort each DayInCalendar
 | 
			
		||||
        for i, c in self.calendar_days.items():
 | 
			
		||||
            c.filter_events()
 | 
			
		||||
 | 
			
		||||
@@ -93,23 +127,35 @@ class CalendarList:
 | 
			
		||||
            qs = Event.objects.all()
 | 
			
		||||
        else:
 | 
			
		||||
            qs = self.filter.qs
 | 
			
		||||
        startdatetime = datetime.combine(self.c_firstdate, time.min)
 | 
			
		||||
        lastdatetime = datetime.combine(self.c_lastdate, time.max)
 | 
			
		||||
        self.events = qs.filter(
 | 
			
		||||
            (Q(end_day__isnull=True) & Q(start_day__gte=self.c_firstdate) & Q(start_day__lte=self.c_lastdate)) |
 | 
			
		||||
            (Q(end_day__isnull=False) & ~(Q(start_day__gt=self.c_lastdate) | Q(end_day__lt=self.c_firstdate)))
 | 
			
		||||
            (Q(recurrence_dtend__isnull=True) & Q(recurrence_dtstart__lte=lastdatetime)) |
 | 
			
		||||
            (Q(recurrence_dtend__isnull=False) & ~(Q(recurrence_dtstart__gt=lastdatetime) | Q(recurrence_dtend__lt=startdatetime)))
 | 
			
		||||
        ).order_by("start_day", "start_time")
 | 
			
		||||
 | 
			
		||||
        for e in self.events:
 | 
			
		||||
            for d in daterange(e.start_day, e.end_day):
 | 
			
		||||
        firstdate = datetime.fromordinal(self.c_firstdate.toordinal())
 | 
			
		||||
        if firstdate.tzinfo is None or firstdate.tzinfo.utcoffset(firstdate) is None:
 | 
			
		||||
            firstdate = timezone.make_aware(firstdate, timezone.get_default_timezone())
 | 
			
		||||
 | 
			
		||||
                if d.__str__() in self.calendar_days:
 | 
			
		||||
                    self.calendar_days[d.__str__()].add_event(e)
 | 
			
		||||
 | 
			
		||||
        lastdate = datetime.fromordinal(self.c_lastdate.toordinal())
 | 
			
		||||
        if lastdate.tzinfo is None or lastdate.tzinfo.utcoffset(lastdate) is None:
 | 
			
		||||
            lastdate = timezone.make_aware(lastdate, timezone.get_default_timezone())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        for e in self.events:
 | 
			
		||||
            for e_rec in e.get_recurrences_between(firstdate, lastdate):
 | 
			
		||||
                for d in daterange(e_rec.start_day, e_rec.end_day):
 | 
			
		||||
                    if d.__str__() in self.calendar_days:
 | 
			
		||||
                        self.calendar_days[d.__str__()].add_event(e_rec)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def create_calendar_days(self):
 | 
			
		||||
        # create daylist
 | 
			
		||||
        self.calendar_days = {}
 | 
			
		||||
        for d in daterange(self.c_firstdate, self.c_lastdate):
 | 
			
		||||
            self.calendar_days[d.strftime("%Y-%m-%d")] = CalendarDay(d, d >= self.firstdate and d <= self.lastdate)
 | 
			
		||||
            self.calendar_days[d.strftime("%Y-%m-%d")] = DayInCalendar(d, d >= self.firstdate and d <= self.lastdate)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def is_single_week(self):
 | 
			
		||||
@@ -162,3 +208,8 @@ class CalendarWeek(CalendarList):
 | 
			
		||||
    def previous_week(self):
 | 
			
		||||
        return self.firstdate + timedelta(days=-7)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CalendarDay(CalendarList):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, date, filter):
 | 
			
		||||
        super().__init__(date, date, filter, exact=True)
 | 
			
		||||
 
 | 
			
		||||
@@ -51,14 +51,14 @@ def import_events_from_json(self, json):
 | 
			
		||||
 | 
			
		||||
    importer = EventsImporter(self.request.id)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        success, error_message = importer.import_events(json)
 | 
			
		||||
    # try:
 | 
			
		||||
    success, error_message = importer.import_events(json)
 | 
			
		||||
 | 
			
		||||
        # finally, close task
 | 
			
		||||
        close_import_task(self.request.id, success, error_message, importer)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
    # finally, close task
 | 
			
		||||
    close_import_task(self.request.id, success, error_message, importer)
 | 
			
		||||
    """except Exception as e:
 | 
			
		||||
        logger.error(e)
 | 
			
		||||
        close_import_task(self.request.id, False, e, importer)
 | 
			
		||||
        close_import_task(self.request.id, False, e, importer)"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task(bind=True)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,10 @@ import json
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EventsImporter:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, celery_id):
 | 
			
		||||
@@ -55,6 +59,7 @@ class EventsImporter:
 | 
			
		||||
        # get events
 | 
			
		||||
        for event in structure["events"]:
 | 
			
		||||
            # only process events if they are today or the days after
 | 
			
		||||
            
 | 
			
		||||
            if self.event_takes_place_today_or_after(event):
 | 
			
		||||
                # set a default "last modified date"
 | 
			
		||||
                if "last_modified" not in event and self.date is not None:
 | 
			
		||||
@@ -70,6 +75,10 @@ class EventsImporter:
 | 
			
		||||
        return (True, "")
 | 
			
		||||
 | 
			
		||||
    def event_takes_place_today_or_after(self, event):
 | 
			
		||||
        # not optimal, but will work: import recurrent events even if they come from the past
 | 
			
		||||
        if "recurrences" in event:
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        if "start_day" not in event:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
@@ -97,10 +106,12 @@ class EventsImporter:
 | 
			
		||||
 | 
			
		||||
    def load_event(self, event):
 | 
			
		||||
        if self.is_valid_event_structure(event):
 | 
			
		||||
            logger.warning("Valid event: {} {}".format(event["last_modified"], event["title"]))
 | 
			
		||||
            event_obj = Event.from_structure(event, self.url)
 | 
			
		||||
            self.event_objects.append(event_obj)
 | 
			
		||||
            return True
 | 
			
		||||
        else:
 | 
			
		||||
            logger.warning("Not valid event: {}".format(event))
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								src/agenda_culturel/migrations/0023_event_recurrences.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/agenda_culturel/migrations/0023_event_recurrences.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
# Generated by Django 4.2.7 on 2024-01-02 10:13
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
import recurrence.fields
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agenda_culturel', '0022_event_import_sources'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='event',
 | 
			
		||||
            name='recurrences',
 | 
			
		||||
            field=recurrence.fields.RecurrenceField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,28 @@
 | 
			
		||||
# Generated by Django 4.2.7 on 2024-01-04 18:58
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agenda_culturel', '0023_event_recurrences'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='event',
 | 
			
		||||
            name='dtend',
 | 
			
		||||
            field=models.DateTimeField(blank=True, editable=False, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='event',
 | 
			
		||||
            name='dtstart',
 | 
			
		||||
            field=models.DateTimeField(blank=True, editable=False, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='event',
 | 
			
		||||
            name='status',
 | 
			
		||||
            field=models.CharField(choices=[('published', 'Published'), ('draft', 'Draft'), ('trash', 'Trash')], default='draft', max_length=20, verbose_name='Status'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 4.2.7 on 2024-01-04 19:35
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agenda_culturel', '0024_event_dtend_event_dtstart_alter_event_status'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name='event',
 | 
			
		||||
            old_name='dtend',
 | 
			
		||||
            new_name='recurrence_dtend',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name='event',
 | 
			
		||||
            old_name='dtstart',
 | 
			
		||||
            new_name='recurrence_dtstart',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
# Generated by Django 4.2.7 on 2024-01-05 15:23
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
import recurrence.fields
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agenda_culturel', '0025_rename_dtend_event_recurrence_dtend_and_more'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='event',
 | 
			
		||||
            name='recurrences',
 | 
			
		||||
            field=recurrence.fields.RecurrenceField(blank=True, null=True, verbose_name='Recurrence'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										23
									
								
								src/agenda_culturel/migrations/0027_set_dtstart_dtend.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/agenda_culturel/migrations/0027_set_dtstart_dtend.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def forwards_func(apps, schema_editor):
 | 
			
		||||
    Event = apps.get_model("agenda_culturel", "Event")
 | 
			
		||||
    db_alias = schema_editor.connection.alias
 | 
			
		||||
    events = Event.objects.filter(recurrence_dtstart__isnull=True)
 | 
			
		||||
    for e in events:
 | 
			
		||||
        e.update_recurrence_dtstartend()
 | 
			
		||||
    Event.objects.bulk_update(events, ["recurrence_dtstart", "recurrence_dtend"])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agenda_culturel', '0026_alter_event_recurrences'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RunPython(forwards_func),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -12,7 +12,9 @@ from django.core.files import File
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.contrib.postgres.search import TrigramSimilarity
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
 | 
			
		||||
import recurrence.fields
 | 
			
		||||
import recurrence
 | 
			
		||||
import copy
 | 
			
		||||
 | 
			
		||||
from django.template.defaultfilters import date as _date
 | 
			
		||||
from datetime import time, timedelta, date
 | 
			
		||||
@@ -139,6 +141,9 @@ class Event(models.Model):
 | 
			
		||||
    imported_date = models.DateTimeField(blank=True, null=True)
 | 
			
		||||
    modified_date = models.DateTimeField(blank=True, null=True)
 | 
			
		||||
 | 
			
		||||
    recurrence_dtstart = models.DateTimeField(editable=False, blank=True, null=True)
 | 
			
		||||
    recurrence_dtend = models.DateTimeField(editable=False, blank=True, null=True)
 | 
			
		||||
 | 
			
		||||
    title = models.CharField(verbose_name=_('Title'), help_text=_('Short title'), max_length=512)
 | 
			
		||||
 | 
			
		||||
    status = models.CharField(_("Status"), max_length=20, choices=STATUS.choices, default=STATUS.DRAFT)
 | 
			
		||||
@@ -151,6 +156,8 @@ class Event(models.Model):
 | 
			
		||||
    end_day = models.DateField(verbose_name=_('End day of the event'), help_text=_('End day of the event, only required if different from the start day.'), blank=True, null=True)
 | 
			
		||||
    end_time = models.TimeField(verbose_name=_('Final time'), help_text=_('Final time'), blank=True, null=True)
 | 
			
		||||
 | 
			
		||||
    recurrences = recurrence.fields.RecurrenceField(verbose_name=_("Recurrence"), include_dtstart=False, blank=True, null=True)
 | 
			
		||||
 | 
			
		||||
    location = models.CharField(verbose_name=_('Location'), help_text=_('Address of the event'), max_length=512)
 | 
			
		||||
 | 
			
		||||
    description = models.TextField(verbose_name=_('Description'), help_text=_('General description of the event'), blank=True, null=True)
 | 
			
		||||
@@ -198,7 +205,10 @@ class Event(models.Model):
 | 
			
		||||
        return d >= self.start_day and d <= self.get_consolidated_end_day(intuitive)
 | 
			
		||||
 | 
			
		||||
    def get_absolute_url(self):
 | 
			
		||||
        return reverse("view_event", kwargs={"pk": self.pk, "extra": slugify(self.title)})
 | 
			
		||||
        return reverse("view_event", kwargs={"year": self.start_day.year, 
 | 
			
		||||
            "month": self.start_day.month, 
 | 
			
		||||
            "day": self.start_day.day, 
 | 
			
		||||
            "pk": self.pk, "extra": slugify(self.title)})
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return _date(self.start_day) + ": " + self.title
 | 
			
		||||
@@ -255,7 +265,7 @@ class Event(models.Model):
 | 
			
		||||
    def set_in_importation_process(self):
 | 
			
		||||
        self.in_importation_process = True
 | 
			
		||||
 | 
			
		||||
    def update_dates(self):
 | 
			
		||||
    def update_modification_dates(self):
 | 
			
		||||
        now = timezone.now()
 | 
			
		||||
        if not self.id:
 | 
			
		||||
            self.created_date = now
 | 
			
		||||
@@ -265,8 +275,68 @@ class Event(models.Model):
 | 
			
		||||
            self.modified_date = now
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def get_recurrence_at_date(self, year, month, day):
 | 
			
		||||
        dtstart = timezone.make_aware(datetime(year, month, day, 0, 0), timezone.get_default_timezone())
 | 
			
		||||
        recurrences = self.get_recurrences_between(dtstart, dtstart)
 | 
			
		||||
        if len(recurrences) == 0:
 | 
			
		||||
            return self
 | 
			
		||||
        else:
 | 
			
		||||
            return recurrences[0]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    # return a copy of the current object for each recurrence between first an last date (included)
 | 
			
		||||
    def get_recurrences_between(self, firstdate, lastdate):
 | 
			
		||||
        if self.recurrences is None:
 | 
			
		||||
            return [self]
 | 
			
		||||
        else:
 | 
			
		||||
            result = []
 | 
			
		||||
            dtstart = timezone.make_aware(datetime.combine(self.start_day, time()), timezone.get_default_timezone())
 | 
			
		||||
            self.recurrences.dtstart = dtstart
 | 
			
		||||
            for d in self.recurrences.between(firstdate, lastdate, inc=True, dtstart=dtstart):
 | 
			
		||||
                
 | 
			
		||||
                c = copy.deepcopy(self)
 | 
			
		||||
                c.start_day = d.date()
 | 
			
		||||
                if c.end_day is not None:
 | 
			
		||||
                    shift = d.date() - self.start_day
 | 
			
		||||
                    c.end_day += shift
 | 
			
		||||
                result.append(c)
 | 
			
		||||
 | 
			
		||||
            return result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def update_recurrence_dtstartend(self):
 | 
			
		||||
        sday = date.fromisoformat(self.start_day) if isinstance(self.start_day, str) else self.start_day
 | 
			
		||||
        eday = date.fromisoformat(self.end_day) if isinstance(self.end_day, str) else self.end_day
 | 
			
		||||
        stime = time.fromisoformat(self.start_time) if isinstance(self.start_time, str) else time() if self.start_time is None else self.start_time
 | 
			
		||||
        etime = time.fromisoformat(self.end_time) if isinstance(self.end_time, str) else time() if self.end_time is None else self.end_time
 | 
			
		||||
 | 
			
		||||
        self.recurrence_dtstart = datetime.combine(sday, stime)
 | 
			
		||||
 | 
			
		||||
        if self.recurrences is None:
 | 
			
		||||
            if self.end_day is None:
 | 
			
		||||
                self.dtend = None
 | 
			
		||||
            else:
 | 
			
		||||
                self.recurrence_dtend = datetime.combine(eday, etime)
 | 
			
		||||
        else:
 | 
			
		||||
            if self.recurrences.rrules[0].until is None and self.recurrences.rrules[0].count is None:
 | 
			
		||||
                self.recurrence_dtend = None
 | 
			
		||||
            else:
 | 
			
		||||
                self.recurrences.dtstart = datetime.combine(sday, time())
 | 
			
		||||
                occurrence = self.recurrences.occurrences()
 | 
			
		||||
                try:
 | 
			
		||||
                    self.recurrence_dtend = occurrence[-1]
 | 
			
		||||
                    if self.recurrences.dtend is not None and self.recurrences.dtstart is not None:
 | 
			
		||||
                        self.recurrence_dtend += self.recurrences.dtend - self.recurrences.dtstart
 | 
			
		||||
                except:
 | 
			
		||||
                    self.recurrence_dtend = self.recurrence_dtstart
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def prepare_save(self):
 | 
			
		||||
        self.update_dates()
 | 
			
		||||
        self.update_modification_dates()
 | 
			
		||||
 | 
			
		||||
        # TODO: update recurrences.dtstart et recurrences.dtend
 | 
			
		||||
 | 
			
		||||
        self.update_recurrence_dtstartend()
 | 
			
		||||
 | 
			
		||||
        # if the image is defined but not locally downloaded
 | 
			
		||||
        if self.image and not self.local_image:
 | 
			
		||||
@@ -297,6 +367,8 @@ class Event(models.Model):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def from_structure(event_structure, import_source = None):
 | 
			
		||||
        if event_structure["title"].endswith("ole"):
 | 
			
		||||
            logger.warning("on choope {}".format(event_structure))
 | 
			
		||||
        if "category" in event_structure and event_structure["category"] is not None:
 | 
			
		||||
            event_structure["category"] = Category.objects.get(name=event_structure["category"])
 | 
			
		||||
 | 
			
		||||
@@ -316,6 +388,8 @@ class Event(models.Model):
 | 
			
		||||
 | 
			
		||||
        if "last_modified" in event_structure and event_structure["last_modified"] is not None:
 | 
			
		||||
            d = datetime.fromisoformat(event_structure["last_modified"])
 | 
			
		||||
            if d.year == 2024 and d.month > 2:
 | 
			
		||||
                logger.warning("last modified {}".format(d))
 | 
			
		||||
            if d.tzinfo is None or d.tzinfo.utcoffset(d) is None:
 | 
			
		||||
                d = timezone.make_aware(d, timezone.get_default_timezone())
 | 
			
		||||
            event_structure["modified_date"] = d
 | 
			
		||||
@@ -332,6 +406,14 @@ class Event(models.Model):
 | 
			
		||||
        if "description" in event_structure and event_structure["description"] is None:
 | 
			
		||||
            event_structure["description"] = ""
 | 
			
		||||
 | 
			
		||||
        if "recurrences" in event_structure and event_structure["recurrences"] is not None:
 | 
			
		||||
            event_structure["recurrences"] = recurrence.deserialize(event_structure["recurrences"])
 | 
			
		||||
            event_structure["recurrences"].exdates = [e.replace(hour=0, minute=0, second=0) for e in event_structure["recurrences"].exdates]
 | 
			
		||||
            event_structure["recurrences"].rdates = [e.replace(hour=0, minute=0, second=0) for e in event_structure["recurrences"].rdates]
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            event_structure["recurrences"] = None
 | 
			
		||||
 | 
			
		||||
        if import_source is not None:
 | 
			
		||||
            event_structure["import_sources"] = [import_source]
 | 
			
		||||
 | 
			
		||||
@@ -358,6 +440,19 @@ class Event(models.Model):
 | 
			
		||||
        return None if self.uuids is None or len(self.uuids) == 0 else Event.objects.filter(uuids__contains=self.uuids)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def is_generic_uuid(uuid1, uuid2):
 | 
			
		||||
        return uuid1 != "" and uuid2.startswith(uuid1)
 | 
			
		||||
 | 
			
		||||
    def is_generic_by_uuid(self, event):
 | 
			
		||||
        if self.uuids is None or event.uuids is None:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        for s_uuid in self.uuids:
 | 
			
		||||
            for e_uuid in event.uuids:
 | 
			
		||||
                if Event.is_generic_uuid(s_uuid, e_uuid):
 | 
			
		||||
                    return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def get_possibly_duplicated(self):
 | 
			
		||||
        if self.possibly_duplicated is None:
 | 
			
		||||
            return []
 | 
			
		||||
@@ -391,7 +486,7 @@ class Event(models.Model):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def data_fields():
 | 
			
		||||
        return ["title", "location", "start_day", "start_time", "end_day", "end_time", "description", "image", "image_alt", "image_alt", "reference_urls"]
 | 
			
		||||
        return ["title", "location", "start_day", "start_time", "end_day", "end_time", "description", "image", "image_alt", "image_alt", "reference_urls", "recurrences"]
 | 
			
		||||
 | 
			
		||||
    def same_event_by_data(self, other):
 | 
			
		||||
        for attr in Event.data_fields():
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,7 @@ INSTALLED_APPS = [
 | 
			
		||||
    'django_filters',
 | 
			
		||||
    'compressor',
 | 
			
		||||
    'ckeditor',
 | 
			
		||||
    'recurrence',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
MIDDLEWARE = [
 | 
			
		||||
@@ -200,3 +201,7 @@ if os_getenv("EMAIL_BACKEND"):
 | 
			
		||||
# increase upload size for debug experiments
 | 
			
		||||
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 2621440
 | 
			
		||||
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000
 | 
			
		||||
 | 
			
		||||
# recurrence translation
 | 
			
		||||
 | 
			
		||||
RECURRENCE_I18N_URL = "javascript-catalog"
 | 
			
		||||
@@ -641,3 +641,80 @@ aside nav a.badge {
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
  pointer-events: all;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* mise en forme pour les récurrences */
 | 
			
		||||
.container-fluid article form p .recurrence-widget {
 | 
			
		||||
 | 
			
		||||
    .header a, .add-button {
 | 
			
		||||
        @extend [role="button"];
 | 
			
		||||
        margin-right: var(--nav-element-spacing-horizontal);
 | 
			
		||||
 | 
			
		||||
        span.plus {
 | 
			
		||||
            margin-right: var(--nav-element-spacing-horizontal);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    li {
 | 
			
		||||
        list-style: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .freq, .count, .interval, .until {
 | 
			
		||||
        width: 50%;
 | 
			
		||||
        float: left;
 | 
			
		||||
    }
 | 
			
		||||
    .freq, .until {
 | 
			
		||||
        padding-right: .2em;
 | 
			
		||||
    }
 | 
			
		||||
    .interval, .count {
 | 
			
		||||
        padding-left: .2em;
 | 
			
		||||
    }
 | 
			
		||||
    [name="position"], [name="weekday"] {
 | 
			
		||||
        width: 49%;
 | 
			
		||||
    }
 | 
			
		||||
    [name="position"] {
 | 
			
		||||
        float: left;
 | 
			
		||||
    }
 | 
			
		||||
    [name="weekday"] {
 | 
			
		||||
        float: right;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .limit, .monthday {
 | 
			
		||||
        clear: both;
 | 
			
		||||
    }
 | 
			
		||||
    [name="freq"], .date-selector {
 | 
			
		||||
        margin-left: .4em;
 | 
			
		||||
        width: 15em;
 | 
			
		||||
    }
 | 
			
		||||
    [name="interval"], [name="count"] {
 | 
			
		||||
        width: 3em;
 | 
			
		||||
        margin: 0 0.4em;
 | 
			
		||||
    }
 | 
			
		||||
    .control {
 | 
			
		||||
        clear: both;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table.grid {
 | 
			
		||||
        td {
 | 
			
		||||
            @extend [role="button"];
 | 
			
		||||
            background: transparent;
 | 
			
		||||
            color: var(--secondary);
 | 
			
		||||
            border-color: var(--secondary);
 | 
			
		||||
            
 | 
			
		||||
            margin-right: var(--nav-element-spacing-horizontal);
 | 
			
		||||
            width: 5em;
 | 
			
		||||
 | 
			
		||||
            &.active {
 | 
			
		||||
                @extend [role="button"];
 | 
			
		||||
                width: 5em;
 | 
			
		||||
                margin-right: var(--nav-element-spacing-horizontal);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container-fluid article form .hidden {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
{% extends "agenda_culturel/page.html" %}
 | 
			
		||||
{% load static %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Proposer un événement{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block entete_header %}
 | 
			
		||||
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
 | 
			
		||||
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
 | 
			
		||||
<script src="/static/admin/js/jquery.init.js"></script>
 | 
			
		||||
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
 | 
			
		||||
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
 | 
			
		||||
<script src="{% static 'js/adjust_datetimes.js' %}"></script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
<h1>Proposer un événement</h1>
 | 
			
		||||
 | 
			
		||||
<article>
 | 
			
		||||
{% url 'add_event' as local_url %}
 | 
			
		||||
{% include "agenda_culturel/static_content.html" with name="add_event" url_path=local_url %}
 | 
			
		||||
</article>
 | 
			
		||||
 | 
			
		||||
<form method="post">{% csrf_token %}
 | 
			
		||||
    {{ form.as_p }}
 | 
			
		||||
    <input type="submit" value="Enregistrer">
 | 
			
		||||
</form>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -21,6 +21,7 @@
 | 
			
		||||
<h1>Édition de l'événement {{ object.title }} ({{ object.start_day }})</h1>
 | 
			
		||||
 | 
			
		||||
<form method="post">{% csrf_token %}
 | 
			
		||||
    {{ form.media }}
 | 
			
		||||
    {{ form.as_p }}
 | 
			
		||||
    <div class="grid">
 | 
			
		||||
        <a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,20 @@
 | 
			
		||||
 | 
			
		||||
{% block title %}Importer un événement{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% load static %}
 | 
			
		||||
 | 
			
		||||
{% block entete_header %}
 | 
			
		||||
    <script src="{% url 'jsi18n' %}"></script>
 | 
			
		||||
    <script src="/static/admin/js/vendor/jquery/jquery.js"></script>
 | 
			
		||||
    <script src="/static/admin/js/jquery.init.js"></script>
 | 
			
		||||
    <script src="/static/admin/js/admin/DateTimeShortcuts.js"></script>
 | 
			
		||||
 | 
			
		||||
    <script src="{% static 'recurrence/js/recurrence.js' %}"></script>
 | 
			
		||||
    <script src="/static/admin/js/admin/RelatedObjectLookups.js"></script>
 | 
			
		||||
    <script src="{% static 'recurrence/js/recurrence-widget.js' %}"></script>
 | 
			
		||||
    <script src="{% static 'recurrence/js/recurrence-widget.init.js' %}"></script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
@@ -17,8 +31,10 @@
 | 
			
		||||
        <h2>Ajout automatique</h2>
 | 
			
		||||
        <p>Si l'événement est déjà en ligne sur un autre site internet, on essaye de l'importer...</p>
 | 
			
		||||
    </header>
 | 
			
		||||
        <div id="container"></div>
 | 
			
		||||
        <form method="post" action="">
 | 
			
		||||
            {% csrf_token %}
 | 
			
		||||
            {{ form.media }}
 | 
			
		||||
            {{ form.as_p }}
 | 
			
		||||
            <input type="submit" value="Lancer l'import">
 | 
			
		||||
        </form>
 | 
			
		||||
 
 | 
			
		||||
@@ -70,22 +70,26 @@
 | 
			
		||||
        <article>
 | 
			
		||||
            <head>
 | 
			
		||||
                <h2>En résumé</h2>
 | 
			
		||||
                {% regroup events by category as events_by_category %}
 | 
			
		||||
                <nav>
 | 
			
		||||
                    <ul>
 | 
			
		||||
                    {% for category in events_by_category %}
 | 
			
		||||
                    {% with category.grouper.id|stringformat:"i" as idcat %}
 | 
			
		||||
                        {% with filter.get_url_without_filters|add:"?category="|add:idcat as cat_url %}
 | 
			
		||||
                        {% with category.list|length as nb %}
 | 
			
		||||
                            <li>
 | 
			
		||||
                                <a class="small-cat contrast selected" role="button" href="{{ cat_url }}"><span class="cat {{ category.grouper.css_class }}"></span>{{ category.grouper.name }} : {{ category.list|length }}</a>
 | 
			
		||||
                            </li>
 | 
			
		||||
                {% if events|length == 0 %}
 | 
			
		||||
                <p class="remarque">Il n'y a pas d'événement le {{ day }}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                    {% regroup events by category as events_by_category %}
 | 
			
		||||
                    <nav>
 | 
			
		||||
                        <ul>
 | 
			
		||||
                        {% for category in events_by_category %}
 | 
			
		||||
                        {% with category.grouper.id|stringformat:"i" as idcat %}
 | 
			
		||||
                            {% with filter.get_url_without_filters|add:"?category="|add:idcat as cat_url %}
 | 
			
		||||
                            {% with category.list|length as nb %}
 | 
			
		||||
                                <li>
 | 
			
		||||
                                    <a class="small-cat contrast selected" role="button" href="{{ cat_url }}"><span class="cat {{ category.grouper.css_class }}"></span>{{ category.grouper.name }} : {{ category.list|length }}</a>
 | 
			
		||||
                                </li>
 | 
			
		||||
                                {% endwith %}
 | 
			
		||||
                            {% endwith %}
 | 
			
		||||
                        {% endwith %}
 | 
			
		||||
                    {% endwith %}
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </nav>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </nav>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </head>
 | 
			
		||||
        </article>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,13 +13,15 @@
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
<div class="grid two-columns">
 | 
			
		||||
 
 | 
			
		||||
    {% include "agenda_culturel/single-event/event-single-inc.html" with event=event filter=filter %}
 | 
			
		||||
    <aside>
 | 
			
		||||
        <!-- TODO: en même temps -->
 | 
			
		||||
        <article>
 | 
			
		||||
            {% with event.get_nb_events_same_dates as nb_events_same_dates %}
 | 
			
		||||
            {% with nb_events_same_dates|length as c_dates %}
 | 
			
		||||
            <head>
 | 
			
		||||
                <h2>Voir aussi</h2>
 | 
			
		||||
                {% if c_dates != 1 %}
 | 
			
		||||
                <p class="remarque">
 | 
			
		||||
                    Retrouvez ci-dessous tous les événements 
 | 
			
		||||
                    {% if event.is_single_day %}
 | 
			
		||||
@@ -29,16 +31,23 @@
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    que l'événement affiché.
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </head>
 | 
			
		||||
            <nav>
 | 
			
		||||
                <ul>
 | 
			
		||||
                    {% for nbevents_date in event.get_nb_events_same_dates %}
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <a href="{% url 'day_view' nbevents_date.1.year nbevents_date.1.month nbevents_date.1.day %}">{{ nbevents_date.0 }} événement{{ nbevents_date.0 | pluralize }} le {{ nbevents_date.1 }}</a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </ul>
 | 
			
		||||
                {% if c_dates == 1 %}
 | 
			
		||||
                <a role="button" href="{% url 'day_view' nb_events_same_dates.0.1.year nb_events_same_dates.0.1.month nb_events_same_dates.0.1.day %}">Toute la journée</a>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                    <ul>
 | 
			
		||||
                        {% for nbevents_date in nb_events_same_dates %}
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <a href="{% url 'day_view' nbevents_date.1.year nbevents_date.1.month nbevents_date.1.day %}">{{ nbevents_date.0 }} événement{{ nbevents_date.0 | pluralize }} le {{ nbevents_date.1 }}</a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </ul>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </nav>
 | 
			
		||||
            {% endwith %}
 | 
			
		||||
            {% endwith %}
 | 
			
		||||
        </article>
 | 
			
		||||
        {% if event.possibly_duplicated %}
 | 
			
		||||
        <article>
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,14 @@
 | 
			
		||||
        {% if event.end_day %}du{% else %}le{% endif %}
 | 
			
		||||
        {% include "agenda_culturel/date-times-inc.html" with event=event %}
 | 
			
		||||
        {% picto_from_name "map-pin" %}
 | 
			
		||||
    {% if event.recurrences %}
 | 
			
		||||
        <p class="subentry-search">
 | 
			
		||||
        {% picto_from_name "repeat" %}
 | 
			
		||||
        depuis le {{ event.recurrences.dtstart.date }}{% for r in event.recurrences.rrules %},
 | 
			
		||||
        {{ r.to_text }}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
        </p>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {% if event.location_hl %}{{ event.location_hl | safe }}{% else %}{{ event.location }}{% endif %}</p>
 | 
			
		||||
    <p class="subentry-search">
 | 
			
		||||
        {% picto_from_name "tag" %}
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,6 @@
 | 
			
		||||
    {% if event.location %}
 | 
			
		||||
    <h4>
 | 
			
		||||
        {% picto_from_name "map-pin" %}
 | 
			
		||||
 | 
			
		||||
        {{ event.location }}
 | 
			
		||||
        </h4>
 | 
			
		||||
    </hgroup>
 | 
			
		||||
@@ -48,7 +47,7 @@
 | 
			
		||||
        </article>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
    <p>{{ event.description |truncatewords:20 |linebreaks }}</p>
 | 
			
		||||
    <p>{{ event.description |linebreaks }}</p>
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    <footer class="infos-and-buttons">
 | 
			
		||||
@@ -69,7 +68,15 @@
 | 
			
		||||
        {% else %}
 | 
			
		||||
            <p><em>Cet événement est disponible uniquement sur les nuits énimagmatiques.</em></p>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
            {% if event.recurrences %}
 | 
			
		||||
            <p class="footer">
 | 
			
		||||
            {% picto_from_name "repeat" %}
 | 
			
		||||
            depuis le {{ event.recurrences.dtstart.date }}{% for r in event.recurrences.rrules %},
 | 
			
		||||
            {{ r.to_text }}
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
            </p>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        {% if user.is_authenticated %}
 | 
			
		||||
        <div class="buttons">
 | 
			
		||||
            {% include "agenda_culturel/edit-buttons-inc.html" with event=event %}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@
 | 
			
		||||
        {% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %}
 | 
			
		||||
        {{ event.category | small_cat }}
 | 
			
		||||
        <h1>{{ event|picto_status }} {{ event.title }}</h1>
 | 
			
		||||
        <p>
 | 
			
		||||
            {% picto_from_name "calendar" %}
 | 
			
		||||
            {% if event.end_day %}du{% else %}le{% endif %}
 | 
			
		||||
            {% include "agenda_culturel/date-times-inc.html" with event=event %}
 | 
			
		||||
@@ -29,7 +30,8 @@
 | 
			
		||||
    
 | 
			
		||||
    <footer class="infos-and-buttons">
 | 
			
		||||
        <div class="infos">
 | 
			
		||||
            <p>
 | 
			
		||||
 | 
			
		||||
        <p>
 | 
			
		||||
                {% for tag in event.tags %}
 | 
			
		||||
                <a href="{% url 'view_tag' tag %}" role="button" class="small-cat">{{ tag }}</a>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
@@ -44,6 +46,20 @@
 | 
			
		||||
        {% else %}
 | 
			
		||||
            <p><em>À notre connaissance, cet événement n'est pas référencé autre part sur internet.</em></p>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        {% if event.recurrences %}
 | 
			
		||||
        <p class="footer">
 | 
			
		||||
        {% picto_from_name "repeat" %}
 | 
			
		||||
        {% for r in event.recurrences.rrules %}
 | 
			
		||||
        {{ r.to_text }}{% if not forloop.first %}, {% endif %}{% endfor %}, depuis le {{ event.recurrences.dtstart.date }}
 | 
			
		||||
        {% if event.recurrences.exdates|length > 0 %}, sauf 
 | 
			
		||||
            le{{ recurrences.exdates|length|pluralize }}
 | 
			
		||||
            {% for e in event.recurrences.exdates %}{% if not forloop.first %}{% if forloop.last %} et {% else %}, {% endif %}{% endif %}
 | 
			
		||||
            {{ e.date }}{% endfor %}
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        </p>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        <p class="footer">Création : {{ event.created_date }} 
 | 
			
		||||
            {% if event.modified %}
 | 
			
		||||
            — dernière modification : {{ event.modified_date }}
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,6 @@ def picto_status(event):
 | 
			
		||||
 | 
			
		||||
@register.simple_tag
 | 
			
		||||
def show_badges_events():
 | 
			
		||||
    # TODO: seulement ceux dans le futur ?
 | 
			
		||||
    nb_drafts = Event.nb_draft_events()
 | 
			
		||||
    if nb_drafts != 0:
 | 
			
		||||
        return mark_safe('<a href="' + reverse_lazy("moderation") + '?status=draft" class="badge" data-tooltip="' + str(nb_drafts) + ' brouillon' + pluralize(nb_drafts) + ' à valider">' + picto_from_name("calendar") + " " + str(nb_drafts) + '</a>')
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@ from django.contrib import admin
 | 
			
		||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 | 
			
		||||
from django.urls import path, include, re_path
 | 
			
		||||
from django.contrib.auth import views as auth_views
 | 
			
		||||
from django.views.i18n import JavaScriptCatalog
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from .views import *
 | 
			
		||||
 | 
			
		||||
@@ -18,7 +20,7 @@ urlpatterns = [
 | 
			
		||||
    path("tag/<t>/", view_tag, name='view_tag'),
 | 
			
		||||
    path("tags/", tag_list, name='view_all_tags'),
 | 
			
		||||
    path("moderation/", moderation, name='moderation'),
 | 
			
		||||
    path("event/<int:pk>-<extra>", EventDetailView.as_view(), name="view_event"),
 | 
			
		||||
    path("event/<int:year>/<int:month>/<int:day>/<int:pk>-<extra>", EventDetailView.as_view(), name="view_event"),
 | 
			
		||||
    path("event/<int:pk>/edit", EventUpdateView.as_view(), name="edit_event"),
 | 
			
		||||
    path("event/<int:pk>/change-status/<status>", change_status_event, name="change_status_event"),
 | 
			
		||||
    path("event/<int:pk>/delete", EventDeleteView.as_view(), name="delete_event"),
 | 
			
		||||
@@ -42,3 +44,12 @@ urlpatterns = [
 | 
			
		||||
if settings.DEBUG:
 | 
			
		||||
    urlpatterns += staticfiles_urlpatterns()
 | 
			
		||||
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
 | 
			
		||||
 | 
			
		||||
# If you already have a js_info_dict dictionary, just add
 | 
			
		||||
# 'recurrence' to the existing 'packages' tuple.
 | 
			
		||||
js_info_dict = {
 | 
			
		||||
    'packages': ('recurrence', ),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# jsi18n can be anything you like here
 | 
			
		||||
urlpatterns += [ path('jsi18n.js', JavaScriptCatalog.as_view(packages=['recurrence']), name='jsi18n'), ]
 | 
			
		||||
@@ -29,7 +29,7 @@ from django.contrib.auth.decorators import login_required
 | 
			
		||||
from django.contrib import messages
 | 
			
		||||
from django.contrib.messages.views import SuccessMessageMixin
 | 
			
		||||
 | 
			
		||||
from .calendar import CalendarMonth, CalendarWeek 
 | 
			
		||||
from .calendar import CalendarMonth, CalendarWeek, CalendarDay
 | 
			
		||||
from .extractors import ExtractorAllURLs
 | 
			
		||||
 | 
			
		||||
from .celery import app as celery_app, import_events_from_json, import_events_from_url
 | 
			
		||||
@@ -152,9 +152,9 @@ def day_view(request, year = None, month = None, day = None):
 | 
			
		||||
    day = date(year, month, day)
 | 
			
		||||
    
 | 
			
		||||
    filter = EventFilter(request.GET, get_event_qs(request), request=request)
 | 
			
		||||
    events = filter.qs.filter((Q(start_day__lte=day) & (Q(end_day__gte=day)) | Q(start_day=day))).order_by("start_day", F("start_time").desc(nulls_last=True))
 | 
			
		||||
    cday = CalendarDay(day, filter)
 | 
			
		||||
 | 
			
		||||
    context = {"day": day, "events": events, "filter": filter}
 | 
			
		||||
    context = {"day": day, "events": cday.calendar_days_list()[0].events, "filter": filter}
 | 
			
		||||
    return render(request, 'agenda_culturel/page-day.html', context)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -214,6 +214,14 @@ class EventDetailView(UserPassesTestMixin, DetailView):
 | 
			
		||||
    def test_func(self):
 | 
			
		||||
        return self.request.user.is_authenticated or self.get_object().status == Event.STATUS.PUBLISHED
 | 
			
		||||
 | 
			
		||||
    def get_object(self):
 | 
			
		||||
        o = super().get_object()
 | 
			
		||||
        y = self.kwargs["year"]
 | 
			
		||||
        m = self.kwargs["month"]
 | 
			
		||||
        d = self.kwargs["day"]
 | 
			
		||||
        return o.get_recurrence_at_date(y, m, d)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required(login_url="/accounts/login/")
 | 
			
		||||
def change_status_event(request, pk, status):
 | 
			
		||||
    event = get_object_or_404(Event, pk=pk)
 | 
			
		||||
 
 | 
			
		||||
@@ -32,4 +32,4 @@ django-filter==23.3
 | 
			
		||||
django-compressor==4.4
 | 
			
		||||
django-libsass==0.9
 | 
			
		||||
django-ckeditor==6.7.0
 | 
			
		||||
 | 
			
		||||
django-recurrence==1.11.1
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user