Newer
Older
Benjamin Hättasch
committed
import datetime
import json
import os
import tempfile
from itertools import zip_longest
from django.contrib import messages
from django.db.models.functions import Now
from django.utils.dateparse import parse_datetime
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView, DetailView
Benjamin Hättasch
committed
from django_tex.core import render_template_with_context, run_tex_in_directory
from django_tex.response import PDFResponse
from AKModel.forms import SlideExportForm, DefaultSlotEditorForm
from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin
from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner
Benjamin Hättasch
committed
class UserView(TemplateView):
"""
View: Start page for logged in user
Will over a link to backend or inform the user that their account still needs to be confirmed
"""
Benjamin Hättasch
committed
template_name = "AKModel/user.html"
class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
"""
View: Export slides to present AKs
Over a form to choose some settings for the export and then generate the PDF
"""
Benjamin Hättasch
committed
title = _('Export AK Slides')
form_class = SlideExportForm
def form_valid(self, form):
Benjamin Hättasch
committed
template_name = 'admin/AKModel/export/slides.tex'
Benjamin Hättasch
committed
NEXT_AK_LIST_LENGTH = form.cleaned_data['num_next']
RESULT_PRESENTATION_MODE = form.cleaned_data["presentation_mode"]
SPACE_FOR_NOTES_IN_WISHES = form.cleaned_data["wish_notes"]
translations = {
'symbols': _("Symbols"),
'who': _("Who?"),
'duration': _("Duration(s)"),
'reso': _("Reso intention?"),
'category': _("Category (for Wishes)"),
'wishes': _("Wishes"),
}
def build_ak_list_with_next_aks(ak_list):
"""
Create a list of tuples cosisting of an AK and a list of upcoming AKs (list length depending on setting)
"""
Benjamin Hättasch
committed
next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None)
return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=[])]
Benjamin Hättasch
committed
# Get all relevant AKs (wishes separately, and either all AKs or only those who should directly or indirectly
# be presented when restriction setting was chosen)
categories_with_aks, ak_wishes = self.event.get_categories_with_aks(wishes_seperately=True, filter_func=lambda
Benjamin Hättasch
committed
ak: not RESULT_PRESENTATION_MODE or (ak.present or (ak.present is None and ak.category.present_by_default)))
Benjamin Hättasch
committed
context = {
'title': self.event.name,
'categories_with_aks': [(category, build_ak_list_with_next_aks(ak_list)) for category, ak_list in
categories_with_aks],
'subtitle': _("AKs"),
"wishes": build_ak_list_with_next_aks(ak_wishes),
"translations": translations,
"result_presentation_mode": RESULT_PRESENTATION_MODE,
"space_for_notes_in_wishes": SPACE_FOR_NOTES_IN_WISHES,
}
source = render_template_with_context(template_name, context)
# Perform real compilation (run latex twice for correct page numbers)
with tempfile.TemporaryDirectory() as tempdir:
run_tex_in_directory(source, tempdir, template_name=self.template_name)
os.remove(f'{tempdir}/texput.tex')
pdf = run_tex_in_directory(source, tempdir, template_name=self.template_name)
# Show PDF file to the user (with a filename containing a timestamp to prevent confusions about the right
# version to use when generating multiple versions of the slides, e.g., because owners did last-minute changes
# to their AKs
Benjamin Hättasch
committed
timestamp = datetime.datetime.now(tz=self.event.timezone).strftime("%Y-%m-%d_%H_%M")
return PDFResponse(pdf, filename=f'{self.event.slug}_ak_slides_{timestamp}.pdf')
class CVMarkResolvedView(IntermediateAdminActionView):
"""
Admin action view: Mark one or multitple constraint violation(s) as resolved
"""
Benjamin Hättasch
committed
title = _('Mark Constraint Violations as manually resolved')
model = ConstraintViolation
confirmation_message = _("The following Constraint Violations will be marked as manually resolved")
success_message = _("Constraint Violations marked as resolved")
def action(self, form):
self.entities.update(manually_resolved=True)
class CVSetLevelViolationView(IntermediateAdminActionView):
"""
Admin action view: Set one or multitple constraint violation(s) as to level "violation"
"""
Benjamin Hättasch
committed
title = _('Set Constraint Violations to level "violation"')
model = ConstraintViolation
confirmation_message = _("The following Constraint Violations will be set to level 'violation'")
success_message = _("Constraint Violations set to level 'violation'")
def action(self, form):
self.entities.update(level=ConstraintViolation.ViolationLevel.VIOLATION)
class CVSetLevelWarningView(IntermediateAdminActionView):
"""
Admin action view: Set one or multitple constraint violation(s) as to level "warning"
"""
Benjamin Hättasch
committed
title = _('Set Constraint Violations to level "warning"')
model = ConstraintViolation
confirmation_message = _("The following Constraint Violations will be set to level 'warning'")
success_message = _("Constraint Violations set to level 'warning'")
def action(self, form):
self.entities.update(level=ConstraintViolation.ViolationLevel.WARNING)
class PlanPublishView(IntermediateAdminActionView):
"""
Admin action view: Publish the plan of one or multitple event(s)
"""
Benjamin Hättasch
committed
title = _('Publish plan')
model = Event
confirmation_message = _('Publish the plan(s) of:')
success_message = _('Plan published')
def action(self, form):
self.entities.update(plan_published_at=Now(), plan_hidden=False)
class PlanUnpublishView(IntermediateAdminActionView):
"""
Admin action view: Unpublish the plan of one or multitple event(s)
"""
Benjamin Hättasch
committed
title = _('Unpublish plan')
model = Event
confirmation_message = _('Unpublish the plan(s) of:')
success_message = _('Plan unpublished')
def action(self, form):
self.entities.update(plan_published_at=None, plan_hidden=True)
class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
"""
Admin view: Allow to edit the default slots of an event
"""
Benjamin Hättasch
committed
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
template_name = "admin/AKModel/default_slot_editor.html"
form_class = DefaultSlotEditorForm
title = _("Edit Default Slots")
def get_success_url(self):
return self.request.path
def get_initial(self):
initial = super().get_initial()
default_slots = [
{"id": s.id, "start": s.start_iso, "end": s.end_iso, "allDay": False}
for s in self.event.defaultslot_set.all()
]
initial['availabilities'] = json.dumps({
'availabilities': default_slots
})
return initial
def form_valid(self, form):
default_slots_raw = json.loads(form.cleaned_data['availabilities'])["availabilities"]
tz = self.event.timezone
created_count = 0
updated_count = 0
previous_slot_ids = set(s.id for s in self.event.defaultslot_set.all())
Benjamin Hättasch
committed
for slot in default_slots_raw:
start = parse_datetime(slot["start"]).replace(tzinfo=tz)
end = parse_datetime(slot["end"]).replace(tzinfo=tz)
Benjamin Hättasch
committed
if slot["id"] != '':
slot_id = int(slot["id"])
if slot_id not in previous_slot_ids:
Benjamin Hättasch
committed
# Make sure only slots (currently) belonging to this event are edited
# (user did not manipulate IDs and slots have not been deleted in another session in the meantime)
messages.add_message(
self.request,
messages.WARNING,
_("Could not update slot {id} since it does not belong to {event}")
.format(id=slot['id'], event=self.event.name)
)
else:
# Update existing entries
previous_slot_ids.remove(slot_id)
original_slot = DefaultSlot.objects.get(id=slot_id)
Benjamin Hättasch
committed
if original_slot.start != start or original_slot.end != end:
original_slot.start = start
original_slot.end = end
original_slot.save()
updated_count += 1
else:
# Create new entries
DefaultSlot.objects.create(
start=start,
end=end,
event=self.event
)
created_count += 1
# Delete all slots not re-submitted by the user (and hence deleted in editor)
deleted_count = len(previous_slot_ids)
for d_id in previous_slot_ids:
DefaultSlot.objects.get(id=d_id).delete()
Benjamin Hättasch
committed
if created_count + updated_count + deleted_count > 0:
messages.add_message(
self.request,
messages.SUCCESS,
_("Updated {u} slot(s). created {c} new slot(s) and deleted {d} slot(s)")
.format(u=str(updated_count), c=str(created_count), d=str(deleted_count))
)
return super().form_valid(form)
class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView):
"""
View: Show all AKs of a given user
"""
model = AKOwner
context_object_name = 'owner'
template_name = "admin/AKModel/aks_by_user.html"