Source code for AKSubmission.views

from datetime import timedelta
from math import floor
from abc import ABC, abstractmethod

from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy
from django.utils.datetime_safe import datetime
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView

from AKModel.availability.models import Availability
from AKModel.metaviews import status_manager
from AKModel.metaviews.status import TemplateStatusWidget
from AKModel.models import AK, AKCategory, AKOwner, AKSlot, AKTrack, AKOrgaMessage
from AKModel.metaviews.admin import EventSlugMixin, FilterByEventSlugMixin
from AKSubmission.api import ak_interest_indication_active
from AKSubmission.forms import AKWishForm, AKOwnerForm, AKSubmissionForm, AKDurationForm, AKOrgaMessageForm, \
    AKForm


[docs] class SubmissionErrorNotConfiguredView(EventSlugMixin, TemplateView): """ View to show when submission is not correctly configured yet for this event and hence the submission component cannot be used already. """ template_name = "AKSubmission/submission_not_configured.html"
[docs] class AKOverviewView(FilterByEventSlugMixin, ListView): """ View: Show a tabbed list of AKs belonging to this event split by categories Wishes show up in between of the other AKs in the category they belong to. In contrast to :class:`SubmissionOverviewView` that inherits from this view, on this view there is no form to add new AKs or edit owners. Since the inherited version of this view will have a slightly different behaviour, this view contains multiple methods that can be overriden for this adaption. """ model = AKCategory context_object_name = "categories" template_name = "AKSubmission/ak_overview.html" wishes_as_category = False
[docs] def filter_aks(self, context, category): # pylint: disable=unused-argument """ Filter which AKs to display based on the given context and category In the default case, all AKs of that category are returned (including wishes) :param context: context of the view :param category: category to filter the AK list for :return: filtered list of AKs for the given category :rtype: QuerySet[AK] """ # Use prefetching and relation selection/joining to reduce the amount of necessary queries return category.ak_set.select_related('event').prefetch_related('owners').all()
[docs] def get_active_category_name(self, context): """ Get the category name to display by default/before further user interaction In the default case, simply the first category (the one with the lowest ID for this event) is used :param context: context of the view :return: name of the default category :rtype: str """ return context["categories_with_aks"][0][0].name
[docs] def get_table_title(self, context): # pylint: disable=unused-argument """ Specify the title above the AK list/table in this view :param context: context of the view :return: title to use :rtype: str """ return _("All AKs")
[docs] def get(self, request, *args, **kwargs): """ Handle GET request Overriden to allow checking for correct configuration and redirect to error page if necessary (see :class:`SubmissionErrorNotConfiguredView`) """ self._load_event() self.object_list = self.get_queryset() # pylint: disable=attribute-defined-outside-init # No categories yet? Redirect to configuration error page if self.object_list.count() == 0: return redirect(reverse_lazy("submit:error_not_configured", kwargs={'event_slug': self.event.slug})) context = self.get_context_data() return self.render_to_response(context)
def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) # ========================================================== # Sort AKs into different lists (by their category) # ========================================================== ak_wishes = [] categories_with_aks = [] # Loop over categories, load AKs (while filtering them if necessary) and create a list of (category, aks)-tuples # Depending on the setting of self.wishes_as_category, wishes are either included # or added to a special "Wish"-Category that is created on-the-fly to provide consistent handling in the # template (without storing it in the database) for category in context["categories"]: aks_for_category = [] for ak in self.filter_aks(context, category): if self.wishes_as_category and ak.wish: ak_wishes.append(ak) else: aks_for_category.append(ak) categories_with_aks.append((category, aks_for_category)) if self.wishes_as_category: categories_with_aks.append( (AKCategory(name=_("Wishes"), pk=0, description=_("AKs one would like to have")), ak_wishes)) context["categories_with_aks"] = categories_with_aks context["active_category"] = self.get_active_category_name(context) context['table_title'] = self.get_table_title(context) # ========================================================== # Display interest indication button? # ========================================================== current_timestamp = datetime.now().astimezone(self.event.timezone) context['interest_indication_active'] = ak_interest_indication_active(self.event, current_timestamp) return context
[docs] class SubmissionOverviewView(AKOverviewView): """ View: List of AKs and possibility to add AKs or adapt owner information Main/start view of the component. This view inherits from :class:`AKOverviewView`, but treats wishes as separate category if requested in the settings and handles the change actions mentioned above. """ model = AKCategory context_object_name = "categories" template_name = "AKSubmission/submission_overview.html" # this mainly steers the different handling of wishes # since the code for that is already included in the parent class wishes_as_category = settings.WISHES_AS_CATEGORY
[docs] def get_table_title(self, context): """ Specify the title above the AK list/table in this view :param context: context of the view :return: title to use :rtype: str """ return _("Currently planned AKs")
def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) # Get list of existing owners for event (for AK submission start) context["existingOwners"] = AKOwner.objects.filter(event=self.event) return context
[docs] class AKListByCategoryView(AKOverviewView): """ View: List of only the AKs belonging to a certain category. This view inherits from :class:`AKOverviewView`, but produces only one list instead of a tabbed one. """ def dispatch(self, request, *args, **kwargs): # Override dispatching # Needed to handle the checking whether the category exists self.category = get_object_or_404(AKCategory, pk=kwargs['category_pk']) # pylint: disable=attribute-defined-outside-init,line-too-long return super().dispatch(request, *args, **kwargs)
[docs] def get_active_category_name(self, context): """ Get the category name to display by default/before further user interaction In this case, this will be the name of the category specified via pk :param context: context of the view :return: name of the category :rtype: str """ return self.category.name
[docs] class AKListByTrackView(AKOverviewView): """ View: List of only the AKs belonging to a certain track. This view inherits from :class:`AKOverviewView` and there will be one list per category -- but only AKs of a certain given track will be included in them. """ def dispatch(self, request, *args, **kwargs): # Override dispatching # Needed to handle the checking whether the track exists self.track = get_object_or_404(AKTrack, pk=kwargs['track_pk']) # pylint: disable=attribute-defined-outside-init return super().dispatch(request, *args, **kwargs)
[docs] def filter_aks(self, context, category): """ Filter which AKs to display based on the given context and category In this case, the list is further restricted by the track :param context: context of the view :param category: category to filter the AK list for :return: filtered list of AKs for the given category :rtype: QuerySet[AK] """ return super().filter_aks(context, category).filter(track=self.track)
def get_table_title(self, context): return f"{_('AKs with Track')} = {self.track.name}"
[docs] class AKDetailView(EventSlugMixin, DetailView): """ View: AK Details """ model = AK context_object_name = "ak" template_name = "AKSubmission/ak_detail.html" def get_queryset(self): # Get information about the AK and do some query optimization return super().get_queryset().select_related('event').prefetch_related('owners') def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) context["availabilities"] = Availability.objects.filter(ak=context["ak"]) current_timestamp = datetime.now().astimezone(self.event.timezone) # Is this AK taking place now or soon (used for top page visualization) context["featured_slot_type"] = "NONE" if apps.is_installed("AKPlan"): in_two_hours = current_timestamp + timedelta(hours=2) slots = context["ak"].akslot_set.filter(start__isnull=False, room__isnull=False).select_related('room') for slot in slots: if slot.end > current_timestamp: if slot.start <= current_timestamp: context["featured_slot_type"] = "CURRENT" remaining = slot.end - current_timestamp elif slot.start <= in_two_hours: context["featured_slot_type"] = "UPCOMING" remaining = slot.start - current_timestamp else: continue context["featured_slot"] = slot context["featured_slot_remaining"] = floor(remaining.days * 24 * 60 + remaining.seconds / 60) break # Display interest indication button? context['interest_indication_active'] = ak_interest_indication_active(self.event, current_timestamp) return context
[docs] class AKHistoryView(EventSlugMixin, DetailView): """ View: Show history of a given AK """ model = AK context_object_name = "ak" template_name = "AKSubmission/ak_history.html"
[docs] class EventInactiveRedirectMixin: """ Mixin that will cause a redirect when actions are performed on an inactive event. Will add a message explaining why the action was not performed to the user and then redirect to start page of the submission component """
[docs] def get_error_message(self): """ Error message to display after redirect (can be adjusted by this method) :return: error message :rtype: str """ return _("Event inactive. Cannot create or update.")
[docs] def get(self, request, *args, **kwargs): """ Override GET request handling Will either perform the redirect including the message creation or continue with the planned dispatching """ s = super().get(request, *args, **kwargs) if not self.event.active: messages.add_message(self.request, messages.ERROR, self.get_error_message()) return redirect(reverse_lazy('submit:submission_overview', kwargs={'event_slug': self.event.slug})) return s
[docs] class AKAndAKWishSubmissionView(EventSlugMixin, EventInactiveRedirectMixin, CreateView): """ View: Submission form for AKs and Wishes Base view, will be used by :class:`AKSubmissionView` and :class:`AKWishSubmissionView` """ model = AK template_name = 'AKSubmission/submit_new.html' form_class = AKSubmissionForm def get_success_url(self): messages.add_message(self.request, messages.SUCCESS, _("AK successfully created")) return self.object.detail_url def form_valid(self, form): if not form.cleaned_data["event"].active: messages.add_message(self.request, messages.ERROR, self.get_error_message()) return redirect(reverse_lazy('submit:submission_overview', kwargs={'event_slug': form.cleaned_data["event"].slug})) # Try to save AK and get redirect URL super_form_valid = super().form_valid(form) # Generate slot(s) (but not for wishes) if "durations" in form.cleaned_data: for duration in form.cleaned_data["durations"]: new_slot = AKSlot(ak=self.object, duration=duration, event=self.object.event) new_slot.save() return super_form_valid
[docs] class AKSubmissionView(AKAndAKWishSubmissionView): """ View: AK submission form Extends :class:`AKAndAKWishSubmissionView` """ def get_initial(self): # Load initial values for the form # Used to directly add the first owner and the event this AK will belong to initials = super(AKAndAKWishSubmissionView, self).get_initial() initials['owners'] = [AKOwner.get_by_slug(self.event, self.kwargs['owner_slug'])] initials['event'] = self.event return initials def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) context['owner'] = get_object_or_404(AKOwner, event=self.event, slug=self.kwargs['owner_slug']) return context
[docs] class AKWishSubmissionView(AKAndAKWishSubmissionView): """ View: Wish submission form Extends :class:`AKAndAKWishSubmissionView` """ template_name = 'AKSubmission/submit_new_wish.html' form_class = AKWishForm def get_initial(self): # Load initial values for the form # Used to directly select the event this AK will belong to initials = super(AKAndAKWishSubmissionView, self).get_initial() initials['event'] = self.event return initials
[docs] class AKEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView): """ View: Update an AK This allows to change most fields of an AK as specified in :class:`AKSubmission.forms.AKForm`, including the availabilities. It will also handle the change from AK to wish and vice versa (triggered by adding or removing owners) and automatically create or delete (unscheduled) slots """ model = AK template_name = 'AKSubmission/ak_edit.html' form_class = AKForm def get_success_url(self): # Redirection after successfully saving to detail page of AK where also a success message is displayed messages.add_message(self.request, messages.SUCCESS, _("AK successfully updated")) return self.object.detail_url def form_valid(self, form): # Handle valid form submission # Only save when event is active, otherwise redirect if not form.cleaned_data["event"].active: messages.add_message(self.request, messages.ERROR, self.get_error_message()) return redirect(reverse_lazy('submit:submission_overview', kwargs={'event_slug': form.cleaned_data["event"].slug})) # Remember owner count before saving to know whether the AK changed its state between AK and wish previous_owner_count = self.object.owners.count() # Perform saving and redirect handling by calling default/parent implementation of form_valid redirect_response = super().form_valid(form) # Did this AK change from wish to AK or vice versa? new_owner_count = self.object.owners.count() # Now AK: if previous_owner_count == 0 and new_owner_count > 0 and self.object.akslot_set.count() == 0: # Create one slot with default length AKSlot.objects.create(ak=self.object, duration=self.object.event.default_slot, event=self.object.event) # Now wish: elif previous_owner_count > 0 and new_owner_count == 0: # Delete all unscheduled slots self.object.akslot_set.filter(start__isnull=True).delete() # Redirect to success url return redirect_response
[docs] class AKOwnerCreateView(EventSlugMixin, EventInactiveRedirectMixin, CreateView): """ View: Create a new owner """ model = AKOwner template_name = 'AKSubmission/akowner_create_update.html' form_class = AKOwnerForm def get_success_url(self): # The redirect url depends on the source this view was called from: # Called from an existing AK? Add the new owner as an owner of that AK, notify the user and redirect to detail # page of that AK if "add_to_existing_ak" in self.request.GET: ak_pk = self.request.GET['add_to_existing_ak'] ak = get_object_or_404(AK, pk=ak_pk) ak.owners.add(self.object) messages.add_message(self.request, messages.SUCCESS, _("Added '{owner}' as new owner of '{ak.name}'").format(owner=self.object, ak=ak)) return ak.detail_url # Called from the submission overview? Offer the user to create a new AK with the recently created owner # prefilled as owner of that AK in the creation form return reverse_lazy('submit:submit_ak', kwargs={'event_slug': self.kwargs['event_slug'], 'owner_slug': self.object.slug}) def get_initial(self): # Set the event in the (hidden) event field in the form based on the URL this view was called with initials = super().get_initial() initials['event'] = self.event return initials def form_valid(self, form): # Prevent changes if event is not active if not form.cleaned_data["event"].active: messages.add_message(self.request, messages.ERROR, self.get_error_message()) return redirect(reverse_lazy('submit:submission_overview', kwargs={'event_slug': form.cleaned_data["event"].slug})) return super().form_valid(form)
[docs] class AKOwnerDispatchView(ABC, EventSlugMixin, View): """ Base view: Dispatch to correct view based upon Will be used by :class:`AKOwnerSelectDispatchView` and :class:`AKOwnerEditDispatchView` to handle button clicks for "New AK" and "Edit Person Info" in submission overview based upon the selection in the owner dropdown field """
[docs] @abstractmethod def get_new_owner_redirect(self, event_slug): """ Get redirect when user selected "I do not own AKs yet" :param event_slug: slug of the event, needed for constructing redirect :return: redirect to perform :rtype: HttpResponseRedirect """
[docs] @abstractmethod def get_valid_owner_redirect(self, event_slug, owner): """ Get redirect when user selected "I do not own AKs yet" :param event_slug: slug of the event, needed for constructing redirect :param owner: owner to perform the dispatching for :return: redirect to perform :rtype: HttpResponseRedirect """
def post(self, request, *args, **kwargs): # This view is solely meant to handle POST requests # Perform dispatching based on the submitted owner_id # No owner_id? Redirect to submission overview view if "owner_id" not in request.POST: return redirect('submit:submission_overview', event_slug=kwargs['event_slug']) owner_id = request.POST["owner_id"] # Special owner_id "-1" (value of "I do not own AKs yet)? Redirect to owner creation view if owner_id == "-1": return self.get_new_owner_redirect(kwargs['event_slug']) # Normal owner_id given? Check vor validity and redirect to AK submission page with that owner prefilled # or display a 404 error page if no owner for the given id can be found. The latter should only happen when the # user manipulated the value before sending or when the owner was deleted in backend and the user did not # reload the dropdown between deletion and sending the dispatch request owner = get_object_or_404(AKOwner, pk=request.POST["owner_id"]) return self.get_valid_owner_redirect(kwargs['event_slug'], owner) def get(self, request, *args, **kwargs): # This view should never be called with GET, perform a redirect to overview in that case return redirect('submit:submission_overview', event_slug=kwargs['event_slug'])
[docs] class AKOwnerSelectDispatchView(AKOwnerDispatchView): """ View: Handle submission from the owner selection dropdown in submission overview for AK creation ("New AK" button) This view will perform redirects depending on the selection in the owner dropdown field. Based upon the abstract base view :class:`AKOwnerDispatchView`. """ def get_new_owner_redirect(self, event_slug): return redirect('submit:akowner_create', event_slug=event_slug) def get_valid_owner_redirect(self, event_slug, owner): return redirect('submit:submit_ak', event_slug=event_slug, owner_slug=owner.slug)
[docs] class AKOwnerEditDispatchView(AKOwnerDispatchView): """ View: Handle submission from the owner selection dropdown in submission overview for owner editing ("Edit Person Info" button) This view will perform redirects depending on the selection in the owner dropdown field. Based upon the abstract base view :class:`AKOwnerDispatchView`. """ def get_new_owner_redirect(self, event_slug): messages.add_message(self.request, messages.WARNING, _("No user selected")) return redirect('submit:submission_overview', event_slug) def get_valid_owner_redirect(self, event_slug, owner): return redirect('submit:akowner_edit', event_slug=event_slug, slug=owner.slug)
[docs] class AKOwnerEditView(FilterByEventSlugMixin, EventSlugMixin, EventInactiveRedirectMixin, UpdateView): """ View: Edit an owner """ model = AKOwner template_name = "AKSubmission/akowner_create_update.html" form_class = AKOwnerForm def get_success_url(self): messages.add_message(self.request, messages.SUCCESS, _("Person Info successfully updated")) return reverse_lazy('submit:submission_overview', kwargs={'event_slug': self.kwargs['event_slug']}) def form_valid(self, form): # Prevent updating if event is not active if not form.cleaned_data["event"].active: messages.add_message(self.request, messages.ERROR, self.get_error_message()) return redirect(reverse_lazy('submit:submission_overview', kwargs={'event_slug': form.cleaned_data["event"].slug})) return super().form_valid(form)
[docs] class AKSlotAddView(EventSlugMixin, EventInactiveRedirectMixin, CreateView): """ View: Add an additional slot to an AK The user has to select the duration of the slot in this view The view will only process the request when the event is active (as steered by :class:`EventInactiveRedirectMixin`) """ model = AKSlot form_class = AKDurationForm template_name = "AKSubmission/akslot_add_update.html" def get_initial(self): initials = super().get_initial() initials['event'] = self.event initials['ak'] = get_object_or_404(AK, pk=self.kwargs['pk']) initials['duration'] = self.event.default_slot return initials def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) context['ak'] = get_object_or_404(AK, pk=self.kwargs['pk']) return context def get_success_url(self): messages.add_message(self.request, messages.SUCCESS, _("AK Slot successfully added")) return self.object.ak.detail_url
[docs] class AKSlotEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView): """ View: Update the duration of an AK slot The view will only process the request when the event is active (as steered by :class:`EventInactiveRedirectMixin`) and only slots that are not scheduled yet may be changed """ model = AKSlot form_class = AKDurationForm template_name = "AKSubmission/akslot_add_update.html" def get(self, request, *args, **kwargs): akslot = get_object_or_404(AKSlot, pk=kwargs["pk"]) if akslot.start is not None: messages.add_message(self.request, messages.WARNING, _("You cannot edit a slot that has already been scheduled")) return HttpResponseRedirect(akslot.ak.detail_url) return super().get(request, *args, **kwargs) def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) context['ak'] = self.object.ak return context def get_success_url(self): messages.add_message(self.request, messages.SUCCESS, _("AK Slot successfully updated")) return self.object.ak.detail_url
[docs] class AKSlotDeleteView(EventSlugMixin, EventInactiveRedirectMixin, DeleteView): """ View: Delete an AK slot The view will only process the request when the event is active (as steered by :class:`EventInactiveRedirectMixin`) and only slots that are not scheduled yet may be deleted """ model = AKSlot template_name = "AKSubmission/akslot_delete.html" def get(self, request, *args, **kwargs): akslot = get_object_or_404(AKSlot, pk=kwargs["pk"]) if akslot.start is not None: messages.add_message(self.request, messages.WARNING, _("You cannot delete a slot that has already been scheduled")) return HttpResponseRedirect(akslot.ak.detail_url) return super().get(request, *args, **kwargs) def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) context['ak'] = self.object.ak return context def get_success_url(self): messages.add_message(self.request, messages.SUCCESS, _("AK Slot successfully deleted")) return self.object.ak.detail_url
[docs] @status_manager.register(name="event_ak_messages") class EventAKMessagesWidget(TemplateStatusWidget): """ Status page widget: AK Messages A widget to display information about AK-related messages sent to organizers for the given event """ required_context_type = "event" title = _("Messages") template_name = "admin/AKModel/render_ak_messages.html" def get_context_data(self, context) -> dict: context["ak_messages"] = AKOrgaMessage.objects.filter(ak__event=context["event"]) return context def render_actions(self, context: {}) -> list[dict]: return [ { "text": _("Delete all messages"), "url": reverse_lazy("admin:ak_delete_orga_messages", kwargs={"event_slug": context["event"].slug}), }, ]
[docs] class AKAddOrgaMessageView(EventSlugMixin, CreateView): """ View: Form to create a (confidential) message to the organizers as defined in :class:`AKSubmission.forms.AKOrgaMessageForm` """ model = AKOrgaMessage form_class = AKOrgaMessageForm template_name = "AKSubmission/akmessage_add.html" def get_initial(self): initials = super().get_initial() initials['ak'] = get_object_or_404(AK, pk=self.kwargs['pk']) initials['event'] = initials['ak'].event return initials def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) context['ak'] = get_object_or_404(AK, pk=self.kwargs['pk']) return context def get_success_url(self): messages.add_message(self.request, messages.SUCCESS, _("Message to organizers successfully saved")) return self.object.ak.detail_url