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

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