Première version fonctionnelle qui gère les événements récurrents.

Fix #7
This commit is contained in:
Jean-Marie Favreau 2024-01-06 23:08:59 +01:00
parent 70d65bfcc1
commit 72da8a7445
24 changed files with 510 additions and 92 deletions

View File

@ -10,10 +10,11 @@ from selenium import webdriver
from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.options import Options
import icalendar import icalendar
from icalendar import vDatetime
from datetime import datetime, date from datetime import datetime, date
import json import json
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import pickle
class Downloader(ABC): class Downloader(ABC):
@ -77,7 +78,7 @@ class Extractor(ABC):
def clear_events(self): def clear_events(self):
self.events = [] 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: if title is None:
print("ERROR: cannot import an event without name") print("ERROR: cannot import an event without name")
return return
@ -107,6 +108,9 @@ class Extractor(ABC):
if last_modified is not None: if last_modified is not None:
event["last_modified"] = last_modified event["last_modified"] = last_modified
if recurrences is not None:
event["recurrences"] = recurrences
self.events.append(event) self.events.append(event)
def default_value_if_exists(self, default_values, key): 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) last_modified = self.get_item_from_vevent(event, "LAST-MODIFIED", raw = True)
rrule = self.get_item_from_vevent(event, "RRULE", raw = True) recurrence_entries = {}
if rrule is not None: for e in ["RRULE", "EXRULE", "EXDATE", "RDATE"]:
print("Recurrent event not yet supported", rrule) 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() return self.get_structure()

View File

@ -1,6 +1,8 @@
from datetime import datetime, timedelta, date, time from datetime import datetime, timedelta, date, time
import calendar import calendar
from django.db.models import Q from django.db.models import Q
from django.utils import timezone
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -16,7 +18,7 @@ def daterange(start, end, step=timedelta(1)):
curr += step curr += step
class CalendarDay: class DayInCalendar:
midnight = time(23, 59, 59) midnight = time(23, 59, 59)
def __init__(self, d, on_requested_interval = True): def __init__(self, d, on_requested_interval = True):
@ -36,8 +38,40 @@ class CalendarDay:
def is_today(self): def is_today(self):
return self.today 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): def add_event(self, event):
if event.contains_date(self.date): 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) self.events.append(event)
if event.category is None: if event.category is None:
if not "" in self.events_by_category: if not "" in self.events_by_category:
@ -49,7 +83,7 @@ class CalendarDay:
self.events_by_category[event.category.name].append(event) self.events_by_category[event.category.name].append(event)
def filter_events(self): 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: class CalendarList:
@ -70,13 +104,13 @@ class CalendarList:
self.c_lastdate = lastdate + timedelta(days=6-lastdate.weekday()) self.c_lastdate = lastdate + timedelta(days=6-lastdate.weekday())
# create a list of CalendarDays # create a list of DayInCalendars
self.create_calendar_days() self.create_calendar_days()
# fill CalendarDays with events # fill DayInCalendars with events
self.fill_calendar_days() self.fill_calendar_days()
# finally, sort each CalendarDay # finally, sort each DayInCalendar
for i, c in self.calendar_days.items(): for i, c in self.calendar_days.items():
c.filter_events() c.filter_events()
@ -93,23 +127,35 @@ class CalendarList:
qs = Event.objects.all() qs = Event.objects.all()
else: else:
qs = self.filter.qs qs = self.filter.qs
startdatetime = datetime.combine(self.c_firstdate, time.min)
lastdatetime = datetime.combine(self.c_lastdate, time.max)
self.events = qs.filter( self.events = qs.filter(
(Q(end_day__isnull=True) & Q(start_day__gte=self.c_firstdate) & Q(start_day__lte=self.c_lastdate)) | (Q(recurrence_dtend__isnull=True) & Q(recurrence_dtstart__lte=lastdatetime)) |
(Q(end_day__isnull=False) & ~(Q(start_day__gt=self.c_lastdate) | Q(end_day__lt=self.c_firstdate))) (Q(recurrence_dtend__isnull=False) & ~(Q(recurrence_dtstart__gt=lastdatetime) | Q(recurrence_dtend__lt=startdatetime)))
).order_by("start_day", "start_time") ).order_by("start_day", "start_time")
for e in self.events: firstdate = datetime.fromordinal(self.c_firstdate.toordinal())
for d in daterange(e.start_day, e.end_day): 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): def create_calendar_days(self):
# create daylist # create daylist
self.calendar_days = {} self.calendar_days = {}
for d in daterange(self.c_firstdate, self.c_lastdate): 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): def is_single_week(self):
@ -162,3 +208,8 @@ class CalendarWeek(CalendarList):
def previous_week(self): def previous_week(self):
return self.firstdate + timedelta(days=-7) return self.firstdate + timedelta(days=-7)
class CalendarDay(CalendarList):
def __init__(self, date, filter):
super().__init__(date, date, filter, exact=True)

View File

@ -51,14 +51,14 @@ def import_events_from_json(self, json):
importer = EventsImporter(self.request.id) importer = EventsImporter(self.request.id)
try: # try:
success, error_message = importer.import_events(json) success, error_message = importer.import_events(json)
# finally, close task # finally, close task
close_import_task(self.request.id, success, error_message, importer) close_import_task(self.request.id, success, error_message, importer)
except Exception as e: """except Exception as e:
logger.error(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) @app.task(bind=True)

View File

@ -3,6 +3,10 @@ import json
from datetime import datetime from datetime import datetime
from django.utils import timezone from django.utils import timezone
import logging
logger = logging.getLogger(__name__)
class EventsImporter: class EventsImporter:
def __init__(self, celery_id): def __init__(self, celery_id):
@ -55,6 +59,7 @@ class EventsImporter:
# get events # get events
for event in structure["events"]: for event in structure["events"]:
# only process events if they are today or the days after # only process events if they are today or the days after
if self.event_takes_place_today_or_after(event): if self.event_takes_place_today_or_after(event):
# set a default "last modified date" # set a default "last modified date"
if "last_modified" not in event and self.date is not None: if "last_modified" not in event and self.date is not None:
@ -70,6 +75,10 @@ class EventsImporter:
return (True, "") return (True, "")
def event_takes_place_today_or_after(self, event): 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: if "start_day" not in event:
return False return False
@ -97,10 +106,12 @@ class EventsImporter:
def load_event(self, event): def load_event(self, event):
if self.is_valid_event_structure(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) event_obj = Event.from_structure(event, self.url)
self.event_objects.append(event_obj) self.event_objects.append(event_obj)
return True return True
else: else:
logger.warning("Not valid event: {}".format(event))
return False return False

View 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),
),
]

View File

@ -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'),
),
]

View File

@ -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',
),
]

View File

@ -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'),
),
]

View 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),
]

View File

@ -12,7 +12,9 @@ from django.core.files import File
from django.utils import timezone from django.utils import timezone
from django.contrib.postgres.search import TrigramSimilarity from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q from django.db.models import Q
import recurrence.fields
import recurrence
import copy
from django.template.defaultfilters import date as _date from django.template.defaultfilters import date as _date
from datetime import time, timedelta, date from datetime import time, timedelta, date
@ -139,6 +141,9 @@ class Event(models.Model):
imported_date = models.DateTimeField(blank=True, null=True) imported_date = models.DateTimeField(blank=True, null=True)
modified_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) 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) 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_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) 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) 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) 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) return d >= self.start_day and d <= self.get_consolidated_end_day(intuitive)
def get_absolute_url(self): 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): def __str__(self):
return _date(self.start_day) + ": " + self.title return _date(self.start_day) + ": " + self.title
@ -255,7 +265,7 @@ class Event(models.Model):
def set_in_importation_process(self): def set_in_importation_process(self):
self.in_importation_process = True self.in_importation_process = True
def update_dates(self): def update_modification_dates(self):
now = timezone.now() now = timezone.now()
if not self.id: if not self.id:
self.created_date = now self.created_date = now
@ -265,8 +275,68 @@ class Event(models.Model):
self.modified_date = now 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): 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 the image is defined but not locally downloaded
if self.image and not self.local_image: if self.image and not self.local_image:
@ -297,6 +367,8 @@ class Event(models.Model):
def from_structure(event_structure, import_source = None): 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: if "category" in event_structure and event_structure["category"] is not None:
event_structure["category"] = Category.objects.get(name=event_structure["category"]) 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: if "last_modified" in event_structure and event_structure["last_modified"] is not None:
d = datetime.fromisoformat(event_structure["last_modified"]) 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: if d.tzinfo is None or d.tzinfo.utcoffset(d) is None:
d = timezone.make_aware(d, timezone.get_default_timezone()) d = timezone.make_aware(d, timezone.get_default_timezone())
event_structure["modified_date"] = d event_structure["modified_date"] = d
@ -332,6 +406,14 @@ class Event(models.Model):
if "description" in event_structure and event_structure["description"] is None: if "description" in event_structure and event_structure["description"] is None:
event_structure["description"] = "" 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: if import_source is not None:
event_structure["import_sources"] = [import_source] 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) 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): def get_possibly_duplicated(self):
if self.possibly_duplicated is None: if self.possibly_duplicated is None:
return [] return []
@ -391,7 +486,7 @@ class Event(models.Model):
def data_fields(): 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): def same_event_by_data(self, other):
for attr in Event.data_fields(): for attr in Event.data_fields():

View File

@ -46,6 +46,7 @@ INSTALLED_APPS = [
'django_filters', 'django_filters',
'compressor', 'compressor',
'ckeditor', 'ckeditor',
'recurrence',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -200,3 +201,7 @@ if os_getenv("EMAIL_BACKEND"):
# increase upload size for debug experiments # increase upload size for debug experiments
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 2621440 DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 2621440
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000 DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000
# recurrence translation
RECURRENCE_I18N_URL = "javascript-catalog"

View File

@ -641,3 +641,80 @@ aside nav a.badge {
opacity: 1; opacity: 1;
pointer-events: all; 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;
}

View File

@ -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 %}

View File

@ -21,6 +21,7 @@
<h1>Édition de l'événement {{ object.title }} ({{ object.start_day }})</h1> <h1>Édition de l'événement {{ object.title }} ({{ object.start_day }})</h1>
<form method="post">{% csrf_token %} <form method="post">{% csrf_token %}
{{ form.media }}
{{ form.as_p }} {{ form.as_p }}
<div class="grid"> <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> <a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>

View File

@ -2,6 +2,20 @@
{% block title %}Importer un événement{% endblock %} {% 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 %} {% block content %}
@ -17,8 +31,10 @@
<h2>Ajout automatique</h2> <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> <p>Si l'événement est déjà en ligne sur un autre site internet, on essaye de l'importer...</p>
</header> </header>
<div id="container"></div>
<form method="post" action=""> <form method="post" action="">
{% csrf_token %} {% csrf_token %}
{{ form.media }}
{{ form.as_p }} {{ form.as_p }}
<input type="submit" value="Lancer l'import"> <input type="submit" value="Lancer l'import">
</form> </form>

View File

@ -70,22 +70,26 @@
<article> <article>
<head> <head>
<h2>En résumé</h2> <h2>En résumé</h2>
{% regroup events by category as events_by_category %} {% if events|length == 0 %}
<nav> <p class="remarque">Il n'y a pas d'événement le {{ day }}</p>
<ul> {% else %}
{% for category in events_by_category %} {% regroup events by category as events_by_category %}
{% with category.grouper.id|stringformat:"i" as idcat %} <nav>
{% with filter.get_url_without_filters|add:"?category="|add:idcat as cat_url %} <ul>
{% with category.list|length as nb %} {% for category in events_by_category %}
<li> {% with category.grouper.id|stringformat:"i" as idcat %}
<a class="small-cat contrast selected" role="button" href="{{ cat_url }}"><span class="cat {{ category.grouper.css_class }}"></span>{{ category.grouper.name }}&nbsp;: {{ category.list|length }}</a> {% with filter.get_url_without_filters|add:"?category="|add:idcat as cat_url %}
</li> {% 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 }}&nbsp;: {{ category.list|length }}</a>
</li>
{% endwith %}
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
{% endwith %} {% endfor %}
{% endfor %} </ul>
</ul> </nav>
</nav> {% endif %}
</head> </head>
</article> </article>

View File

@ -13,13 +13,15 @@
{% block content %} {% block content %}
<div class="grid two-columns"> <div class="grid two-columns">
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event filter=filter %} {% include "agenda_culturel/single-event/event-single-inc.html" with event=event filter=filter %}
<aside> <aside>
<!-- TODO: en même temps --> <!-- TODO: en même temps -->
<article> <article>
{% with event.get_nb_events_same_dates as nb_events_same_dates %}
{% with nb_events_same_dates|length as c_dates %}
<head> <head>
<h2>Voir aussi</h2> <h2>Voir aussi</h2>
{% if c_dates != 1 %}
<p class="remarque"> <p class="remarque">
Retrouvez ci-dessous tous les événements Retrouvez ci-dessous tous les événements
{% if event.is_single_day %} {% if event.is_single_day %}
@ -29,16 +31,23 @@
{% endif %} {% endif %}
que l'événement affiché. que l'événement affiché.
</p> </p>
{% endif %}
</head> </head>
<nav> <nav>
<ul> {% if c_dates == 1 %}
{% for nbevents_date in event.get_nb_events_same_dates %} <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>
<li> {% else %}
<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> <ul>
</li> {% for nbevents_date in nb_events_same_dates %}
{% endfor %} <li>
</ul> <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> </nav>
{% endwith %}
{% endwith %}
</article> </article>
{% if event.possibly_duplicated %} {% if event.possibly_duplicated %}
<article> <article>

View File

@ -10,6 +10,14 @@
{% if event.end_day %}du{% else %}le{% endif %} {% if event.end_day %}du{% else %}le{% endif %}
{% include "agenda_culturel/date-times-inc.html" with event=event %} {% include "agenda_culturel/date-times-inc.html" with event=event %}
{% picto_from_name "map-pin" %} {% 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> {% if event.location_hl %}{{ event.location_hl | safe }}{% else %}{{ event.location }}{% endif %}</p>
<p class="subentry-search"> <p class="subentry-search">
{% picto_from_name "tag" %} {% picto_from_name "tag" %}

View File

@ -28,7 +28,6 @@
{% if event.location %} {% if event.location %}
<h4> <h4>
{% picto_from_name "map-pin" %} {% picto_from_name "map-pin" %}
{{ event.location }} {{ event.location }}
</h4> </h4>
</hgroup> </hgroup>
@ -48,7 +47,7 @@
</article> </article>
{% endif %} {% endif %}
<p>{{ event.description |truncatewords:20 |linebreaks }}</p> <p>{{ event.description |linebreaks }}</p>
<footer class="infos-and-buttons"> <footer class="infos-and-buttons">
@ -69,7 +68,15 @@
{% else %} {% else %}
<p><em>Cet événement est disponible uniquement sur les nuits énimagmatiques.</em></p> <p><em>Cet événement est disponible uniquement sur les nuits énimagmatiques.</em></p>
{% endif %} {% 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 %} {% if user.is_authenticated %}
<div class="buttons"> <div class="buttons">
{% include "agenda_culturel/edit-buttons-inc.html" with event=event %} {% include "agenda_culturel/edit-buttons-inc.html" with event=event %}

View File

@ -9,6 +9,7 @@
{% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %} {% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %}
{{ event.category | small_cat }} {{ event.category | small_cat }}
<h1>{{ event|picto_status }} {{ event.title }}</h1> <h1>{{ event|picto_status }} {{ event.title }}</h1>
<p>
{% picto_from_name "calendar" %} {% picto_from_name "calendar" %}
{% if event.end_day %}du{% else %}le{% endif %} {% if event.end_day %}du{% else %}le{% endif %}
{% include "agenda_culturel/date-times-inc.html" with event=event %} {% include "agenda_culturel/date-times-inc.html" with event=event %}
@ -29,7 +30,8 @@
<footer class="infos-and-buttons"> <footer class="infos-and-buttons">
<div class="infos"> <div class="infos">
<p>
<p>
{% for tag in event.tags %} {% for tag in event.tags %}
<a href="{% url 'view_tag' tag %}" role="button" class="small-cat">{{ tag }}</a> <a href="{% url 'view_tag' tag %}" role="button" class="small-cat">{{ tag }}</a>
{% endfor %} {% endfor %}
@ -44,6 +46,20 @@
{% else %} {% else %}
<p><em>À notre connaissance, cet événement n'est pas référencé autre part sur internet.</em></p> <p><em>À notre connaissance, cet événement n'est pas référencé autre part sur internet.</em></p>
{% endif %} {% 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&nbsp;: {{ event.created_date }} <p class="footer">Création&nbsp;: {{ event.created_date }}
{% if event.modified %} {% if event.modified %}
— dernière modification&nbsp;: {{ event.modified_date }} — dernière modification&nbsp;: {{ event.modified_date }}

View File

@ -45,7 +45,6 @@ def picto_status(event):
@register.simple_tag @register.simple_tag
def show_badges_events(): def show_badges_events():
# TODO: seulement ceux dans le futur ?
nb_drafts = Event.nb_draft_events() nb_drafts = Event.nb_draft_events()
if nb_drafts != 0: 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>') 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>')

View File

@ -4,6 +4,8 @@ from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path, include, re_path from django.urls import path, include, re_path
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.views.i18n import JavaScriptCatalog
from .views import * from .views import *
@ -18,7 +20,7 @@ urlpatterns = [
path("tag/<t>/", view_tag, name='view_tag'), path("tag/<t>/", view_tag, name='view_tag'),
path("tags/", tag_list, name='view_all_tags'), path("tags/", tag_list, name='view_all_tags'),
path("moderation/", moderation, name='moderation'), 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>/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>/change-status/<status>", change_status_event, name="change_status_event"),
path("event/<int:pk>/delete", EventDeleteView.as_view(), name="delete_event"), path("event/<int:pk>/delete", EventDeleteView.as_view(), name="delete_event"),
@ -42,3 +44,12 @@ urlpatterns = [
if settings.DEBUG: if settings.DEBUG:
urlpatterns += staticfiles_urlpatterns() urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 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'), ]

View File

@ -29,7 +29,7 @@ from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from .calendar import CalendarMonth, CalendarWeek from .calendar import CalendarMonth, CalendarWeek, CalendarDay
from .extractors import ExtractorAllURLs from .extractors import ExtractorAllURLs
from .celery import app as celery_app, import_events_from_json, import_events_from_url 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) day = date(year, month, day)
filter = EventFilter(request.GET, get_event_qs(request), request=request) 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) return render(request, 'agenda_culturel/page-day.html', context)
@ -214,6 +214,14 @@ class EventDetailView(UserPassesTestMixin, DetailView):
def test_func(self): def test_func(self):
return self.request.user.is_authenticated or self.get_object().status == Event.STATUS.PUBLISHED 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/") @login_required(login_url="/accounts/login/")
def change_status_event(request, pk, status): def change_status_event(request, pk, status):
event = get_object_or_404(Event, pk=pk) event = get_object_or_404(Event, pk=pk)

View File

@ -32,4 +32,4 @@ django-filter==23.3
django-compressor==4.4 django-compressor==4.4
django-libsass==0.9 django-libsass==0.9
django-ckeditor==6.7.0 django-ckeditor==6.7.0
django-recurrence==1.11.1