Ajout des affichages de périodes remarquables

This commit is contained in:
Jean-Marie Favreau 2025-04-06 12:44:29 +02:00
parent 60b0255942
commit 9fa3a946b7
12 changed files with 724 additions and 387 deletions

View File

@ -44,6 +44,9 @@ class DayInCalendar:
self.time_intervals = None self.time_intervals = None
self.id = d.strftime("%Y-%m-%d") self.id = d.strftime("%Y-%m-%d")
self.periods = []
self.has_special_period_same_week = False
def is_in_past(self): def is_in_past(self):
return self.in_past return self.in_past
@ -77,6 +80,37 @@ class DayInCalendar:
[(k, v) for k, v in self.events_by_category.items() if len(v) > 0] [(k, v) for k, v in self.events_by_category.items() if len(v) > 0]
) )
def add_special_period(self, period):
self.periods.append(period)
def is_public_holiday(self):
from .models import SpecialPeriod
return (
len(
[
p
for p in self.periods
if p.periodtype == SpecialPeriod.PERIODTYPE.PUBLICHOLIDAYS
]
)
> 0
)
def is_school_vacation(self):
from .models import SpecialPeriod
return (
len(
[
p
for p in self.periods
if p.periodtype == SpecialPeriod.PERIODTYPE.SCHOOLVACATIONS
]
)
> 0
)
def add_event(self, event): def add_event(self, event):
if event.contains_date(self.date): if event.contains_date(self.date):
if self.is_ancestor_uuid_event_from_other(event): if self.is_ancestor_uuid_event_from_other(event):
@ -276,6 +310,9 @@ class CalendarList:
# create a list of DayInCalendars # create a list of DayInCalendars
self.create_calendar_days() self.create_calendar_days()
# get special periods
self.load_special_periods()
# fill DayInCalendars with events # fill DayInCalendars with events
self.fill_calendar_days() self.fill_calendar_days()
@ -283,6 +320,13 @@ class CalendarList:
for i, c in self.calendar_days.items(): for i, c in self.calendar_days.items():
c.filter_events() c.filter_events()
def load_special_periods(self):
from .models import SpecialPeriod
self.special_periods = SpecialPeriod.objects.filter(
Q(start_date__lte=self.lastdate) & Q(end_date__gte=self.firstdate)
).order_by("-periodtype")
def get_calendar_days(self): def get_calendar_days(self):
if self.calendar_days is None: if self.calendar_days is None:
self.build_internal() self.build_internal()
@ -388,6 +432,42 @@ class CalendarList:
): ):
self.calendar_days[d.__str__()].add_event(e_rec) self.calendar_days[d.__str__()].add_event(e_rec)
for p in self.special_periods:
first_date = max(p.start_date, self.firstdate)
end_date = min(p.end_date, self.lastdate)
current_date = first_date
while current_date <= end_date:
self.calendar_days[current_date.__str__()].add_special_period(p)
current_date += timedelta(days=1)
start_week = first_date - timedelta(days=first_date.weekday())
end_week = end_date + timedelta(end_date.weekday())
c = start_week
while c < p.start_date:
if c.__str__() in self.calendar_days:
self.calendar_days[c.__str__()].has_special_period_same_week = True
c += timedelta(days=1)
c = p.end_date + timedelta(days=1)
while c <= end_week:
if c.__str__() in self.calendar_days:
self.calendar_days[c.__str__()].has_special_period_same_week = True
c += timedelta(days=1)
def has_school_vacation(self):
from .models import SpecialPeriod
return (
len(
[
p
for p in self.special_periods
if p.periodtype == SpecialPeriod.PERIODTYPE.SCHOOLVACATIONS
]
)
> 0
)
def create_calendar_days(self): def create_calendar_days(self):
# create daylist # create daylist
self.calendar_days = {} self.calendar_days = {}

View File

@ -20,6 +20,7 @@ from django.forms import (
TextInput, TextInput,
URLField, URLField,
ValidationError, ValidationError,
FileField,
formset_factory, formset_factory,
) )
from django.utils.formats import localize from django.utils.formats import localize
@ -1003,3 +1004,12 @@ class SpecialPeriodForm(ModelForm):
"start_date": TextInput(attrs={"type": "date"}), "start_date": TextInput(attrs={"type": "date"}),
"end_date": TextInput(attrs={"type": "date"}), "end_date": TextInput(attrs={"type": "date"}),
} }
class SpecialPeriodFileForm(Form):
periodtype = ChoiceField(
label=_("Period type"),
required=True,
choices=SpecialPeriod.PERIODTYPE.choices,
)
file = FileField(label=_("ICAL file"), required=True)

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ import uuid
from collections import defaultdict from collections import defaultdict
from datetime import date, time, timedelta from datetime import date, time, timedelta
from urllib.parse import urlparse from urllib.parse import urlparse
import icalendar
import emoji import emoji
import recurrence import recurrence
@ -3166,6 +3167,9 @@ class SpecialPeriod(models.Model):
class Meta: class Meta:
verbose_name = _("Special period") verbose_name = _("Special period")
verbose_name_plural = _("Special periods") verbose_name_plural = _("Special periods")
indexes = [
models.Index(fields=["start_date", "end_date"]),
]
class PERIODTYPE(models.TextChoices): class PERIODTYPE(models.TextChoices):
PUBLICHOLIDAYS = "public holidays", _("public holidays") PUBLICHOLIDAYS = "public holidays", _("public holidays")
@ -3193,10 +3197,63 @@ class SpecialPeriod(models.Model):
) )
def __str__(self): def __str__(self):
n = self.periodtype + ' "' + self.name + '" ' n = self.periodtype + ' "' + self.name + '"'
if self.start_date == self.end_date: if self.start_date == self.end_date:
return n + _(" on ") + str(self.start_date) return n + _(" on ") + str(self.start_date)
else: else:
return ( return (
n + _(" from ") + str(self.start_date) + _(" to ") + str(self.end_day) n + _(" from ") + str(self.start_date) + _(" to ") + str(self.end_date)
) )
def load_from_ical(uploaded_file, periodtype):
# load file
try:
file_content = uploaded_file.read()
calendar = icalendar.Calendar.from_ical(file_content)
nb_error = 0
nb_overlap = 0
periods = []
periods_to_create = []
# extract events
for event in calendar.walk("VEVENT"):
try:
name = event.decoded("SUMMARY").decode()
r = event.decoded("DTSTART")
if isinstance(r, datetime):
start_date = r.date()
elif isinstance(r, date):
start_date = r
r = event.decoded("DTEND")
if isinstance(r, datetime):
end_date = r.date()
elif isinstance(r, date):
end_date = r
periods.append(
SpecialPeriod(
name=name,
periodtype=periodtype,
start_date=start_date,
end_date=end_date,
)
)
except Exception:
nb_error += 1
for p in periods:
overlap_exists = SpecialPeriod.objects.filter(
Q(periodtype=p.periodtype)
& Q(start_date__lte=p.end_date)
& Q(end_date__gte=p.start_date)
).exists()
if overlap_exists:
nb_overlap += 1
else:
periods_to_create.append(p)
SpecialPeriod.objects.bulk_create(periods_to_create)
return len(periods_to_create), nb_overlap, nb_error, None
except Exception as e:
return 0, 0, 0, str(e)

View File

@ -2193,3 +2193,15 @@ dialog {
@extend article; @extend article;
font-size: 110%; font-size: 110%;
} }
.special_period {
font-size: 75%;
margin: 0;
text-align: center;
height: 1.5em;
border-radius: var(--border-radius);
}
.special_period.holiday,
.special_period.vacation {
background: var(--primary);
color: var(--primary-inverse);
}

View File

@ -0,0 +1,19 @@
{% extends "agenda_culturel/page.html" %}
{% block title %}
{% block og_title %}Importer des périodes remarquables depuis un fichier ical{% endblock %}
{% endblock %}
{% block fluid %}{% endblock %}
{% block content %}
<h1>Importer des périodes remarquables depuis un fichier ical</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{% url 'list_specialperiods' %}{% endif %}"
role="button"
class="secondary">Annuler</a>
<input type="submit" value="Enregistrer">
</div>
</form>
{{ form.media }}
{% endblock %}

View File

