1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-11-26 18:37:12 +00:00

Export activites as an ICS Calendar

This commit is contained in:
Yohann D'ANELLO 2020-09-04 18:36:20 +02:00
parent 5c7fe716ad
commit 70e1a611dd
3 changed files with 346 additions and 287 deletions

View File

@ -14,4 +14,5 @@ urlpatterns = [
path('<int:pk>/entry/', views.ActivityEntryView.as_view(), name='activity_entry'), path('<int:pk>/entry/', views.ActivityEntryView.as_view(), name='activity_entry'),
path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'), path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'),
path('new/', views.ActivityCreateView.as_view(), name='activity_create'), path('new/', views.ActivityCreateView.as_view(), name='activity_create'),
path('calendar.ics', views.CalendarView.as_view(), name='calendar_ics'),
] ]

View File

@ -1,283 +1,344 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from hashlib import md5
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied from django.contrib.contenttypes.models import ContentType
from django.db.models import F, Q from django.core.exceptions import PermissionDenied
from django.urls import reverse_lazy from django.db.models import F, Q
from django.utils import timezone from django.http import HttpResponse
from django.utils.translation import gettext_lazy as _ from django.urls import reverse_lazy
from django.views.generic import DetailView, TemplateView, UpdateView from django.utils import timezone
from django_tables2.views import SingleTableView from django.utils.translation import gettext_lazy as _
from note.models import Alias, NoteSpecial, NoteUser from django.views import View
from permission.backends import PermissionBackend from django.views.generic import DetailView, TemplateView, UpdateView
from permission.views import ProtectQuerysetMixin, ProtectedCreateView from django_tables2.views import SingleTableView
from note.models import Alias, NoteSpecial, NoteUser
from .forms import ActivityForm, GuestForm from permission.backends import PermissionBackend
from .models import Activity, Entry, Guest from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .tables import ActivityTable, EntryTable, GuestTable
from .forms import ActivityForm, GuestForm
from .models import Activity, Entry, Guest
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView): from .tables import ActivityTable, EntryTable, GuestTable
"""
View to create a new Activity
""" class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
model = Activity """
form_class = ActivityForm View to create a new Activity
extra_context = {"title": _("Create new activity")} """
model = Activity
def get_sample_object(self): form_class = ActivityForm
return Activity( extra_context = {"title": _("Create new activity")}
name="",
description="", def get_sample_object(self):
creater=self.request.user, return Activity(
activity_type_id=1, name="",
organizer_id=1, description="",
attendees_club_id=1, creater=self.request.user,
date_start=timezone.now(), activity_type_id=1,
date_end=timezone.now(), organizer_id=1,
) attendees_club_id=1,
date_start=timezone.now(),
def form_valid(self, form): date_end=timezone.now(),
form.instance.creater = self.request.user )
return super().form_valid(form)
def form_valid(self, form):
def get_success_url(self, **kwargs): form.instance.creater = self.request.user
self.object.refresh_from_db() return super().form_valid(form)
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
"""
Displays all Activities, and classify if they are on-going or upcoming ones.
""" class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
model = Activity """
table_class = ActivityTable Displays all Activities, and classify if they are on-going or upcoming ones.
ordering = ('-date_start',) """
extra_context = {"title": _("Activities")} model = Activity
table_class = ActivityTable
def get_queryset(self): ordering = ('-date_start',)
return super().get_queryset().distinct() extra_context = {"title": _("Activities")}
def get_context_data(self, **kwargs): def get_queryset(self):
context = super().get_context_data(**kwargs) return super().get_queryset().distinct()
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now()) def get_context_data(self, **kwargs):
context['upcoming'] = ActivityTable( context = super().get_context_data(**kwargs)
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
prefix='upcoming-', upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
) context['upcoming'] = ActivityTable(
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
started_activities = Activity.objects\ prefix='upcoming-',
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ )
.filter(open=True, valid=True).all()
context["started_activities"] = started_activities started_activities = Activity.objects\
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
return context .filter(open=True, valid=True).all()
context["started_activities"] = started_activities
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): return context
"""
Shows details about one activity. Add guest to context
""" class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = Activity """
context_object_name = "activity" Shows details about one activity. Add guest to context
extra_context = {"title": _("Activity detail")} """
model = Activity
def get_context_data(self, **kwargs): context_object_name = "activity"
context = super().get_context_data() extra_context = {"title": _("Activity detail")}
table = GuestTable(data=Guest.objects.filter(activity=self.object) def get_context_data(self, **kwargs):
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))) context = super().get_context_data()
context["guests"] = table
table = GuestTable(data=Guest.objects.filter(activity=self.object)
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start) .filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view")))
context["guests"] = table
return context
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): return context
"""
Updates one Activity
""" class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Activity """
form_class = ActivityForm Updates one Activity
extra_context = {"title": _("Update activity")} """
model = Activity
def get_success_url(self, **kwargs): form_class = ActivityForm
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) extra_context = {"title": _("Update activity")}
def get_success_url(self, **kwargs):
class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView): return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
"""
Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm`
""" class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
model = Guest """
form_class = GuestForm Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm`
template_name = "activity/activity_form.html" """
model = Guest
def get_sample_object(self): form_class = GuestForm
""" Creates a standart Guest binds to the Activity""" template_name = "activity/activity_form.html"
activity = Activity.objects.get(pk=self.kwargs["pk"])
return Guest( def get_sample_object(self):
activity=activity, """ Creates a standart Guest binds to the Activity"""
first_name="", activity = Activity.objects.get(pk=self.kwargs["pk"])
last_name="", return Guest(
inviter=self.request.user.note, activity=activity,
) first_name="",
last_name="",
def get_context_data(self, **kwargs): inviter=self.request.user.note,
context = super().get_context_data(**kwargs) )
activity = context["form"].activity
context["title"] = _('Invite guest to the activity "{}"').format(activity.name) def get_context_data(self, **kwargs):
return context context = super().get_context_data(**kwargs)
activity = context["form"].activity
def get_form(self, form_class=None): context["title"] = _('Invite guest to the activity "{}"').format(activity.name)
form = super().get_form(form_class) return context
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.get(pk=self.kwargs["pk"]) def get_form(self, form_class=None):
form.fields["inviter"].initial = self.request.user.note form = super().get_form(form_class)
return form form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.get(pk=self.kwargs["pk"])
def form_valid(self, form): form.fields["inviter"].initial = self.request.user.note
form.instance.activity = Activity.objects\ return form
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"])
return super().form_valid(form) def form_valid(self, form):
form.instance.activity = Activity.objects\
def get_success_url(self, **kwargs): .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"])
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) return super().form_valid(form)
def get_success_url(self, **kwargs):
class ActivityEntryView(LoginRequiredMixin, TemplateView): return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
"""
Manages entry to an activity
""" class ActivityEntryView(LoginRequiredMixin, TemplateView):
template_name = "activity/activity_entry.html" """
Manages entry to an activity
def dispatch(self, request, *args, **kwargs): """
""" template_name = "activity/activity_entry.html"
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
it is closed or doesn't manage entries. def dispatch(self, request, *args, **kwargs):
""" """
activity = Activity.objects.get(pk=self.kwargs["pk"]) Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
it is closed or doesn't manage entries.
sample_entry = Entry(activity=activity, note=self.request.user.note) """
if not PermissionBackend.check_perm(self.request.user, "activity.add_entry", sample_entry): activity = Activity.objects.get(pk=self.kwargs["pk"])
raise PermissionDenied(_("You are not allowed to display the entry interface for this activity."))
sample_entry = Entry(activity=activity, note=self.request.user.note)
if not activity.activity_type.manage_entries: if not PermissionBackend.check_perm(self.request.user, "activity.add_entry", sample_entry):
raise PermissionDenied(_("This activity does not support activity entries.")) raise PermissionDenied(_("You are not allowed to display the entry interface for this activity."))
if not activity.open: if not activity.activity_type.manage_entries:
raise PermissionDenied(_("This activity is closed.")) raise PermissionDenied(_("This activity does not support activity entries."))
return super().dispatch(request, *args, **kwargs)
if not activity.open:
def get_invited_guest(self, activity): raise PermissionDenied(_("This activity is closed."))
""" return super().dispatch(request, *args, **kwargs)
Retrieves all Guests to the activity
""" def get_invited_guest(self, activity):
"""
guest_qs = Guest.objects\ Retrieves all Guests to the activity
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\ """
.filter(activity=activity)\
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\ guest_qs = Guest.objects\
.order_by('last_name', 'first_name').distinct() .annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
.filter(activity=activity)\
if "search" in self.request.GET and self.request.GET["search"]: .filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\
pattern = self.request.GET["search"] .order_by('last_name', 'first_name').distinct()
if pattern[0] != "^":
pattern = "^" + pattern if "search" in self.request.GET and self.request.GET["search"]:
guest_qs = guest_qs.filter( pattern = self.request.GET["search"]
Q(first_name__regex=pattern) if pattern[0] != "^":
| Q(last_name__regex=pattern) pattern = "^" + pattern
| Q(inviter__alias__name__regex=pattern) guest_qs = guest_qs.filter(
| Q(inviter__alias__normalized_name__regex=Alias.normalize(pattern)) Q(first_name__regex=pattern)
) | Q(last_name__regex=pattern)
else: | Q(inviter__alias__name__regex=pattern)
guest_qs = guest_qs.none() | Q(inviter__alias__normalized_name__regex=Alias.normalize(pattern))
return guest_qs )
else:
def get_invited_note(self, activity): guest_qs = guest_qs.none()
""" return guest_qs
Retrieves all Note that can attend the activity,
they need to have an up-to-date membership in the attendees_club. def get_invited_note(self, activity):
""" """
note_qs = Alias.objects.annotate(last_name=F("note__noteuser__user__last_name"), Retrieves all Note that can attend the activity,
first_name=F("note__noteuser__user__first_name"), they need to have an up-to-date membership in the attendees_club.
username=F("note__noteuser__user__username"), """
note_name=F("name"), note_qs = Alias.objects.annotate(last_name=F("note__noteuser__user__last_name"),
balance=F("note__balance")) first_name=F("note__noteuser__user__first_name"),
username=F("note__noteuser__user__username"),
# Keep only users that have a note note_name=F("name"),
note_qs = note_qs.filter(note__noteuser__isnull=False) balance=F("note__balance"))
# Keep only members # Keep only users that have a note
note_qs = note_qs.filter( note_qs = note_qs.filter(note__noteuser__isnull=False)
note__noteuser__user__memberships__club=activity.attendees_club,
note__noteuser__user__memberships__date_start__lte=timezone.now(), # Keep only members
note__noteuser__user__memberships__date_end__gte=timezone.now(), note_qs = note_qs.filter(
) note__noteuser__user__memberships__club=activity.attendees_club,
note__noteuser__user__memberships__date_start__lte=timezone.now(),
# Filter with permission backend note__noteuser__user__memberships__date_end__gte=timezone.now(),
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")) )
if "search" in self.request.GET and self.request.GET["search"]: # Filter with permission backend
pattern = self.request.GET["search"] note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
note_qs = note_qs.filter(
Q(note__noteuser__user__first_name__regex=pattern) if "search" in self.request.GET and self.request.GET["search"]:
| Q(note__noteuser__user__last_name__regex=pattern) pattern = self.request.GET["search"]
| Q(name__regex=pattern) note_qs = note_qs.filter(
| Q(normalized_name__regex=Alias.normalize(pattern)) Q(note__noteuser__user__first_name__regex=pattern)
) | Q(note__noteuser__user__last_name__regex=pattern)
else: | Q(name__regex=pattern)
note_qs = note_qs.none() | Q(normalized_name__regex=Alias.normalize(pattern))
)
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql': else:
note_qs = note_qs.distinct('note__pk')[:20] note_qs = note_qs.none()
else:
# SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql':
# have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page. note_qs = note_qs.distinct('note__pk')[:20]
# In production mode, please use PostgreSQL. else:
note_qs = note_qs.distinct()[:20] # SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only
return note_qs # have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page.
# In production mode, please use PostgreSQL.
def get_context_data(self, **kwargs): note_qs = note_qs.distinct()[:20]
""" return note_qs
Query the list of Guest and Note to the activity and add information to makes entry with JS.
""" def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) """
Query the list of Guest and Note to the activity and add information to makes entry with JS.
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ """
.distinct().get(pk=self.kwargs["pk"]) context = super().get_context_data(**kwargs)
context["activity"] = activity
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
matched = [] .distinct().get(pk=self.kwargs["pk"])
context["activity"] = activity
for guest in self.get_invited_guest(activity):
guest.type = "Invité" matched = []
matched.append(guest)
for guest in self.get_invited_guest(activity):
for note in self.get_invited_note(activity): guest.type = "Invité"
note.type = "Adhérent" matched.append(guest)
note.activity = activity
matched.append(note) for note in self.get_invited_note(activity):
note.type = "Adhérent"
table = EntryTable(data=matched) note.activity = activity
context["table"] = table matched.append(note)
context["entries"] = Entry.objects.filter(activity=activity) table = EntryTable(data=matched)
context["table"] = table
context["title"] = _('Entry for activity "{}"').format(activity.name)
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk context["entries"] = Entry.objects.filter(activity=activity)
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
context["title"] = _('Entry for activity "{}"').format(activity.name)
activities_open = Activity.objects.filter(open=True).filter( context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all() context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
context["activities_open"] = [a for a in activities_open
if PermissionBackend.check_perm(self.request.user, activities_open = Activity.objects.filter(open=True).filter(
"activity.add_entry", PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
Entry(activity=a, note=self.request.user.note,))] context["activities_open"] = [a for a in activities_open
if PermissionBackend.check_perm(self.request.user,
return context "activity.add_entry",
Entry(activity=a, note=self.request.user.note,))]
return context
class CalendarView(View):
"""
Render an ICS calendar with all valid activities.
"""
def multilines(self, string, maxlength, offset=0):
newstring = string[:maxlength - offset]
string = string[maxlength - offset:]
while string:
newstring += "\r\n "
newstring += string[:maxlength - 1]
string = string[maxlength - 1:]
return newstring
def get(self, request, *args, **kwargs):
ics = """BEGIN:VCALENDAR
VERSION: 2.0
PRODID:Note Kfet 2020
X-WR-CALNAME:Kfet Calendar
NAME:Kfet Calendar
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
TZURL:http://tzurl.org/zoneinfo-outlook/Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
"""
for activity in Activity.objects.filter(valid=True).order_by("-date_start").all():
ics += f"""BEGIN:VEVENT
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
UID:{activity.id}
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
DTSTART;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}
DTEND;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_end)}
LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"}
DESCRIPTION;CHARSET=UTF-8:{self.multilines(activity.description, 75, 26)}
-- {activity.organizer.name}
END:VEVENT
"""
ics += "END:VCALENDAR"
ics = ics.replace("\r", "").replace("\n", "\r\n")
return HttpResponse(ics, content_type="text/calendar; charset=UTF-8")

View File

@ -117,10 +117,7 @@ def delete_object(sender, instance, **kwargs):
Each time a model is deleted, an entry in the table `Changelog` is added in the database Each time a model is deleted, an entry in the table `Changelog` is added in the database
""" """
# noinspection PyProtectedMember # noinspection PyProtectedMember
if instance._meta.label_lower in EXCLUDED: if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_log"):
return
if hasattr(instance, "_no_log"):
return return
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP