Fix linebreaks in ICS file

This commit is contained in:
Yohann D'ANELLO 2020-09-04 19:24:48 +02:00
parent 70e1a611dd
commit 6d1b75b9b6
1 changed files with 345 additions and 344 deletions

View File

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