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.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()

View File

@ -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)

View File

@ -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)

View File

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

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.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():

View File

@ -46,6 +46,7 @@ INSTALLED_APPS = [
'django_filters',
'compressor',
'ckeditor',
'recurrence',
]
MIDDLEWARE = [
@ -199,4 +200,8 @@ if os_getenv("EMAIL_BACKEND"):
# increase upload size for debug experiments
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

@ -640,4 +640,81 @@ aside nav a.badge {
.django-ckeditor-widget a[role="button"]:not([href]) {
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;
}

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>
<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>

View File

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

View File

@ -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 }}&nbsp;: {{ 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 }}&nbsp;: {{ category.list|length }}</a>
</li>
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
</ul>
</nav>
{% endfor %}
</ul>
</nav>
{% endif %}
</head>
</article>

View File

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

View File

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

View File

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

View File

@ -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&nbsp;: {{ event.created_date }}
{% if event.modified %}
— dernière modification&nbsp;: {{ event.modified_date }}

View File

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

View File

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

View File

@ -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)

View File

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