@ -126,6 +126,13 @@
</script> </script>
{% endif %} {% endif %}
<header {% if day.is_today %}id="today"{% endif %}> <header {% if day.is_today %}id="today"{% endif %}>
{% if day.is_school_vacation or day.has_special_period_same_week %}
<div class="special_period{% if day.is_school_vacation %} vacation{% endif %}">
{% for p in day.periods %}
{% if p.periodtype == "school vacations" %}{{ p.name }}{% endif %}
{% endfor %}
</div>
{% endif %}
<h3> <h3>
<a href="{{ day.date | url_day:category }}?{{ filter.get_url }}" <a href="{{ day.date | url_day:category }}?{{ filter.get_url }}"
class="visible-link"> class="visible-link">
@ -136,6 +143,13 @@
{% endif %} {% endif %}
</a> </a>
</h3> </h3>
{% if day.is_public_holiday %}
<div class="special_period holiday">
{% for p in day.periods %}
{% if p.periodtype == "public holidays" %}{{ p.name }}{% endif %}
{% endfor %}
</div>
{% endif %}
</header> </header>
{% if day.events %} {% if day.events %}
<ul> <ul>

View File

@ -77,7 +77,14 @@
onClick="toggleModal(event)">{% picto_from_name "calendar" %}</a> onClick="toggleModal(event)">{% picto_from_name "calendar" %}</a>
</h2> </h2>
<h3> <h3>
{% if cd.is_today or cd.is_tomorrow %}{{ cd.date|date:"l j F Y"|frdate }}{% endif %} {% if cd.is_today or cd.is_tomorrow %}
{{ cd.date|date:"l j F Y"|frdate }}
{% if cd.periods|length > 0 %}<br>{% endif %}
{% endif %}
{% for p in cd.periods %}
{{ p.name }}
{% if not forloop.last %},{% endif %}
{% endfor %}
</h3> </h3>
</hgroup> </hgroup>
{% endwith %} {% endwith %}
@ -112,7 +119,16 @@
Aujourd'hui <a href="#{{ cd.id }}" role="button" class="badge">{{ nb_events }} événement{{ nb_events|pluralize }} {% picto_from_name "calendar" %}</a> Aujourd'hui <a href="#{{ cd.id }}" role="button" class="badge">{{ nb_events }} événement{{ nb_events|pluralize }} {% picto_from_name "calendar" %}</a>
</h2> </h2>
{% endwith %} {% endwith %}
<h3>{{ cd.date|date:"l j F Y"|frdate }}</h3> <h3>
{{ cd.date|date:"l j F Y"|frdate }}
{% if cd.periods|length > 0 %}
<br>
{% for p in cd.periods %}
{{ p.name }}
{% if not forloop.last %},{% endif %}
{% endfor %}
{% endif %}
</h3>
</hgroup> </hgroup>
{% else %} {% else %}
{% if cd.is_tomorrow %} {% if cd.is_tomorrow %}
@ -120,7 +136,30 @@
<h2> <h2>
Demain <a href="#{{ cd.id }}" role="button" class="badge">{{ cd.events|length }} {% picto_from_name "calendar" %}</a> Demain <a href="#{{ cd.id }}" role="button" class="badge">{{ cd.events|length }} {% picto_from_name "calendar" %}</a>
</h2> </h2>
<h3>{{ cd.date|date:"l j F Y"|frdate }}</h3> <h3>
{{ cd.date|date:"l j F Y"|frdate }}
{% if cd.periods|length > 0 %}
<br>
{% for p in cd.periods %}
{{ p.name }}
{% if not forloop.last %},{% endif %}
{% endfor %}
{% endif %}
</h3>
</hgroup>
{% else %}
{% if cd.periods|length > 0 %}
<hgroup>
<h2>
{{ cd.date|date:"l j F Y"|frdate |capfirst }} <a href="#{{ cd.id }}" role="button" class="badge">{{ cd.events|length }} {% picto_from_name "calendar" %}</a>
</h2>
<h3>
{% if cd.periods|length > 0 %}
{% for p in cd.periods %}
{{ p.name }}
{% if not forloop.last %},{% endif %}
{% endfor %}
</h3>
</hgroup> </hgroup>
{% else %} {% else %}
<h2> <h2>
@ -128,6 +167,8 @@
</h2> </h2>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %}
{% endif %}
</header> </header>
{% endif %} {% endif %}
{% if cd.events|length > 0 %} {% if cd.events|length > 0 %}

View File

@ -111,10 +111,24 @@
</script> </script>
{% endif %} {% endif %}
<header {% if day.is_today %}id="today"{% endif %}> <header {% if day.is_today %}id="today"{% endif %}>
{% if day.is_school_vacation or calendar.has_school_vacation %}
<div class="special_period{% if day.is_school_vacation %} vacation{% endif %}">
{% for p in day.periods %}
{% if p.periodtype == "school vacations" %}{{ p.name }}{% endif %}
{% endfor %}
</div>
{% endif %}
<h2> <h2>
<a class="visible-link" <a class="visible-link"
href="{{ day.date | url_day:category }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a> href="{{ day.date | url_day:category }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a>
</h2> </h2>
{% if day.is_public_holiday %}
<div class="special_period holiday">
{% for p in day.periods %}
{% if p.periodtype == "public holidays" %}{{ p.name }}{% endif %}
{% endfor %}
</div>
{% endif %}
</header> </header>
{% for ti in day.get_time_intervals %} {% for ti in day.get_time_intervals %}
{% if ti.events|length > 0 %} {% if ti.events|length > 0 %}

View File

@ -21,6 +21,7 @@
<header> <header>
<div class="slide-buttons"> <div class="slide-buttons">
<a href="{% url 'add_specialperiod' %}" role="button">Ajouter {% picto_from_name "plus-circle" %}</a> <a href="{% url 'add_specialperiod' %}" role="button">Ajouter {% picto_from_name "plus-circle" %}</a>
<a href="{% url 'load_specialperiods_from_ical' %}" role="button">Importer (ical) {% picto_from_name "upload" %}</a>
</div> </div>
<h1>Périodes spéciales</h1> <h1>Périodes spéciales</h1>
</header> </header>

View File

@ -110,6 +110,7 @@ from .views import (
SpecialPeriodDeleteView, SpecialPeriodDeleteView,
SpecialPeriodListView, SpecialPeriodListView,
SpecialPeriodUpdateView, SpecialPeriodUpdateView,
load_specialperiods_from_ical,
) )
event_dict = { event_dict = {
@ -502,6 +503,11 @@ urlpatterns = [
SpecialPeriodDeleteView.as_view(), SpecialPeriodDeleteView.as_view(),
name="delete_specialperiod", name="delete_specialperiod",
), ),
path(
"specialperiods/load-from-ical",
load_specialperiods_from_ical,
name="load_specialperiods_from_ical",
),
] ]
if settings.DEBUG: if settings.DEBUG:

View File

@ -29,7 +29,7 @@ from django.utils.decorators import method_decorator
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.timezone import datetime from django.utils.timezone import datetime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _, ngettext
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from django.views.generic.edit import ( from django.views.generic.edit import (
CreateView, CreateView,
@ -86,6 +86,7 @@ from .forms import (
URLSubmissionFormWithContact, URLSubmissionFormWithContact,
UserProfileForm, UserProfileForm,
SpecialPeriodForm, SpecialPeriodForm,
SpecialPeriodFileForm,
) )
from .import_tasks.extractor import Extractor from .import_tasks.extractor import Extractor
from .models import ( from .models import (
@ -3169,3 +3170,56 @@ class SpecialPeriodUpdateView(
success_message = _("The special period has been successfully updated.") success_message = _("The special period has been successfully updated.")
success_url = reverse_lazy("list_specialperiods") success_url = reverse_lazy("list_specialperiods")
form_class = SpecialPeriodForm form_class = SpecialPeriodForm
def load_specialperiods_from_ical(request):
if request.method == "POST":
form = SpecialPeriodFileForm(request.POST, request.FILES)
if form.is_valid():
nb_created, nb_overlap, nb_error, error = SpecialPeriod.load_from_ical(
request.FILES["file"], request.POST["periodtype"]
)
if nb_created > 0:
messages.success(
request,
ngettext(
"%(nb_created)d interval inserted.",
"%(nb_created)d intervals inserted.",
nb_created,
)
% {"nb_created": nb_created},
)
if nb_overlap > 0:
messages.success(
request,
ngettext(
"%(nb_overlap)d insersion was not possible due to overlap.",
"%(nb_overlap)d insersion were not possible due to overlap.",
nb_overlap,
)
% {"nb_overlap": nb_overlap},
)
if nb_error > 0:
messages.success(
request,
ngettext(
"%(nb_error)d error while reading ical file.",
"%(nb_error)d error while reading ical file.",
nb_error,
)
% {"nb_error": nb_error},
)
if error is not None:
messages.success(
request, _("Error during file reading: {}").format(error)
)
return HttpResponseRedirect(reverse_lazy("list_specialperiods"))
else:
form = SpecialPeriodFileForm()
return render(
request, "agenda_culturel/load_specialperiods_from_ical.html", {"form": form}
)