Skip to content
Snippets Groups Projects
Select Git revision
  • bced419b694e6fe49b1dd74b081adef169956658
  • main default protected
  • renovate/django-5.x
  • koma/feature/preference-polling-form
4 results

0023_event_default_slot.py

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    manage.py 11.73 KiB
    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
    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, AKType
    
    
    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
        """
        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
        """
        title = _('Export AK Slides')
        form_class = SlideExportForm
    
        def get_form(self, form_class=None):
            # Filter type choices to those of the current event
            # or completely hide the field if no types are specified for this event
            form = super().get_form(form_class)
            if self.event.aktype_set.count() > 0:
                form.fields['types'].choices = [
                    (ak_type.id, ak_type.name) for ak_type in self.event.aktype_set.all()
                ]
            else:
                form.fields['types'].widget = form.fields['types'].hidden_widget()
            return form
    
        def form_valid(self, form):
            # pylint: disable=invalid-name
            template_name = 'admin/AKModel/export/slides.tex'
    
            # Settings
            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"),
                'types': _("Types"),
            }
    
            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)
                """
                next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None)
                return list(zip_longest(ak_list, next_aks_list, fillvalue=[]))
    
            # Create a list of types to filter AKs by (if at least one type was selected)
            types = None
            types_filter_string = ""
            show_types = self.event.aktype_set.count() > 0
            if len(form.cleaned_data['types']) > 0:
                types = AKType.objects.filter(id__in=form.cleaned_data['types'])
                names_string = ', '.join(AKType.objects.get(pk=t).name for t in form.cleaned_data['types'])
                types_filter_string = f"[{_('Type(s)')}: {names_string}]"
    
            # 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
                ak: not RESULT_PRESENTATION_MODE or (ak.present or (ak.present is None and ak.category.present_by_default)),
                                                                                types=types)
    
            # Create context for LaTeX rendering
            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") + " " + types_filter_string,
                "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,
                "show_types": show_types,
            }
    
            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
            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
        """
        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"
        """
        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"
        """
        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)
        """
        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)
        """
        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 PollPublishView(IntermediateAdminActionView):
        """
        Admin action view: Publish the preference poll of one or multitple event(s)
        """
        title = _('Publish preference poll')
        model = Event
        confirmation_message = _('Publish the poll(s) of:')
        success_message = _('Preference poll published')
    
        def action(self, form):
            self.entities.update(poll_published_at=Now(), poll_hidden=False)
    
    
    class PollUnpublishView(IntermediateAdminActionView):
        """
        Admin action view: Unpublish the preference poll of one or multitple event(s)
        """
        title = _('Unpublish preference poll')
        model = Event
        confirmation_message = _('Unpublish the preference poll(s) of:')
        success_message = _('Preference poll unpublished')
    
        def action(self, form):
            self.entities.update(poll_published_at=None, poll_hidden=True)
    
    class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
        """
        Admin view: Allow to edit the default slots of an event
        """
        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())
    
            # Loop over inputs and update or add slots
            for slot in default_slots_raw:
                start = parse_datetime(slot["start"]).replace(tzinfo=tz)
                end = parse_datetime(slot["end"]).replace(tzinfo=tz)
    
                if slot["id"] != '':
                    slot_id = int(slot["id"])
                    if slot_id not in previous_slot_ids:
                        # 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)
                        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()
    
            # Inform user about changes performed
            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"