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.id = d.strftime("%Y-%m-%d")
self.periods = []
self.has_special_period_same_week = False
def is_in_past(self):
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]
)
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):
if event.contains_date(self.date):
if self.is_ancestor_uuid_event_from_other(event):
@ -276,6 +310,9 @@ class CalendarList:
# create a list of DayInCalendars
self.create_calendar_days()
# get special periods
self.load_special_periods()
# fill DayInCalendars with events
self.fill_calendar_days()
@ -283,6 +320,13 @@ class CalendarList:
for i, c in self.calendar_days.items():
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):
if self.calendar_days is None:
self.build_internal()
@ -388,6 +432,42 @@ class CalendarList:
):
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):
# create daylist
self.calendar_days = {}

View File

@ -20,6 +20,7 @@ from django.forms import (
TextInput,
URLField,
ValidationError,
FileField,
formset_factory,
)
from django.utils.formats import localize
@ -1003,3 +1004,12 @@ class SpecialPeriodForm(ModelForm):
"start_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 datetime import date, time, timedelta
from urllib.parse import urlparse
import icalendar
import emoji
import recurrence
@ -3166,6 +3167,9 @@ class SpecialPeriod(models.Model):
class Meta:
verbose_name = _("Special period")
verbose_name_plural = _("Special periods")
indexes = [
models.Index(fields=["start_date", "end_date"]),
]
class PERIODTYPE(models.TextChoices):
PUBLICHOLIDAYS = "public holidays", _("public holidays")
@ -3193,10 +3197,63 @@ class SpecialPeriod(models.Model):
)
def __str__(self):
n = self.periodtype + ' "' + self.name + '" '
n = self.periodtype + ' "' + self.name + '"'
if self.start_date == self.end_date:
return n + _(" on ") + str(self.start_date)
else:
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;
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>
{% 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>
<a href="{{ day.date | url_day:category }}?{{ filter.get_url }}"
class="visible-link">
@ -136,6 +143,13 @@
{% endif %}
</a>
</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>
{% if day.events %}
<ul>

View File

@ -77,7 +77,14 @@
onClick="toggleModal(event)">{% picto_from_name "calendar" %}</a>
</h2>
<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>
</hgroup>
{% 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>
</h2>
{% 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>
{% else %}
{% if cd.is_tomorrow %}
@ -120,7 +136,30 @@
<h2>
Demain <a href="#{{ cd.id }}" role="button" class="badge">{{ cd.events|length }} {% picto_from_name "calendar" %}</a>
</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>
{% else %}
<h2>
@ -128,6 +167,8 @@
</h2>
{% endif %}
{% endif %}
{% endif %}
{% endif %}
</header>
{% endif %}
{% if cd.events|length > 0 %}

View File

@ -111,10 +111,24 @@
</script>
{% 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>
<a class="visible-link"
href="{{ day.date | url_day:category }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a>
</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>
{% for ti in day.get_time_intervals %}
{% if ti.events|length > 0 %}

View File

@ -21,6 +21,7 @@
<header>
<div class="slide-buttons">
<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>
<h1>Périodes spéciales</h1>
</header>

View File

@ -110,6 +110,7 @@ from .views import (
SpecialPeriodDeleteView,
SpecialPeriodListView,
SpecialPeriodUpdateView,
load_specialperiods_from_ical,
)
event_dict = {
@ -502,6 +503,11 @@ urlpatterns = [
SpecialPeriodDeleteView.as_view(),
name="delete_specialperiod",
),
path(
"specialperiods/load-from-ical",
load_specialperiods_from_ical,
name="load_specialperiods_from_ical",
),
]
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.safestring import mark_safe
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.edit import (
CreateView,
@ -86,6 +86,7 @@ from .forms import (
URLSubmissionFormWithContact,
UserProfileForm,
SpecialPeriodForm,
SpecialPeriodFileForm,
)
from .import_tasks.extractor import Extractor
from .models import (
@ -3169,3 +3170,56 @@ class SpecialPeriodUpdateView(
success_message = _("The special period has been successfully updated.")
success_url = reverse_lazy("list_specialperiods")
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}
)