Première version fonctionnelle qui gère les événements récurrents.
Fix #7
This commit is contained in:
parent
70d65bfcc1
commit
72da8a7445
@ -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())
|
||||
|
||||
|
||||
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)
|
||||
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:
|
||||
# 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:
|
||||
"""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,6 +70,9 @@
|
||||
<article>
|
||||
<head>
|
||||
<h2>En résumé</h2>
|
||||
{% 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>
|
||||
@ -86,6 +89,7 @@
|
||||
{% 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>
|
||||
{% 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 event.get_nb_events_same_dates %}
|
||||
{% 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">
|
||||
@ -68,6 +67,14 @@
|
||||
</p>
|
||||
{% else %}
|
||||
<p><em>Cet événement est disponible uniquement sur les nuits énimagmatiques.</em></p>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
@ -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,6 +30,7 @@
|
||||
|
||||
<footer class="infos-and-buttons">
|
||||
<div class="infos">
|
||||
|
||||
<p>
|
||||
{% for tag in event.tags %}
|
||||
<a href="{% url 'view_tag' tag %}" role="button" class="small-cat">{{ tag }}</a>
|
||||
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user