Skip to content
Snippets Groups Projects
Commit 63c18d2e authored by Benjamin Hättasch's avatar Benjamin Hättasch
Browse files

Improve AKScheduling

Add or complete docstrings
Remove code smells
Disable irrelevant warnings
Remove empty admin.py (to disable doc generation for an empty module)
Add additional test cases and improve basic test interface (support both 403 and 302 in case the user lacks rights to see a view through optional configuration)
Improve usage of types for API endpoints (e.g., restrict writing endpoints to writing only)
parent 83223f52
No related branches found
No related tags found
No related merge requests found
...@@ -106,15 +106,17 @@ class BasicViewTests: ...@@ -106,15 +106,17 @@ class BasicViewTests:
""" """
# Not logged in? Views should not be visible # Not logged in? Views should not be visible
self.client.logout() self.client.logout()
for view_name in self.VIEWS_STAFF_ONLY: for view_name_info in self.VIEWS_STAFF_ONLY:
view_name_with_prefix, url = self._name_and_url(view_name) expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2]
view_name_with_prefix, url = self._name_and_url(view_name_info)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 302, msg=f"{view_name_with_prefix} ({url}) accessible by non-staff") self.assertEqual(response.status_code, expected_response_code,
msg=f"{view_name_with_prefix} ({url}) accessible by non-staff")
# Logged in? Views should be visible # Logged in? Views should be visible
self.client.force_login(self.staff_user) self.client.force_login(self.staff_user)
for view_name in self.VIEWS_STAFF_ONLY: for view_name_info in self.VIEWS_STAFF_ONLY:
view_name_with_prefix, url = self._name_and_url(view_name) view_name_with_prefix, url = self._name_and_url(view_name_info)
try: try:
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, self.assertEqual(response.status_code, 200,
...@@ -125,10 +127,11 @@ class BasicViewTests: ...@@ -125,10 +127,11 @@ class BasicViewTests:
# Disabled user? Views should not be visible # Disabled user? Views should not be visible
self.client.force_login(self.deactivated_user) self.client.force_login(self.deactivated_user)
for view_name in self.VIEWS_STAFF_ONLY: for view_name_info in self.VIEWS_STAFF_ONLY:
view_name_with_prefix, url = self._name_and_url(view_name) expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2]
view_name_with_prefix, url = self._name_and_url(view_name_info)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 302, self.assertEqual(response.status_code, expected_response_code,
msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user") msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user")
def _to_sendable_value(self, val): def _to_sendable_value(self, val):
......
# Register your models here.
...@@ -12,20 +12,32 @@ from AKModel.metaviews.admin import EventSlugMixin ...@@ -12,20 +12,32 @@ from AKModel.metaviews.admin import EventSlugMixin
class ResourceSerializer(serializers.ModelSerializer): class ResourceSerializer(serializers.ModelSerializer):
"""
REST Framework Serializer for Rooms to produce format required for fullcalendar resources
"""
class Meta: class Meta:
model = Room model = Room
fields = ['id', 'title'] fields = ['id', 'title']
title = serializers.SerializerMethodField('transform_title') title = serializers.SerializerMethodField('transform_title')
def transform_title(self, obj): @staticmethod
def transform_title(obj):
"""
Adapt title, add capacity information if room has a restriction (capacity is not -1)
"""
if obj.capacity > 0: if obj.capacity > 0:
return f"{obj.title} [{obj.capacity}]" return f"{obj.title} [{obj.capacity}]"
return obj.title return obj.title
class ResourcesViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): class ResourcesViewSet(EventSlugMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) """
API View: Rooms (resources to schedule for in fullcalendar)
Read-only, adaption to fullcalendar format through :class:`ResourceSerializer`
"""
permission_classes = (permissions.DjangoModelPermissions,)
serializer_class = ResourceSerializer serializer_class = ResourceSerializer
def get_queryset(self): def get_queryset(self):
...@@ -33,6 +45,13 @@ class ResourcesViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListMod ...@@ -33,6 +45,13 @@ class ResourcesViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListMod
class EventsView(LoginRequiredMixin, EventSlugMixin, ListView): class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
"""
API View: Slots (events to schedule in fullcalendar)
Read-only, JSON formatted response is created manually since it requires a bunch of "custom" fields that have
different names compared to the normal model or are not present at all and need to be computed to create the
required format for fullcalendar.
"""
model = AKSlot model = AKSlot
def get_queryset(self): def get_queryset(self):
...@@ -42,13 +61,16 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView): ...@@ -42,13 +61,16 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
return JsonResponse( return JsonResponse(
[{ [{
"slotID": slot.pk, "slotID": slot.pk,
"title": f'{slot.ak.short_name}: \n{slot.ak.owners_list}', "title": f'{slot.ak.short_name}:\n{slot.ak.owners_list}',
"description": slot.ak.details, "description": slot.ak.details,
"resourceId": slot.room.id, "resourceId": slot.room.id,
"start": timezone.localtime(slot.start, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"), "start": timezone.localtime(slot.start, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"),
"end": timezone.localtime(slot.end, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"), "end": timezone.localtime(slot.end, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"),
"backgroundColor": slot.ak.category.color, "backgroundColor": slot.ak.category.color,
"borderColor": "#2c3e50" if slot.fixed else '#e74c3c' if slot.constraintviolation_set.count() > 0 else slot.ak.category.color, "borderColor":
"#2c3e50" if slot.fixed
else '#e74c3c' if slot.constraintviolation_set.count() > 0
else slot.ak.category.color,
"constraint": 'roomAvailable', "constraint": 'roomAvailable',
"editable": not slot.fixed, "editable": not slot.fixed,
'url': str(reverse('admin:AKModel_akslot_change', args=[slot.pk])), 'url': str(reverse('admin:AKModel_akslot_change', args=[slot.pk])),
...@@ -59,6 +81,13 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView): ...@@ -59,6 +81,13 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView): class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView):
"""
API view: Availabilities of rooms
Read-only, JSON formatted response is created manually since it requires a bunch of "custom" fields that have
different names compared to the normal model or are not present at all and need to be computed to create the
required format for fullcalendar.
"""
model = Availability model = Availability
context_object_name = "availabilities" context_object_name = "availabilities"
...@@ -81,6 +110,13 @@ class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView): ...@@ -81,6 +110,13 @@ class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView):
class DefaultSlotsView(LoginRequiredMixin, EventSlugMixin, ListView): class DefaultSlotsView(LoginRequiredMixin, EventSlugMixin, ListView):
"""
API view: default slots
Read-only, JSON formatted response is created manually since it requires a bunch of "custom" fields that have
different names compared to the normal model or are not present at all and need to be computed to create the
required format for fullcalendar.
"""
model = DefaultSlot model = DefaultSlot
context_object_name = "default_slots" context_object_name = "default_slots"
...@@ -105,6 +141,9 @@ class DefaultSlotsView(LoginRequiredMixin, EventSlugMixin, ListView): ...@@ -105,6 +141,9 @@ class DefaultSlotsView(LoginRequiredMixin, EventSlugMixin, ListView):
class EventSerializer(serializers.ModelSerializer): class EventSerializer(serializers.ModelSerializer):
"""
REST framework serializer to adapt between AKSlot model and the event format of fullcalendar
"""
class Meta: class Meta:
model = AKSlot model = AKSlot
fields = ['id', 'start', 'end', 'roomId'] fields = ['id', 'start', 'end', 'roomId']
...@@ -114,17 +153,31 @@ class EventSerializer(serializers.ModelSerializer): ...@@ -114,17 +153,31 @@ class EventSerializer(serializers.ModelSerializer):
roomId = serializers.IntegerField(source='room.pk') roomId = serializers.IntegerField(source='room.pk')
def update(self, instance, validated_data): def update(self, instance, validated_data):
# Ignore timezone of input (treat it as timezone-less) and set the event timezone
# By working like this, the client does not need to know about timezones, since every timestamp it deals with
# has the timezone offsets already applied
start = timezone.make_aware(timezone.make_naive(validated_data.get('start')), instance.event.timezone) start = timezone.make_aware(timezone.make_naive(validated_data.get('start')), instance.event.timezone)
end = timezone.make_aware(timezone.make_naive(validated_data.get('end')), instance.event.timezone) end = timezone.make_aware(timezone.make_naive(validated_data.get('end')), instance.event.timezone)
instance.start = start instance.start = start
instance.room = get_object_or_404(Room, pk=validated_data.get('room')["pk"]) # Also, adapt from start & end format of fullcalendar to our start & duration model
diff = end - start diff = end - start
instance.duration = round(diff.days * 24 + (diff.seconds / 3600), 2) instance.duration = round(diff.days * 24 + (diff.seconds / 3600), 2)
# Updated room if needed (pk changed -- otherwise, no need for an additional database lookup)
new_room_id = validated_data.get('room')["pk"]
if instance.room.pk != new_room_id:
instance.room = get_object_or_404(Room, pk=new_room_id)
instance.save() instance.save()
return instance return instance
class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet): class EventsViewSet(EventSlugMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
"""
API view: Update scheduling of a slot (event in fullcalendar format)
Write-only (will however reply with written values to PUT request)
"""
permission_classes = (permissions.DjangoModelPermissions,) permission_classes = (permissions.DjangoModelPermissions,)
serializer_class = EventSerializer serializer_class = EventSerializer
...@@ -136,17 +189,26 @@ class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet): ...@@ -136,17 +189,26 @@ class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet):
class ConstraintViolationSerializer(serializers.ModelSerializer): class ConstraintViolationSerializer(serializers.ModelSerializer):
"""
REST Framework Serializer for constraint violations
"""
class Meta: class Meta:
model = ConstraintViolation model = ConstraintViolation
fields = ['pk', 'type_display', 'aks', 'ak_slots', 'ak_owner', 'room', 'requirement', 'category', 'comment', 'timestamp_display', 'manually_resolved', 'level_display', 'details', 'edit_url'] fields = ['pk', 'type_display', 'aks', 'ak_slots', 'ak_owner', 'room', 'requirement', 'category', 'comment',
'timestamp_display', 'manually_resolved', 'level_display', 'details', 'edit_url']
class ConstraintViolationsViewSet(EventSlugMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
API View: Constraint Violations of an event
class ConstraintViolationsViewSet(EventSlugMixin, viewsets.ModelViewSet): Read-only, fields and model selected in :class:`ConstraintViolationSerializer`
"""
permission_classes = (permissions.DjangoModelPermissions,) permission_classes = (permissions.DjangoModelPermissions,)
serializer_class = ConstraintViolationSerializer serializer_class = ConstraintViolationSerializer
def get_object(self):
return get_object_or_404(ConstraintViolation, pk=self.kwargs["pk"])
def get_queryset(self): def get_queryset(self):
return ConstraintViolation.objects.select_related('event', 'room').prefetch_related('aks', 'ak_slots', 'ak_owner', 'requirement', 'category').filter(event=self.event).order_by('manually_resolved', '-type', '-timestamp') # Optimize query to reduce database load
return (ConstraintViolation.objects.select_related('event', 'room')
.prefetch_related('aks', 'ak_slots', 'ak_owner', 'requirement', 'category')
.filter(event=self.event).order_by('manually_resolved', '-type', '-timestamp'))
...@@ -2,4 +2,7 @@ from django.apps import AppConfig ...@@ -2,4 +2,7 @@ from django.apps import AppConfig
class AkschedulingConfig(AppConfig): class AkschedulingConfig(AppConfig):
"""
App configuration (default, only specifies name of the app)
"""
name = 'AKScheduling' name = 'AKScheduling'
...@@ -5,6 +5,9 @@ from AKModel.models import AK ...@@ -5,6 +5,9 @@ from AKModel.models import AK
class AKInterestForm(forms.ModelForm): class AKInterestForm(forms.ModelForm):
"""
Form for quickly changing the interest count and notes of an AK
"""
required_css_class = 'required' required_css_class = 'required'
class Meta: class Meta:
......
# This file mainly contains signal receivers, which follow a very strong interface, having e.g., a sender attribute
# that is hardly used by us. Nevertheless, to follow the django receiver coding style and since changes might
# cause issues when loading fixtures or model dumps, it is not wise to replace that attribute with "_".
# Therefore, the check that finds unused arguments is disabled for this whole file:
# pylint: disable=unused-argument
from django.db.models.signals import post_save, m2m_changed, pre_delete from django.db.models.signals import post_save, m2m_changed, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from AKModel.availability.models import Availability from AKModel.availability.models import Availability
from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation from AKModel.models import AK, AKSlot, Room, Event, ConstraintViolation
def update_constraint_violations(new_violations, existing_violations_to_check): def update_constraint_violations(new_violations, existing_violations_to_check):
...@@ -43,11 +49,15 @@ def update_cv_reso_deadline_for_slot(slot): ...@@ -43,11 +49,15 @@ def update_cv_reso_deadline_for_slot(slot):
:type slot: AKSlot :type slot: AKSlot
""" """
event = slot.event event = slot.event
# Update only if reso_deadline exists
# if event was changed and reso_deadline is removed, CVs will be deleted by event changed handler
# Update only has to be done for already scheduled slots with reso intention
if slot.ak.reso and slot.event.reso_deadline and slot.start: if slot.ak.reso and slot.event.reso_deadline and slot.start:
# Update only if reso_deadline exists
# if event was changed and reso_deadline is removed, CVs will be deleted by event changed handler
violation_type = ConstraintViolation.ViolationType.AK_AFTER_RESODEADLINE violation_type = ConstraintViolation.ViolationType.AK_AFTER_RESODEADLINE
new_violations = [] new_violations = []
# Violation?
if slot.end > event.reso_deadline: if slot.end > event.reso_deadline:
c = ConstraintViolation( c = ConstraintViolation(
type=violation_type, type=violation_type,
...@@ -69,38 +79,47 @@ def check_capacity_for_slot(slot: AKSlot): ...@@ -69,38 +79,47 @@ def check_capacity_for_slot(slot: AKSlot):
:return: Violation (if any) or None :return: Violation (if any) or None
:rtype: ConstraintViolation or None :rtype: ConstraintViolation or None
""" """
if slot.room:
if slot.room.capacity >= 0: # If slot is scheduled in a room
if slot.room.capacity < slot.ak.interest: if slot.room and slot.room.capacity >= 0:
c = ConstraintViolation( # Create a violation if interest exceeds room capacity
type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED, if slot.room.capacity < slot.ak.interest:
level=ConstraintViolation.ViolationLevel.VIOLATION, c = ConstraintViolation(
event=slot.event, type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED,
room=slot.room, level=ConstraintViolation.ViolationLevel.VIOLATION,
comment=_("Not enough space for AK interest (Interest: %(interest)d, Capacity: %(capacity)d)") event=slot.event,
% {'interest': slot.ak.interest, 'capacity': slot.room.capacity}, room=slot.room,
) comment=_("Not enough space for AK interest (Interest: %(interest)d, Capacity: %(capacity)d)")
c.ak_slots_tmp.add(slot) % {'interest': slot.ak.interest, 'capacity': slot.room.capacity},
c.aks_tmp.add(slot.ak) )
return c c.ak_slots_tmp.add(slot)
elif slot.room.capacity < slot.ak.interest + 5 or slot.room.capacity < slot.ak.interest * 1.25: c.aks_tmp.add(slot.ak)
c = ConstraintViolation( return c
type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED,
level=ConstraintViolation.ViolationLevel.WARNING, # Create a warning if interest is close to room capacity
event=slot.event, if slot.room.capacity < slot.ak.interest + 5 or slot.room.capacity < slot.ak.interest * 1.25:
room=slot.room, c = ConstraintViolation(
comment=_("Space is too close to AK interest (Interest: %(interest)d, Capacity: %(capacity)d)") type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED,
% {'interest': slot.ak.interest, 'capacity': slot.room.capacity} level=ConstraintViolation.ViolationLevel.WARNING,
) event=slot.event,
c.ak_slots_tmp.add(slot) room=slot.room,
c.aks_tmp.add(slot.ak) comment=_("Space is too close to AK interest (Interest: %(interest)d, Capacity: %(capacity)d)")
return c % {'interest': slot.ak.interest, 'capacity': slot.room.capacity}
return None )
c.ak_slots_tmp.add(slot)
c.aks_tmp.add(slot.ak)
return c
return None
@receiver(post_save, sender=AK) @receiver(post_save, sender=AK)
def ak_changed_handler(sender, instance: AK, **kwargs): def ak_changed_handler(sender, instance: AK, **kwargs):
# Changes might affect: Reso intention, Category, Interest """
Signal receiver: Check for violations after AK changed
Changes might affect: Reso intention, Category, Interest
"""
# TODO Reso intention changes # TODO Reso intention changes
# Check room capacities # Check room capacities
...@@ -118,14 +137,12 @@ def ak_changed_handler(sender, instance: AK, **kwargs): ...@@ -118,14 +137,12 @@ def ak_changed_handler(sender, instance: AK, **kwargs):
@receiver(m2m_changed, sender=AK.owners.through) @receiver(m2m_changed, sender=AK.owners.through)
def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs): def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs):
""" """
Owners of AK changed Signal receiver: Owners of AK changed
""" """
# Only signal after change (post_add, post_delete, post_clear) are relevant # Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"): if not action.startswith("post"):
return return
# print(f"{instance} changed")
event = instance.event event = instance.event
# Owner(s) changed: Might affect multiple AKs by the same owner(s) at the same time # Owner(s) changed: Might affect multiple AKs by the same owner(s) at the same time
...@@ -157,8 +174,6 @@ def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs): ...@@ -157,8 +174,6 @@ def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs):
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
#print(f"{owner} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -169,14 +184,12 @@ def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs): ...@@ -169,14 +184,12 @@ def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs):
@receiver(m2m_changed, sender=AK.conflicts.through) @receiver(m2m_changed, sender=AK.conflicts.through)
def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs): def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
""" """
Conflicts of AK changed Signal receiver: Conflicts of AK changed
""" """
# Only signal after change (post_add, post_delete, post_clear) are relevant # Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"): if not action.startswith("post"):
return return
# print(f"{instance} changed")
event = instance.event event = instance.event
# Conflict(s) changed: Might affect multiple AKs that are conflicts of each other # Conflict(s) changed: Might affect multiple AKs that are conflicts of each other
...@@ -186,6 +199,7 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs): ...@@ -186,6 +199,7 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False) slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
conflicts_of_this_ak: [AK] = instance.conflicts.all() conflicts_of_this_ak: [AK] = instance.conflicts.all()
# Loop over all existing conflicts
for ak in conflicts_of_this_ak: for ak in conflicts_of_this_ak:
if ak != instance: if ak != instance:
for other_slot in ak.akslot_set.filter(start__isnull=False): for other_slot in ak.akslot_set.filter(start__isnull=False):
...@@ -203,8 +217,6 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs): ...@@ -203,8 +217,6 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
# print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -215,23 +227,22 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs): ...@@ -215,23 +227,22 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
@receiver(m2m_changed, sender=AK.prerequisites.through) @receiver(m2m_changed, sender=AK.prerequisites.through)
def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs): def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs):
""" """
Prerequisites of AK changed Signal receiver: Prerequisites of AK changed
""" """
# Only signal after change (post_add, post_delete, post_clear) are relevant # Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"): if not action.startswith("post"):
return return
# print(f"{instance} changed")
event = instance.event event = instance.event
# Conflict(s) changed: Might affect multiple AKs that are conflicts of each other # Prerequisite(s) changed: Might affect multiple AKs that should have a certain order
violation_type = ConstraintViolation.ViolationType.AK_BEFORE_PREREQUISITE violation_type = ConstraintViolation.ViolationType.AK_BEFORE_PREREQUISITE
new_violations = [] new_violations = []
slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False) slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
prerequisites_of_this_ak: [AK] = instance.prerequisites.all() prerequisites_of_this_ak: [AK] = instance.prerequisites.all()
# Loop over all prerequisites
for ak in prerequisites_of_this_ak: for ak in prerequisites_of_this_ak:
if ak != instance: if ak != instance:
for other_slot in ak.akslot_set.filter(start__isnull=False): for other_slot in ak.akslot_set.filter(start__isnull=False):
...@@ -249,8 +260,6 @@ def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs ...@@ -249,8 +260,6 @@ def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
# print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -261,14 +270,12 @@ def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs ...@@ -261,14 +270,12 @@ def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs
@receiver(m2m_changed, sender=AK.requirements.through) @receiver(m2m_changed, sender=AK.requirements.through)
def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs): def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs):
""" """
Requirements of AK changed Signal receiver: Requirements of AK changed
""" """
# Only signal after change (post_add, post_delete, post_clear) are relevant # Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"): if not action.startswith("post"):
return return
# print(f"{instance} changed")
event = instance.event event = instance.event
# Requirement(s) changed: Might affect slots and rooms # Requirement(s) changed: Might affect slots and rooms
...@@ -298,8 +305,6 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs) ...@@ -298,8 +305,6 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs)
c.ak_slots_tmp.add(slot) c.ak_slots_tmp.add(slot)
new_violations.append(c) new_violations.append(c)
# print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -309,8 +314,13 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs) ...@@ -309,8 +314,13 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs)
@receiver(post_save, sender=AKSlot) @receiver(post_save, sender=AKSlot)
def akslot_changed_handler(sender, instance: AKSlot, **kwargs): def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
# Changes might affect: Duplicate parallel, Two in room, Resodeadline """
# print(f"{sender} changed") Signal receiver: AKSlot changed
Changes might affect: Duplicate parallel, Two in room, Resodeadline
"""
# TODO Consider rewriting this very long and complex method to resolve several (style) issues:
# pylint: disable=too-many-nested-blocks,too-many-locals,too-many-branches,too-many-statements
event = instance.event event = instance.event
# == Check for two parallel slots by one of the owners == # == Check for two parallel slots by one of the owners ==
...@@ -341,8 +351,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -341,8 +351,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
# print(f"{owner} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -373,8 +381,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -373,8 +381,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
# print(f"Multiple slots in room {instance.room}: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the slot that was recently changed (important!) # belonging to the slot that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -437,8 +443,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -437,8 +443,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
c.ak_slots_tmp.add(instance) c.ak_slots_tmp.add(instance)
new_violations.append(c) new_violations.append(c)
# print(f"{instance.ak} has the following slots outside availabilities: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -470,8 +474,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -470,8 +474,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
c.ak_slots_tmp.add(instance) c.ak_slots_tmp.add(instance)
new_violations.append(c) new_violations.append(c)
# print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -502,8 +504,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -502,8 +504,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
# print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
...@@ -534,8 +534,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -534,8 +534,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
# print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
...@@ -547,15 +545,21 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -547,15 +545,21 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
new_violations = [cv] if cv is not None else [] new_violations = [cv] if cv is not None else []
# Compare to/update list of existing violations of this type for this slot # Compare to/update list of existing violations of this type for this slot
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED)) existing_violations_to_check = list(
instance.constraintviolation_set.filter(type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED)
)
update_constraint_violations(new_violations, existing_violations_to_check) update_constraint_violations(new_violations, existing_violations_to_check)
@receiver(pre_delete, sender=AKSlot) @receiver(pre_delete, sender=AKSlot)
def akslot_deleted_handler(sender, instance: AKSlot, **kwargs): def akslot_deleted_handler(sender, instance: AKSlot, **kwargs):
# Manually clean up or remove constraint violations that belong to this slot since there is no cascade deletion """
# for many2many relationships. Explicitly listening for AK deletion signals is not necessary since they will Signal receiver: AKSlot deleted
# transitively trigger this signal and we always set both AK and AKSlot references in a constraint violation
Manually clean up or remove constraint violations that belong to this slot since there is no cascade deletion
for many2many relationships. Explicitly listening for AK deletion signals is not necessary since they will
transitively trigger this signal and we always set both AK and AKSlot references in a constraint violation
"""
# print(f"{instance} deleted") # print(f"{instance} deleted")
for cv in instance.constraintviolation_set.all(): for cv in instance.constraintviolation_set.all():
...@@ -566,8 +570,11 @@ def akslot_deleted_handler(sender, instance: AKSlot, **kwargs): ...@@ -566,8 +570,11 @@ def akslot_deleted_handler(sender, instance: AKSlot, **kwargs):
@receiver(post_save, sender=Room) @receiver(post_save, sender=Room)
def room_changed_handler(sender, instance: Room, **kwargs): def room_changed_handler(sender, instance: Room, **kwargs):
# Changes might affect: Room size """
Signal receiver: Room changed
Changes might affect: Room size
"""
# Check room capacities # Check room capacities
violation_type = ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED violation_type = ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED
new_violations = [] new_violations = []
...@@ -583,24 +590,23 @@ def room_changed_handler(sender, instance: Room, **kwargs): ...@@ -583,24 +590,23 @@ def room_changed_handler(sender, instance: Room, **kwargs):
@receiver(m2m_changed, sender=Room.properties.through) @receiver(m2m_changed, sender=Room.properties.through)
def room_requirements_changed_handler(sender, instance: Room, action: str, **kwargs): def room_requirements_changed_handler(sender, instance: Room, action: str, **kwargs):
""" """
Requirements of room changed Signal Receiver: Requirements of room changed
""" """
# Only signal after change (post_add, post_delete, post_clear) are relevant # Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"): if not action.startswith("post"):
return return
# print(f"{instance} changed") # event = instance.event
event = instance.event
# TODO React to changes # TODO React to changes
@receiver(post_save, sender=Availability) @receiver(post_save, sender=Availability)
def availability_changed_handler(sender, instance: Availability, **kwargs): def availability_changed_handler(sender, instance: Availability, **kwargs):
# Changes might affect: category availability, AK availability, Room availability """
# print(f"{instance} changed") Signal receiver: Availalability changed
Changes might affect: category availability, AK availability, Room availability
"""
event = instance.event event = instance.event
# An AK's availability changed: Might affect AK slots scheduled outside the permitted time # An AK's availability changed: Might affect AK slots scheduled outside the permitted time
...@@ -627,8 +633,6 @@ def availability_changed_handler(sender, instance: Availability, **kwargs): ...@@ -627,8 +633,6 @@ def availability_changed_handler(sender, instance: Availability, **kwargs):
c.ak_slots_tmp.add(slot) c.ak_slots_tmp.add(slot)
new_violations.append(c) new_violations.append(c)
# print(f"{instance.ak} has the following slots outside availabilities: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
...@@ -638,7 +642,12 @@ def availability_changed_handler(sender, instance: Availability, **kwargs): ...@@ -638,7 +642,12 @@ def availability_changed_handler(sender, instance: Availability, **kwargs):
@receiver(post_save, sender=Event) @receiver(post_save, sender=Event)
def event_changed_handler(sender, instance: Event, **kwargs): def event_changed_handler(sender, instance: Event, **kwargs):
# == Check for reso ak after reso deadline (which might have changed) == """
Signal receiver: Event changed
Changes might affect: Reso deadline
"""
# Check for reso ak after reso deadline (which might have changed)
if instance.reso_deadline: if instance.reso_deadline:
for slot in instance.akslot_set.filter(start__isnull=False, ak__reso=True): for slot in instance.akslot_set.filter(start__isnull=False, ak__reso=True):
update_cv_reso_deadline_for_slot(slot) update_cv_reso_deadline_for_slot(slot)
......
import json
from datetime import timedelta
from django.test import TestCase from django.test import TestCase
from AKModel.tests import BasicViewTests from django.utils import timezone
from AKModel.tests import BasicViewTests
from AKModel.models import AKSlot, Event, Room
class ModelViewTests(BasicViewTests, TestCase): class ModelViewTests(BasicViewTests, TestCase):
"""
Tests for AKScheduling
"""
fixtures = ['model.json'] fixtures = ['model.json']
VIEWS_STAFF_ONLY = [ VIEWS_STAFF_ONLY = [
# Views
('admin:schedule', {'event_slug': 'kif42'}), ('admin:schedule', {'event_slug': 'kif42'}),
('admin:slots_unscheduled', {'event_slug': 'kif42'}), ('admin:slots_unscheduled', {'event_slug': 'kif42'}),
('admin:constraint-violations', {'slug': 'kif42'}), ('admin:constraint-violations', {'slug': 'kif42'}),
...@@ -14,4 +23,66 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -14,4 +23,66 @@ class ModelViewTests(BasicViewTests, TestCase):
('admin:autocreate-availabilities', {'event_slug': 'kif42'}), ('admin:autocreate-availabilities', {'event_slug': 'kif42'}),
('admin:tracks_manage', {'event_slug': 'kif42'}), ('admin:tracks_manage', {'event_slug': 'kif42'}),
('admin:enter-interest', {'event_slug': 'kif42', 'pk': 1}), ('admin:enter-interest', {'event_slug': 'kif42', 'pk': 1}),
# API (Read)
('model:scheduling-resources-list', {'event_slug': 'kif42'}, 403),
('model:scheduling-constraint-violations-list', {'event_slug': 'kif42'}, 403),
('model:scheduling-events', {'event_slug': 'kif42'}),
('model:scheduling-room-availabilities', {'event_slug': 'kif42'}),
('model:scheduling-default-slots', {'event_slug': 'kif42'}),
] ]
def test_scheduling_of_slot_update(self):
"""
Test rescheduling a slot to a different time or room
"""
self.client.force_login(self.admin_user)
event = Event.get_by_slug('kif42')
# Get the first already scheduled slot belonging to this event
slot = event.akslot_set.filter(start__isnull=False).first()
pk = slot.pk
room_id = slot.room_id
events_api_url = f"/kif42/api/scheduling-event/{pk}/"
# Create updated time
offset = timedelta(hours=1)
new_start_time = slot.start + offset
new_end_time = slot.end + offset
new_start_time_string = timezone.localtime(new_start_time, event.timezone).strftime("%Y-%m-%d %H:%M:%S")
new_end_time_string = timezone.localtime(new_end_time, event.timezone).strftime("%Y-%m-%d %H:%M:%S")
# Try API call
response = self.client.put(
events_api_url,
json.dumps({
'start': new_start_time_string,
'end': new_end_time_string,
'roomId': room_id,
}),
content_type = 'application/json'
)
self.assertEqual(response.status_code, 200, "PUT to API endpoint did not work")
# Make sure API call did update the slot as expected
slot = AKSlot.objects.get(pk=pk)
self.assertEqual(new_start_time, slot.start, "Update did not work")
# Test updating room
new_room = Room.objects.exclude(pk=room_id).first()
# Try second API call
response = self.client.put(
events_api_url,
json.dumps({
'start': new_start_time_string,
'end': new_end_time_string,
'roomId': new_room.pk,
}),
content_type = 'application/json'
)
self.assertEqual(response.status_code, 200, "Second PUT to API endpoint did not work")
# Make sure API call did update the slot as expected
slot = AKSlot.objects.get(pk=pk)
self.assertEqual(new_room.pk, slot.room.pk, "Update did not work")
...@@ -11,6 +11,9 @@ from AKScheduling.forms import AKInterestForm, AKAddSlotForm ...@@ -11,6 +11,9 @@ from AKScheduling.forms import AKInterestForm, AKAddSlotForm
class UnscheduledSlotsAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView): class UnscheduledSlotsAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
"""
Admin view: Get a list of all unscheduled slots
"""
template_name = "admin/AKScheduling/unscheduled.html" template_name = "admin/AKScheduling/unscheduled.html"
model = AKSlot model = AKSlot
context_object_name = "akslots" context_object_name = "akslots"
...@@ -25,6 +28,12 @@ class UnscheduledSlotsAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView ...@@ -25,6 +28,12 @@ class UnscheduledSlotsAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView
class SchedulingAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView): class SchedulingAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
"""
Admin view: Scheduler
View and adapt the schedule of an event. This view heavily uses JavaScript to display a calendar view plus
a list of unscheduled slots and to allow dragging slots in and into the calendar
"""
template_name = "admin/AKScheduling/scheduling.html" template_name = "admin/AKScheduling/scheduling.html"
model = AKSlot model = AKSlot
context_object_name = "slots_unscheduled" context_object_name = "slots_unscheduled"
...@@ -46,6 +55,12 @@ class SchedulingAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView): ...@@ -46,6 +55,12 @@ class SchedulingAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
class TrackAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView): class TrackAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
"""
Admin view: Distribute AKs to tracks
Again using JavaScript, the user can here see a list of all AKs split-up by tracks and can move them to other or
even new tracks using drag and drop. The state is then automatically synchronized via API calls in the background
"""
template_name = "admin/AKScheduling/manage_tracks.html" template_name = "admin/AKScheduling/manage_tracks.html"
model = AKTrack model = AKTrack
context_object_name = "tracks" context_object_name = "tracks"
...@@ -57,6 +72,12 @@ class TrackAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView): ...@@ -57,6 +72,12 @@ class TrackAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
class ConstraintViolationsAdminView(AdminViewMixin, DetailView): class ConstraintViolationsAdminView(AdminViewMixin, DetailView):
"""
Admin view: Inspect and adjust all constraint violations of the event
This view populates a table of constraint violations via background API call (JavaScript), offers the option to
see details or edit each of them and provides an auto-reload feature.
"""
template_name = "admin/AKScheduling/constraint_violations.html" template_name = "admin/AKScheduling/constraint_violations.html"
model = Event model = Event
context_object_name = "event" context_object_name = "event"
...@@ -68,6 +89,10 @@ class ConstraintViolationsAdminView(AdminViewMixin, DetailView): ...@@ -68,6 +89,10 @@ class ConstraintViolationsAdminView(AdminViewMixin, DetailView):
class SpecialAttentionAKsAdminView(AdminViewMixin, DetailView): class SpecialAttentionAKsAdminView(AdminViewMixin, DetailView):
"""
Admin view: List all AKs that require special attention via scheduling, e.g., because of free-form comments,
since there are slots even though it is a wish, or no slots even though it is an AK etc.
"""
template_name = "admin/AKScheduling/special_attention.html" template_name = "admin/AKScheduling/special_attention.html"
model = Event model = Event
context_object_name = "event" context_object_name = "event"
...@@ -76,12 +101,16 @@ class SpecialAttentionAKsAdminView(AdminViewMixin, DetailView): ...@@ -76,12 +101,16 @@ class SpecialAttentionAKsAdminView(AdminViewMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = f"{_('AKs requiring special attention for')} {context['event']}" context["title"] = f"{_('AKs requiring special attention for')} {context['event']}"
aks = AK.objects.filter(event=context["event"]).annotate(Count('owners', distinct=True)).annotate(Count('akslot', distinct=True)).annotate(Count('availabilities', distinct=True)) # Load all "special" AKs from the database using annotations to reduce the amount of necessary queries
aks = (AK.objects.filter(event=context["event"]).annotate(Count('owners', distinct=True))
.annotate(Count('akslot', distinct=True)).annotate(Count('availabilities', distinct=True)))
aks_with_comment = [] aks_with_comment = []
ak_wishes_with_slots = [] ak_wishes_with_slots = []
aks_without_availabilities = [] aks_without_availabilities = []
aks_without_slots = [] aks_without_slots = []
# Loop over all AKs of this event and identify all relevant factors that make the AK "special" and add them to
# the respective lists if the AK fullfills an condition
for ak in aks: for ak in aks:
if ak.notes != "": if ak.notes != "":
aks_with_comment.append(ak) aks_with_comment.append(ak)
...@@ -104,6 +133,14 @@ class SpecialAttentionAKsAdminView(AdminViewMixin, DetailView): ...@@ -104,6 +133,14 @@ class SpecialAttentionAKsAdminView(AdminViewMixin, DetailView):
class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMixin, UpdateView): class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMixin, UpdateView):
"""
Admin view: Form view to quickly store information about the interest in an AK
(e.g., during presentation of the AK list)
The view offers a field to update interest and manually set a comment for the current AK, but also features links
to the AKs before and probably coming up next, as well as links to other AKs sorted by category, for quick
and hazzle-free navigation during the AK presentation
"""
template_name = "admin/AKScheduling/interest.html" template_name = "admin/AKScheduling/interest.html"
model = AK model = AK
context_object_name = "ak" context_object_name = "ak"
...@@ -126,6 +163,8 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi ...@@ -126,6 +163,8 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi
last_ak = None last_ak = None
next_is_next = False next_is_next = False
# Building the right navigation is a bit tricky since wishes have to be treated as an own category here
# Hence, depending on the AK we are currently at (displaying the form for) we need to either:
# Find other AK wishes (regardless of the category)... # Find other AK wishes (regardless of the category)...
if context['ak'].wish: if context['ak'].wish:
other_aks = [ak for ak in context['event'].ak_set.prefetch_related('owners').all() if ak.wish] other_aks = [ak for ak in context['event'].ak_set.prefetch_related('owners').all() if ak.wish]
...@@ -133,6 +172,7 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi ...@@ -133,6 +172,7 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi
else: else:
other_aks = [ak for ak in context['ak'].category.ak_set.prefetch_related('owners').all() if not ak.wish] other_aks = [ak for ak in context['ak'].category.ak_set.prefetch_related('owners').all() if not ak.wish]
# Use that list of other AKs belonging to this category to identify the previous and next AK (if any)
for other_ak in other_aks: for other_ak in other_aks:
if next_is_next: if next_is_next:
context['next_ak'] = other_ak context['next_ak'] = other_ak
...@@ -142,6 +182,7 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi ...@@ -142,6 +182,7 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi
next_is_next = True next_is_next = True
last_ak = other_ak last_ak = other_ak
# Gather information for link lists for all categories (and wishes)
for category in context['event'].akcategory_set.prefetch_related('ak_set').all(): for category in context['event'].akcategory_set.prefetch_related('ak_set').all():
aks_for_category = [] aks_for_category = []
for ak in category.ak_set.prefetch_related('owners').all(): for ak in category.ak_set.prefetch_related('owners').all():
...@@ -151,6 +192,8 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi ...@@ -151,6 +192,8 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi
aks_for_category.append(ak) aks_for_category.append(ak)
categories_with_aks.append((category, aks_for_category)) categories_with_aks.append((category, aks_for_category))
# Make sure wishes have the right order (since the list was filled category by category before, this requires
# explicitly reordering them by their primary key)
ak_wishes.sort(key=lambda x: x.pk) ak_wishes.sort(key=lambda x: x.pk)
categories_with_aks.append( categories_with_aks.append(
(AKCategory(name=_("Wishes"), pk=0, description="-"), ak_wishes)) (AKCategory(name=_("Wishes"), pk=0, description="-"), ak_wishes))
...@@ -161,6 +204,16 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi ...@@ -161,6 +204,16 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi
class WishSlotCleanupView(EventSlugMixin, IntermediateAdminView): class WishSlotCleanupView(EventSlugMixin, IntermediateAdminView):
"""
Admin action view: Allow to delete all unscheduled slots for wishes
The view will render a preview of all slots that are affected by this. It is not possible to manually choose
which slots should be deleted (either all or none) and the functionality will therefore delete slots that were
created in the time between rendering of the preview and running the action ofter confirmation as well.
Due to the automated slot cleanup functionality for wishes in the AKSubmission app, this functionality should be
rarely needed/used
"""
title = _('Cleanup: Delete unscheduled slots for wishes') title = _('Cleanup: Delete unscheduled slots for wishes')
def get_success_url(self): def get_success_url(self):
...@@ -180,6 +233,15 @@ class WishSlotCleanupView(EventSlugMixin, IntermediateAdminView): ...@@ -180,6 +233,15 @@ class WishSlotCleanupView(EventSlugMixin, IntermediateAdminView):
class AvailabilityAutocreateView(EventSlugMixin, IntermediateAdminView): class AvailabilityAutocreateView(EventSlugMixin, IntermediateAdminView):
"""
Admin action view: Allow to automatically create default availabilities (event start to end) for all AKs without
any manually specified availability information
The view will render a preview of all AKs that are affected by this. It is not possible to manually choose
which AKs should be affected (either all or none) and the functionality will therefore create availability entries
for AKs that were created in the time between rendering of the preview and running the action ofter confirmation
as well.
"""
title = _('Create default availabilities for AKs') title = _('Create default availabilities for AKs')
def get_success_url(self): def get_success_url(self):
...@@ -194,6 +256,8 @@ class AvailabilityAutocreateView(EventSlugMixin, IntermediateAdminView): ...@@ -194,6 +256,8 @@ class AvailabilityAutocreateView(EventSlugMixin, IntermediateAdminView):
) )
def form_valid(self, form): def form_valid(self, form):
# Local import to prevent cyclic imports
# pylint: disable=import-outside-toplevel
from AKModel.availability.models import Availability from AKModel.availability.models import Availability
success_count = 0 success_count = 0
...@@ -202,7 +266,7 @@ class AvailabilityAutocreateView(EventSlugMixin, IntermediateAdminView): ...@@ -202,7 +266,7 @@ class AvailabilityAutocreateView(EventSlugMixin, IntermediateAdminView):
availability = Availability.with_event_length(event=self.event, ak=ak) availability = Availability.with_event_length(event=self.event, ak=ak)
availability.save() availability.save()
success_count += 1 success_count += 1
except: except: # pylint: disable=bare-except
messages.add_message( messages.add_message(
self.request, messages.WARNING, self.request, messages.WARNING,
_("Could not create default availabilities for AK: {ak}").format(ak=ak) _("Could not create default availabilities for AK: {ak}").format(ak=ak)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment