diff --git a/AKModel/admin.py b/AKModel/admin.py index 01a450cbf4b3a4c92a35e09b3085de7f596524d9..c5d067e6c725cf1cc201626b7d23b741f2f1af66 100644 --- a/AKModel/admin.py +++ b/AKModel/admin.py @@ -4,7 +4,7 @@ from django.contrib import admin from django.contrib.admin import SimpleListFilter from django.db.models import Count, F from django.shortcuts import render, redirect -from django.urls import path, reverse_lazy +from django.urls import reverse_lazy from django.utils import timezone from django.utils.html import format_html from django.utils.safestring import mark_safe @@ -16,11 +16,7 @@ from AKModel.availability.forms import AvailabilitiesFormMixin from AKModel.availability.models import Availability from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKTag, AKRequirement, AK, AKSlot, Room, AKOrgaMessage, \ ConstraintViolation -from AKModel.views import EventStatusView, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, \ - AKRequirementOverview, \ - NewEventWizardStartView, NewEventWizardSettingsView, NewEventWizardPrepareImportView, NewEventWizardFinishView, \ - NewEventWizardImportView, NewEventWizardActivateView -from AKModel.views import export_slides +from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event @admin.register(Event) @@ -32,42 +28,24 @@ class EventAdmin(admin.ModelAdmin): ordering = ['-start'] def add_view(self, request, form_url='', extra_context=None): - # Always use wizard to create new events - # (the built-in form wouldn't work anyways since the timezone cannot be specified before starting to fill the form) + # Always use wizard to create new events (the built-in form wouldn't work anyways since the timezone cannot + # be specified before starting to fill the form) return redirect("admin:new_event_wizard_start") def get_urls(self): - urls = super().get_urls() - custom_urls = [ - path('add/wizard/start/', self.admin_site.admin_view(NewEventWizardStartView.as_view()), - name="new_event_wizard_start"), - path('add/wizard/settings/', self.admin_site.admin_view(NewEventWizardSettingsView.as_view()), - name="new_event_wizard_settings"), - path('add/wizard/created/<slug:event_slug>/', self.admin_site.admin_view(NewEventWizardPrepareImportView.as_view()), - name="new_event_wizard_prepare_import"), - path('add/wizard/import/<slug:event_slug>/from/<slug:import_slug>/', - self.admin_site.admin_view(NewEventWizardImportView.as_view()), - name="new_event_wizard_import"), - path('add/wizard/activate/<slug:slug>/', - self.admin_site.admin_view(NewEventWizardActivateView.as_view()), - name="new_event_wizard_activate"), - path('add/wizard/finish/<slug:slug>/', - self.admin_site.admin_view(NewEventWizardFinishView.as_view()), - name="new_event_wizard_finish"), - path('<slug:slug>/status/', self.admin_site.admin_view(EventStatusView.as_view()), name="event_status"), - path('<slug:event_slug>/requirements/', self.admin_site.admin_view(AKRequirementOverview.as_view()), name="event_requirement_overview"), - path('<slug:event_slug>/ak-csv-export/', self.admin_site.admin_view(AKCSVExportView.as_view()), name="ak_csv_export"), - path('<slug:slug>/ak-wiki-export/', self.admin_site.admin_view(AKWikiExportView.as_view()), name="ak_wiki_export"), - path('<slug:event_slug>/ak-slide-export/', export_slides, name="ak_slide_export"), - path('<slug:slug>/delete-orga-messages/', self.admin_site.admin_view(AKMessageDeleteView.as_view()), - name="ak_delete_orga_messages"), - ] - return custom_urls + urls + urls = get_admin_urls_event_wizard(self.admin_site) + urls.extend(get_admin_urls_event(self.admin_site)) + if apps.is_installed("AKScheduling"): + from AKScheduling.urls import get_admin_urls_scheduling + urls.extend(get_admin_urls_scheduling(self.admin_site)) + urls.extend(super().get_urls()) + return urls def status_url(self, obj): return format_html("<a href='{url}'>{text}</a>", url=reverse_lazy('admin:event_status', kwargs={'slug': obj.slug}), text=_("Status")) - status_url.short_description = text=_("Status") + + status_url.short_description = _("Status") def get_form(self, request, obj=None, change=False, **kwargs): # Use timezone of event @@ -116,18 +94,6 @@ class AKTrackAdmin(admin.ModelAdmin): kwargs['initial'] = Event.get_next_active() return super(AKTrackAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) - def get_urls(self): - urls = super().get_urls() - custom_urls = [] - if apps.is_installed("AKScheduling"): - from AKScheduling.views import TrackAdminView - - custom_urls.extend([ - path('<slug:event_slug>/manage/', self.admin_site.admin_view(TrackAdminView.as_view()), - name="tracks_manage"), - ]) - return custom_urls + urls - @admin.register(AKTag) class AKTagAdmin(admin.ModelAdmin): @@ -240,7 +206,6 @@ class RoomForm(AvailabilitiesFormMixin, forms.ModelForm): self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event) - @admin.register(Room) class RoomAdmin(admin.ModelAdmin): model = Room @@ -281,20 +246,6 @@ class AKSlotAdmin(admin.ModelAdmin): readonly_fields = ['ak_details_link', 'updated'] form = AKSlotAdminForm - def get_urls(self): - urls = super().get_urls() - custom_urls = [] - if apps.is_installed("AKScheduling"): - from AKScheduling.views import SchedulingAdminView, UnscheduledSlotsAdminView - - custom_urls.extend([ - path('<slug:event_slug>/schedule/', self.admin_site.admin_view(SchedulingAdminView.as_view()), - name="schedule"), - path('<slug:event_slug>/unscheduled/', self.admin_site.admin_view(UnscheduledSlotsAdminView.as_view()), - name="slots_unscheduled"), - ]) - return custom_urls + urls - def get_form(self, request, obj=None, change=False, **kwargs): # Use timezone of associated event if obj is not None and obj.event.timezone: @@ -310,10 +261,11 @@ class AKSlotAdmin(admin.ModelAdmin): return super(AKSlotAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) def ak_details_link(self, akslot): - if apps.is_installed("AKScheduling") and akslot.ak is not None: + if apps.is_installed("AKSubmission") and akslot.ak is not None: link = f"<a href={reverse('submit:ak_detail', args=[akslot.event.slug, akslot.ak.pk])}>{str(akslot.ak)}</a>" return mark_safe(link) return "-" + ak_details_link.short_description = _('AK Details') diff --git a/AKModel/environment.py b/AKModel/environment.py index 5883361b0d8a80f647e131185dabca8aaa4d4bd7..a6536beecbdbb0945b73ad414744d15d8fc87bdb 100644 --- a/AKModel/environment.py +++ b/AKModel/environment.py @@ -7,6 +7,7 @@ from django_tex.environment import environment # and would hence cause compilation errors utf8_replace_pattern = re.compile(u'[^\u0000-\u206F]', re.UNICODE) + def latex_escape_utf8(value): """ Escape latex special chars and remove invalid utf-8 values @@ -16,7 +17,10 @@ def latex_escape_utf8(value): :return: escaped string :rtype: str """ - return utf8_replace_pattern.sub('', value).replace('&', '\&').replace('_', '\_').replace('#', '\#').replace('$', '\$').replace('%', '\%').replace('{', '\{').replace('}', '\}') + return utf8_replace_pattern.sub('', value).replace('&', '\&').replace('_', '\_').replace('#', '\#').replace('$', + '\$').replace( + '%', '\%').replace('{', '\{').replace('}', '\}') + def improved_tex_environment(**options): env = environment(**options) diff --git a/AKModel/models.py b/AKModel/models.py index ee27d4f95cfce116ad9f26729cec599099f9bb3e..b2f15fd54693a27ac8b2ae316540f4f25da4a97a 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -360,7 +360,8 @@ class AKSlot(models.Model): duration = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Duration'), help_text=_('Length in hours')) - fixed = models.BooleanField(default=False, verbose_name=_('Scheduling fixed'), help_text=_('Length and time of this AK should not be changed')) + fixed = models.BooleanField(default=False, verbose_name=_('Scheduling fixed'), + help_text=_('Length and time of this AK should not be changed')) event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'), help_text=_('Associated event')) @@ -423,6 +424,9 @@ class AKSlot(models.Model): """ return (timezone.now() - self.updated).total_seconds() + def overlaps(self, other: "AKSlot"): + return self.start <= other.end <= self.end or self.start <= other.start <= self.end + class AKOrgaMessage(models.Model): ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'), @@ -494,7 +498,12 @@ class ConstraintViolation(models.Model): help_text=_('Mark this violation manually as resolved')) fields = ['ak_owner', 'room', 'requirement', 'category'] - fields_mm = ['aks', 'ak_slots'] + fields_mm = ['_aks', '_ak_slots'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.aks_tmp = set() + self.ak_slots_tmp = set() def get_details(self): """ @@ -502,10 +511,10 @@ class ConstraintViolation(models.Model): :return: string of details :rtype: str """ - output = [] - # Stringify all ManyToMany fields - for field_mm in self.fields_mm: - output.append(f"{field_mm}: {', '.join(str(a) for a in getattr(self, field_mm).all())}") + # Stringify aks and ak slots fields (m2m) + output = [f"{_('AKs')}: {self._aks_str}", + f"{_('AK Slots')}: {self._ak_slots_str}"] + # Stringify all other fields for field in self.fields: a = getattr(self, field, None) @@ -515,5 +524,102 @@ class ConstraintViolation(models.Model): get_details.short_description = _('Details') + @property + def details(self): + return self.get_details() + + @property + def level_display(self): + return self.get_level_display() + + @property + def type_display(self): + return self.get_type_display() + + @property + def timestamp_display(self): + return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M') + + @property + def _aks(self): + """ + Get all AKs belonging to this constraint violation + + The distinction between real and tmp relationships is needed since many to many + relations only work for objects already persisted in the database + + :return: set of all AKs belonging to this constraint violation + :rtype: set(AK) + """ + if self.pk and self.pk > 0: + return set(self.aks.all()) + return self.aks_tmp + + @property + def _aks_str(self): + if self.pk and self.pk > 0: + return ', '.join(str(a) for a in self.aks.all()) + return ', '.join(str(a) for a in self.aks_tmp) + + @property + def _ak_slots(self): + """ + Get all AK Slots belonging to this constraint violation + + The distinction between real and tmp relationships is needed since many to many + relations only work for objects already persisted in the database + + :return: set of all AK Slots belonging to this constraint violation + :rtype: set(AKSlot) + """ + if self.pk and self.pk > 0: + return set(self.ak_slots.all()) + return self.ak_slots_tmp + + @property + def _ak_slots_str(self): + if self.pk and self.pk > 0: + return ', '.join(str(a) for a in self.ak_slots.all()) + return ', '.join(str(a) for a in self.ak_slots_tmp) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # Store temporary m2m-relations in db + for ak in self.aks_tmp: + self.aks.add(ak) + for ak_slot in self.ak_slots_tmp: + self.ak_slots.add(ak_slot) + def __str__(self): return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]" + + def matches(self, other): + """ + Check whether one constraint violation instance matches another, + this means has the same type, room, requirement, owner, category + as well as the same lists of aks and ak slots. + PK, timestamp, comments and manual resolving are ignored. + + :param other: second instance to compare to + :type other: ConstraintViolation + :return: true if both instances are similar in the way described, false if not + :rtype: bool + """ + if not isinstance(other, ConstraintViolation): + return False + # Check type + if self.type != other.type: + return False + # Make sure both have the same aks and ak slots + for field_mm in self.fields_mm: + s: set = getattr(self, field_mm) + o: set = getattr(other, field_mm) + if len(s) != len(o): + return False + if len(s.intersection(o)) != len(s): + return False + # Check other "defining" fields + for field in self.fields: + if getattr(self, field) != getattr(other, field): + return False + return True diff --git a/AKModel/templates/admin/AKModel/status.html b/AKModel/templates/admin/AKModel/status.html index 0f5369fac2d89e1d444a98f0855a2b1234148e2e..8ccf44e3dfca2f4ba843c75f4739a5d853dac717 100644 --- a/AKModel/templates/admin/AKModel/status.html +++ b/AKModel/templates/admin/AKModel/status.html @@ -74,6 +74,10 @@ <a class="btn btn-success" href="{% url 'admin:schedule' event_slug=event.slug %}">{% trans "Scheduling" %}</a> + {% if "AKScheduling | is_installed" %} + <a class="btn btn-success" + href="{% url 'admin:constraint-violations' slug=event.slug %}">{% trans "Constraint Violations" %} <span class="badge badge-secondary">{{ event.constraintviolation_set.count }}</span></a> + {% endif %} <a class="btn btn-success" href="{% url 'admin:tracks_manage' event_slug=event.slug %}">{% trans "Manage ak tracks" %}</a> <a class="btn btn-success" diff --git a/AKModel/urls.py b/AKModel/urls.py index 9b1f7591ee877043baca3cb63aac843dcb64ac4d..f70ed739547180a017cdb799a126be082cb05d7d 100644 --- a/AKModel/urls.py +++ b/AKModel/urls.py @@ -3,6 +3,9 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter from AKModel import views +from AKModel.views import NewEventWizardStartView, NewEventWizardSettingsView, NewEventWizardPrepareImportView, \ + NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, EventStatusView, \ + AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, export_slides api_router = DefaultRouter() api_router.register('akowner', views.AKOwnerViewSet, basename='AKOwner') @@ -12,29 +15,29 @@ api_router.register('ak', views.AKViewSet, basename='AK') api_router.register('room', views.RoomViewSet, basename='Room') api_router.register('akslot', views.AKSlotViewSet, basename='AKSlot') - extra_paths = [] if apps.is_installed("AKScheduling"): - from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView, EventsViewSet + from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView, EventsViewSet, \ + ConstraintViolationsViewSet api_router.register('scheduling-resources', ResourcesViewSet, basename='scheduling-resources') api_router.register('scheduling-event', EventsViewSet, basename='scheduling-event') + api_router.register('scheduling-constraint-violations', ConstraintViolationsViewSet, + basename='scheduling-constraint-violations') extra_paths = [ path('api/scheduling-events/', EventsView.as_view(), name='scheduling-events'), - path('api/scheduling-room-availabilities/', RoomAvailabilitiesView.as_view(), name='scheduling-room-availabilities'), + path('api/scheduling-room-availabilities/', RoomAvailabilitiesView.as_view(), + name='scheduling-room-availabilities'), ] - event_specific_paths = [ - path('api/', include(api_router.urls), name='api'), - ] + path('api/', include(api_router.urls), name='api'), +] event_specific_paths.extend(extra_paths) - app_name = 'model' - urlpatterns = [ path( '<slug:event_slug>/', @@ -42,3 +45,39 @@ urlpatterns = [ ), path('user/', views.UserView.as_view(), name="user"), ] + + +def get_admin_urls_event_wizard(admin_site): + return [ + path('add/wizard/start/', admin_site.admin_view(NewEventWizardStartView.as_view()), + name="new_event_wizard_start"), + path('add/wizard/settings/', admin_site.admin_view(NewEventWizardSettingsView.as_view()), + name="new_event_wizard_settings"), + path('add/wizard/created/<slug:event_slug>/', admin_site.admin_view(NewEventWizardPrepareImportView.as_view()), + name="new_event_wizard_prepare_import"), + path('add/wizard/import/<slug:event_slug>/from/<slug:import_slug>/', + admin_site.admin_view(NewEventWizardImportView.as_view()), + name="new_event_wizard_import"), + path('add/wizard/activate/<slug:slug>/', + admin_site.admin_view(NewEventWizardActivateView.as_view()), + name="new_event_wizard_activate"), + path('add/wizard/finish/<slug:slug>/', + admin_site.admin_view(NewEventWizardFinishView.as_view()), + name="new_event_wizard_finish"), + ] + + +def get_admin_urls_event(admin_site): + return [ + path('<slug:slug>/status/', admin_site.admin_view(EventStatusView.as_view()), name="event_status"), + path('<slug:event_slug>/requirements/', admin_site.admin_view(AKRequirementOverview.as_view()), + name="event_requirement_overview"), + path('<slug:event_slug>/ak-csv-export/', admin_site.admin_view(AKCSVExportView.as_view()), + name="ak_csv_export"), + path('<slug:slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()), + name="ak_wiki_export"), + path('<slug:slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()), + name="ak_delete_orga_messages"), + path('<slug:event_slug>/ak-slide-export/', export_slides, name="ak_slide_export"), + + ] diff --git a/AKModel/views.py b/AKModel/views.py index 63b0e7d2fab1d7a6c9076b6355580f7660467f14..3132de5f9fcb3c6eaa70cfb3f51902ae0adc73f4 100644 --- a/AKModel/views.py +++ b/AKModel/views.py @@ -7,8 +7,8 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views.generic import TemplateView, DetailView, ListView, DeleteView, CreateView, FormView, UpdateView -from rest_framework import viewsets, permissions, mixins from django_tex.shortcuts import render_to_pdf +from rest_framework import viewsets, permissions, mixins from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \ NewEventWizardImportForm, NewEventWizardActivateForm diff --git a/AKPlanning/settings_ci.py b/AKPlanning/settings_ci.py index 84a135c53da491761facfa52f1f2b41084d331a8..55de6ac5c1caee213bd2f61b144512e2ea0ff8c2 100644 --- a/AKPlanning/settings_ci.py +++ b/AKPlanning/settings_ci.py @@ -17,7 +17,7 @@ DATABASES = { 'OPTIONS': { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'" }, - 'TEST' : { + 'TEST': { 'NAME': 'test', }, } diff --git a/AKScheduling/api.py b/AKScheduling/api.py index 27b4252e94adc1ffa44e4d2481bc94987d7087f1..af73c791053ddab2e6922f8b69610914640d9ff6 100644 --- a/AKScheduling/api.py +++ b/AKScheduling/api.py @@ -7,7 +7,7 @@ from django.views.generic import ListView from rest_framework import viewsets, mixins, serializers, permissions from AKModel.availability.models import Availability -from AKModel.models import Room, AKSlot +from AKModel.models import Room, AKSlot, ConstraintViolation from AKModel.views import EventSlugMixin @@ -109,3 +109,20 @@ class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet): def get_queryset(self): return AKSlot.objects.filter(event=self.event) + + +class ConstraintViolationSerializer(serializers.ModelSerializer): + class Meta: + model = ConstraintViolation + fields = ['pk', 'type_display', 'aks', 'ak_slots', 'ak_owner', 'room', 'requirement', 'category', 'comment', 'timestamp_display', 'manually_resolved', 'level_display', 'details'] + + +class ConstraintViolationsViewSet(EventSlugMixin, viewsets.ModelViewSet): + permission_classes = (permissions.DjangoModelPermissions,) + serializer_class = ConstraintViolationSerializer + + def get_object(self): + return get_object_or_404(ConstraintViolation, pk=self.kwargs["pk"]) + + def get_queryset(self): + return ConstraintViolation.objects.filter(event=self.event) diff --git a/AKScheduling/locale/de_DE/LC_MESSAGES/django.po b/AKScheduling/locale/de_DE/LC_MESSAGES/django.po index f25c002dbd9be02bcac1bd45292140a48688de0d..5ed003e89f16deaac8e309561323c1cb974345d0 100644 --- a/AKScheduling/locale/de_DE/LC_MESSAGES/django.po +++ b/AKScheduling/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-29 22:48+0000\n" +"POT-Creation-Date: 2021-05-09 18:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -17,49 +17,92 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:11 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:11 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:10 msgid "Scheduling for" msgstr "Scheduling für" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:126 +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:74 +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:128 +msgid "No violations" +msgstr "Keine Verletzungen" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:81 +msgid "Cannot load current violations from server" +msgstr "Kann die aktuellen Verletzungen nicht vom Server laden" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:106 +msgid "Violation(s)" +msgstr "Verletzung(en)" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:109 +msgid "Auto reload?" +msgstr "Automatisch neu laden?" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:113 +msgid "Reload now" +msgstr "Jetzt neu laden" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:118 +msgid "Violation" +msgstr "Verletzung" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:119 +msgid "Problem" +msgstr "Problem" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:120 +msgid "Details" +msgstr "Details" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:121 +msgid "Since" +msgstr "Seit" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:134 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:243 +#: AKScheduling/templates/admin/AKScheduling/scheduling.html:197 +#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34 +msgid "Event Status" +msgstr "Event-Status" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:136 +msgid "Scheduling" +msgstr "Scheduling" + +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:129 msgid "Name of new ak track" msgstr "Name des neuen AK-Tracks" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:142 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:145 msgid "Could not create ak track" msgstr "Konnte neuen AK-Track nicht anlegen" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:168 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:171 msgid "Could not update ak track name" msgstr "Konnte Namen des AK-Tracks nicht ändern" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:174 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:177 msgid "Do you really want to delete this ak track?" msgstr "Soll dieser AK-Track wirklich gelöscht werden?" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:188 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:191 msgid "Could not delete ak track" msgstr "AK-Track konnte nicht gelöscht werden" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:200 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:203 msgid "Manage AK Tracks" msgstr "AK-Tracks verwalten" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:201 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:204 msgid "Add ak track" msgstr "AK-Track hinzufügen" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:206 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:209 msgid "AKs without track" msgstr "AKs ohne Track" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:240 -#: AKScheduling/templates/admin/AKScheduling/scheduling.html:197 -#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34 -msgid "Event Status" -msgstr "Event-Status" - #: AKScheduling/templates/admin/AKScheduling/scheduling.html:87 msgid "Day (Horizontal)" msgstr "Tag (horizontal)" diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 6b2021999398416a78191ac543b7e0e34d86bc2c..f148da47d1f7b1954bae06cd85d8f39655b63d35 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -1 +1,545 @@ -# Create your models here. +from django.db.models.signals import post_save, m2m_changed +from django.dispatch import receiver + +from AKModel.availability.models import Availability +from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation + + +def update_constraint_violations(new_violations, existing_violations_to_check): + """ + Update existing constraint violations (subset for which new violations were computed) based on these new violations. + This will add all new violations without a match, preserve the matching ones + and delete the obsolete ones (those without a match from the newly calculated violations). + + :param new_violations: list of new (not yet saved) violations that exist after the last change + :type new_violations: list[ConstraintViolation] + :param existing_violations_to_check: list of related violations currently in the db + :type existing_violations_to_check: list[ConstraintViolation] + """ + for new_violation in new_violations: + found_match = False + for existing_violation in existing_violations_to_check: + if existing_violation.matches(new_violation): + # Remove from existing violations set since it should stay in db + existing_violations_to_check.remove(existing_violation) + found_match = True + break + + # Only save new violation if no match was found + if not found_match: + new_violation.save() + + # Cleanup obsolete violations (ones without matches computed under current conditions) + for outdated_violation in existing_violations_to_check: + outdated_violation.delete() + + +def update_cv_reso_deadline_for_slot(slot): + """ + Update constraint violation AK_AFTER_RESODEADLINE for given slot + + :param slot: slot to check/update + :type slot: AKSlot + """ + event = slot.event + if slot.ak.reso and slot.event.reso_deadline and slot.start: + violation_type = ConstraintViolation.ViolationType.AK_AFTER_RESODEADLINE + new_violations = [] + if slot.end > event.reso_deadline: + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ) + c.aks_tmp.add(slot.ak) + c.ak_slots_tmp.add(slot) + new_violations.append(c) + update_constraint_violations(new_violations, list(slot.constraintviolation_set.filter(type=violation_type))) + + +@receiver(post_save, sender=AK) +def ak_changed_handler(sender, instance: AK, **kwargs): + # Changes might affect: Reso intention, Category, Interest + # TODO Reso intention changes + pass + + +# TODO adapt for Room's reauirements +@receiver(m2m_changed, sender=AK.owners.through) +def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs): + """ + Owners of AK changed + """ + # Only signal after change (post_add, post_delete, post_clear) are relevant + if not action.startswith("post"): + return + + # print(f"{instance} changed") + + event = instance.event + + # Owner(s) changed: Might affect multiple AKs by the same owner(s) at the same time + violation_type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS + new_violations = [] + + slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False) + + # For all owners (after recent change)... + for owner in instance.owners.all(): + # ...find other slots that might be overlapping... + + for ak in owner.ak_set.all(): + # ...find overlapping slots... + if ak != instance: + for slot in slots_of_this_ak: + for other_slot in ak.akslot_set.filter(start__isnull=False): + if slot.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ak_owner=owner + ) + c.aks_tmp.add(instance) + c.aks_tmp.add(other_slot.ak) + c.ak_slots_tmp.add(slot) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + print(f"{owner} has the following conflicts: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + +@receiver(m2m_changed, sender=AK.conflicts.through) +def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs): + """ + Conflicts of AK changed + """ + # Only signal after change (post_add, post_delete, post_clear) are relevant + if not action.startswith("post"): + return + + # print(f"{instance} changed") + + event = instance.event + + # Conflict(s) changed: Might affect multiple AKs that are conflicts of each other + violation_type = ConstraintViolation.ViolationType.AK_CONFLICT_COLLISION + new_violations = [] + + slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False) + conflicts_of_this_ak: [AK] = instance.conflicts.all() + + for ak in conflicts_of_this_ak: + if ak != instance: + for other_slot in ak.akslot_set.filter(start__isnull=False): + for slot in slots_of_this_ak: + # ...find overlapping slots... + if slot.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ) + c.aks_tmp.add(instance) + c.ak_slots_tmp.add(slot) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + print(f"{instance} has the following conflicts: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + +@receiver(m2m_changed, sender=AK.prerequisites.through) +def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs): + """ + Prerequisites of AK changed + """ + # Only signal after change (post_add, post_delete, post_clear) are relevant + if not action.startswith("post"): + return + + # print(f"{instance} changed") + + event = instance.event + + # Conflict(s) changed: Might affect multiple AKs that are conflicts of each other + violation_type = ConstraintViolation.ViolationType.AK_BEFORE_PREREQUISITE + new_violations = [] + + slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False) + prerequisites_of_this_ak: [AK] = instance.prerequisites.all() + + for ak in prerequisites_of_this_ak: + if ak != instance: + for other_slot in ak.akslot_set.filter(start__isnull=False): + for slot in slots_of_this_ak: + # ...find overlapping slots... + if other_slot.end > slot.start: + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ) + c.aks_tmp.add(instance) + c.ak_slots_tmp.add(slot) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + print(f"{instance} has the following conflicts: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + +@receiver(m2m_changed, sender=AK.requirements.through) +def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs): + """ + Requirements of AK changed + """ + # Only signal after change (post_add, post_delete, post_clear) are relevant + if not action.startswith("post"): + return + + # print(f"{instance} changed") + + event = instance.event + + # Requirement(s) changed: Might affect slots and rooms + violation_type = ConstraintViolation.ViolationType.REQUIRE_NOT_GIVEN + new_violations = [] + + slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False) + + # For all requirements (after recent change)... + for slot in slots_of_this_ak: + + room = slot.room + room_requirements = room.properties.all() + + for requirement in instance.requirements.all(): + + if not requirement in room_requirements: + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + requirement=requirement, + room=room, + ) + c.aks_tmp.add(instance) + c.ak_slots_tmp.add(slot) + new_violations.append(c) + + print(f"{instance} has the following conflicts: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + +@receiver(post_save, sender=AKSlot) +def akslot_changed_handler(sender, instance: AKSlot, **kwargs): + # Changes might affect: Duplicate parallel, Two in room, Resodeadline + print(f"{sender} changed") + event = instance.event + + # == Check for two parallel slots by one of the owners == + + violation_type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS + new_violations = [] + + # For all owners (after recent change)... + for owner in instance.ak.owners.all(): + # ...find other slots that might be overlapping... + + for ak in owner.ak_set.all(): + # ...find overlapping slots... + if ak != instance.ak: + for other_slot in ak.akslot_set.filter(start__isnull=False): + if instance.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ak_owner=owner + ) + c.aks_tmp.add(instance.ak) + c.aks_tmp.add(other_slot.ak) + c.ak_slots_tmp.add(instance) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + print(f"{owner} has the following conflicts: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + # == Check for two aks in the same room at the same time == + + violation_type = ConstraintViolation.ViolationType.ROOM_TWO_SLOTS + new_violations = [] + + # For all slots in this room... + if instance.room: + for other_slot in instance.room.akslot_set.all(): + if other_slot != instance: + # ... find overlapping slots... + if instance.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.WARNING, + event=event, + room=instance.room + ) + c.aks_tmp.add(instance.ak) + c.aks_tmp.add(other_slot.ak) + c.ak_slots_tmp.add(instance) + c.ak_slots_tmp.add(other_slot) + 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 + # belonging to the slot that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + # == Check for reso ak after reso deadline == + + update_cv_reso_deadline_for_slot(instance) + + # == Check for two slots of the same AK at the same time (warning) == + + violation_type = ConstraintViolation.ViolationType.AK_SLOT_COLLISION + new_violations = [] + + # For all other slots of this ak... + for other_slot in instance.ak.akslot_set.filter(start__isnull=False): + if other_slot != instance: + # ... find overlapping slots... + if instance.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.WARNING, + event=event, + ) + c.aks_tmp.add(instance.ak) + c.ak_slots_tmp.add(instance) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + # ... and compare to/update list of existing violations of this type + # belonging to the slot that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + update_constraint_violations(new_violations, existing_violations_to_check) + + # == Check for slot outside availability == + + # An AK's availability changed: Might affect AK slots scheduled outside the permitted time + violation_type = ConstraintViolation.ViolationType.SLOT_OUTSIDE_AVAIL + new_violations = [] + + if instance.start: + availabilities_of_this_ak: [Availability] = instance.ak.availabilities.all() + + covered = False + + for availability in availabilities_of_this_ak: + covered = availability.start <= instance.start and availability.end >= instance.end + if covered: + break + if not covered: + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event + ) + c.aks_tmp.add(instance.ak) + c.ak_slots_tmp.add(instance) + 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 + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + # == Check for requirement not fulfilled by room == + + # Room(s) changed: Might affect slots and rooms + violation_type = ConstraintViolation.ViolationType.REQUIRE_NOT_GIVEN + new_violations = [] + + if instance.room: + + room_requirements = instance.room.properties.all() + + for requirement in instance.ak.requirements.all(): + + if requirement not in room_requirements: + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + requirement=requirement, + room=instance.room, + ) + c.aks_tmp.add(instance.ak) + c.ak_slots_tmp.add(instance) + new_violations.append(c) + + print(f"{instance} has the following conflicts: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + # == check for simultaneous slots of conflicting AKs == + + violation_type = ConstraintViolation.ViolationType.AK_CONFLICT_COLLISION + new_violations = [] + + if instance.start: + conflicts_of_this_ak: [AK] = instance.ak.conflicts.all() + + for ak in conflicts_of_this_ak: + if ak != instance.ak: + for other_slot in ak.akslot_set.filter(start__isnull=False): + # ...find overlapping slots... + if instance.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ) + c.aks_tmp.add(instance.ak) + c.ak_slots_tmp.add(instance) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + print(f"{instance} has the following conflicts: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + # == check for missing prerequisites == + + violation_type = ConstraintViolation.ViolationType.AK_BEFORE_PREREQUISITE + new_violations = [] + + if instance.start: + prerequisites_of_this_ak: [AK] = instance.ak.prerequisites.all() + + for ak in prerequisites_of_this_ak: + if ak != instance.ak: + for other_slot in ak.akslot_set.filter(start__isnull=False): + # ...find slots in the wrong order... + if other_slot.end > instance.start: + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ) + c.aks_tmp.add(instance.ak) + c.ak_slots_tmp.add(instance) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + print(f"{instance} has the following conflicts: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + +@receiver(post_save, sender=Room) +def room_changed_handler(sender, **kwargs): + # Changes might affect: Room size + print(f"{sender} changed") + + +@receiver(post_save, sender=Availability) +def availability_changed_handler(sender, instance: Availability, **kwargs): + # Changes might affect: category availability, AK availability, Room availability + print(f"{instance} changed") + + event = instance.event + + # An AK's availability changed: Might affect AK slots scheduled outside the permitted time + if instance.ak: + violation_type = ConstraintViolation.ViolationType.SLOT_OUTSIDE_AVAIL + new_violations = [] + + availabilities_of_this_ak: [Availability] = instance.ak.availabilities.all() + slots_of_this_ak: [AKSlot] = instance.ak.akslot_set.filter(start__isnull=False) + + for slot in slots_of_this_ak: + covered = False + for availability in availabilities_of_this_ak: + covered = availability.start <= slot.start and availability.end >= slot.end + if covered: + break + if not covered: + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event + ) + c.aks_tmp.add(instance.ak) + c.ak_slots_tmp.add(slot) + new_violations.append(c) + + print(f"{instance.ak} has the following slots putside availabilities: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + +@receiver(post_save, sender=Event) +def event_changed_handler(sender, instance, **kwargs): + # == Check for reso ak after reso deadline (which might have changed) == + if instance.reso_deadline: + for slot in instance.akslot_set.filter(start__isnull=False, ak__reso=True): + update_cv_reso_deadline_for_slot(slot) diff --git a/AKScheduling/templates/admin/AKScheduling/constraint_violations.html b/AKScheduling/templates/admin/AKScheduling/constraint_violations.html new file mode 100644 index 0000000000000000000000000000000000000000..8645c192829c9bc58f91b45e78d1be2819b2892e --- /dev/null +++ b/AKScheduling/templates/admin/AKScheduling/constraint_violations.html @@ -0,0 +1,137 @@ +{% extends "admin/base_site.html" %} +{% load tags_AKModel %} + +{% load i18n %} +{% load l10n %} +{% load tz %} +{% load static %} +{% load tags_AKPlan %} +{% load fontawesome_5 %} + +{% block title %}{% trans "Scheduling for" %} {{event}}{% endblock %} + +{% block extrahead %} + {{ block.super }} + + <script> + document.addEventListener('DOMContentLoaded', function () { + // CSRF Protection/Authentication + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + const csrftoken = getCookie('csrftoken'); + + function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); + } + + $.ajaxSetup({ + beforeSend: function (xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", csrftoken); + } + } + }); + + // (Re-)Load constraint violations using AJAX and visualize using violation count badge and violation table + function reload() { + $.ajax({ + url: "{% url "model:scheduling-constraint-violations-list" event_slug=event.slug %}", + type: 'GET', + success: function (response) { + console.log(response); + + let table_html = ''; + + if(response.length > 0) { + // Update violation count badge + $('#violationCountBadge').html(response.length).removeClass('badge-success').addClass('badge-warning'); + + // Update violations table + for(let i=0;i<response.length;i++) { + table_html += "<tr><td>" + response[i].level_display + "</td><td>" + response[i].type_display + "</td><td>" + response[i].details + "</td><td>" + response[i].timestamp_display + "</td><td></td></tr>"; + } + } + else { + // Update violation count badge + $('#violationCountBadge').html(0).removeClass('badge-warning').addClass('badge-success'); + + // Update violations table + table_html ='<tr class="text-muted"><td colspan="5" class="text-center">{% trans "No violations" %}</td></tr>' + } + + // Show violation list (potentially empty) in violations table + $('#violationsTableBody').html(table_html); + }, + error: function (response) { + alert("{% trans 'Cannot load current violations from server' %}"); + } + }); + } + reload(); + + // Bind reload button + $('#btnReloadNow').click(reload); + + // Toggle automatic reloading (every 30 s) based on checkbox + let autoReloadInterval = undefined; + $('#cbxAutoReload').change(function () { + if(this.checked) { + autoReloadInterval = setInterval(reload, 30*1000); + } + else { + if(autoReloadInterval !== undefined) + clearInterval(autoReloadInterval); + } + }); + }); + </script> +{% endblock extrahead %} + +{% block content %} + <h4 class="mt-4 mb-4"><span id="violationCountBadge" class="badge badge-success">0</span> {% trans "Violation(s)" %}</h4> + + <input type="checkbox" id="cbxAutoReload"> + <label for="cbxAutoReload">{% trans "Auto reload?" %}</label> + + <br> + + <a href="#" id="btnReloadNow" class="btn btn-info">{% fa5_icon "sync-alt" "fas" %} {% trans "Reload now" %}</a> + + <table class="table table-striped mt-4 mb-4"> + <thead> + <tr> + <th>{% trans "Violation" %}</th> + <th>{% trans "Problem" %}</th> + <th>{% trans "Details" %}</th> + <th>{% trans "Since" %}</th> + <th></th> + </tr> + </thead> + <tbody id="violationsTableBody"> + <tr class="text-muted"> + <td colspan="5" class="text-center"> + {% trans "No violations" %} + </td> + </tr> + </tbody> + </table> + + <a href="{% url 'admin:event_status' event.slug %}">{% trans "Event Status" %}</a> + · + <a href="{% url 'admin:schedule' event.slug %}">{% trans "Scheduling" %}</a> +{% endblock %} diff --git a/AKScheduling/urls.py b/AKScheduling/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..93f490950d1b9299a90eb74e3ce0e0b76b2b518f --- /dev/null +++ b/AKScheduling/urls.py @@ -0,0 +1,17 @@ +from django.urls import path + +from AKScheduling.views import SchedulingAdminView, UnscheduledSlotsAdminView, TrackAdminView, \ + ConstraintViolationsAdminView + + +def get_admin_urls_scheduling(admin_site): + return [ + path('<slug:event_slug>/schedule/', admin_site.admin_view(SchedulingAdminView.as_view()), + name="schedule"), + path('<slug:event_slug>/unscheduled/', admin_site.admin_view(UnscheduledSlotsAdminView.as_view()), + name="slots_unscheduled"), + path('<slug:slug>/constraint-violations/', admin_site.admin_view(ConstraintViolationsAdminView.as_view()), + name="constraint-violations"), + path('<slug:event_slug>/tracks/', admin_site.admin_view(TrackAdminView.as_view()), + name="tracks_manage"), + ] diff --git a/AKScheduling/views.py b/AKScheduling/views.py index 86445faccabfb643683794a5e478c1ffaf813fb5..57ec6a211e83a6a6d812c2c8167d45f73350eaf1 100644 --- a/AKScheduling/views.py +++ b/AKScheduling/views.py @@ -1,7 +1,7 @@ -from django.views.generic import ListView from django.utils.translation import gettext_lazy as _ +from django.views.generic import ListView, DetailView -from AKModel.models import AKSlot, AKTrack +from AKModel.models import AKSlot, AKTrack, Event from AKModel.views import AdminViewMixin, FilterByEventSlugMixin @@ -47,3 +47,14 @@ class TrackAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView): context = super().get_context_data(object_list=object_list, **kwargs) context["aks_without_track"] = self.event.ak_set.filter(track=None) return context + + +class ConstraintViolationsAdminView(AdminViewMixin, DetailView): + template_name = "admin/AKScheduling/constraint_violations.html" + model = Event + context_object_name = "event" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = f"{_('Constraint violations for')} {context['event']}" + return context diff --git a/INSTALL.md b/INSTALL.md index 29c9070256f067c320439c4d94f733a9aa83a943..407945e82631fc8b733940804cd5ac0c5075561f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -2,42 +2,39 @@ This repository contains a Django project with several apps. - ## Requirements -AKPlanning has two types of requirements: System requirements are dependent on operating system and need to be installed manually beforehand. Python requirements will be installed inside a virtual environment (strongly recommended) during setup. - +AKPlanning has two types of requirements: System requirements are dependent on operating system and need to be installed +manually beforehand. Python requirements will be installed inside a virtual environment (strongly recommended) during +setup. ### System Requirements * Python 3.7 incl. development tools * Virtualenv -* pdflatex & beamer class (`texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-extra texlive-luatex`) +* pdflatex & beamer + class (`texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-extra texlive-luatex`) * for production using uwsgi: - * C compiler e.g. gcc - * uwsgi - * uwsgi Python3 plugin + * C compiler e.g. gcc + * uwsgi + * uwsgi Python3 plugin * for production using Apache (in addition to uwsgi) - * the mod proxy uwsgi plugin for apache2 - + * the mod proxy uwsgi plugin for apache2 ### Python Requirements Python requirements are listed in ``requirements.txt``. They can be installed with pip using ``-r requirements.txt``. - ## Development Setup * create a new directory that should contain the files in future, e.g. ``mkdir AKPlanning`` * change into that directory ``cd AKPlanning`` * clone this repository ``git clone URL .`` - ### Automatic Setup 1. execute the setup bash script ``Utils/setup.sh`` - ### Manual Setup 1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.7`` @@ -49,7 +46,6 @@ Python requirements are listed in ``requirements.txt``. They can be installed wi 1. create a priviledged user, credentials are entered interactively on CLI ``python manage.py createsuperuser`` 1. deactivate virtualenv ``deactivate`` - ### Development Server **Do not use this for deployment!** @@ -60,11 +56,10 @@ To start the application for development, in the root directory, 1. start development server ``python manage.py runserver 0:8000`` 1. In your browser, access ``http://127.0.0.1:8000/admin/`` and continue from there. - ## Deployment Setup -This application can be deployed using a web server as any other Django application. -Remember to use a secret key that is not stored in any repository or similar, and disable DEBUG mode (``settings.py``). +This application can be deployed using a web server as any other Django application. Remember to use a secret key that +is not stored in any repository or similar, and disable DEBUG mode (``settings.py``). **Step-by-Step Instructions** @@ -77,9 +72,12 @@ Remember to use a secret key that is not stored in any repository or similar, an 1. activate virtualenv ``source venv/bin/activate`` 1. update tools ``pip install --upgrade setuptools pip wheel`` 1. install python requirements ``pip install -r requirements.txt`` -1. create the file ``AKPlanning/settings_secrets.py`` (copy from ``settings_secrets.py.sample``) and fill it with the necessary secrets (e.g. generated by ``tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50``) (it is a good idea to restrict read permissions from others) +1. create the file ``AKPlanning/settings_secrets.py`` (copy from ``settings_secrets.py.sample``) and fill it with the + necessary secrets (e.g. generated by ``tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50``) (it is a good idea + to restrict read permissions from others) 1. if necessary enable uwsgi proxy plugin for Apache e.g.``a2enmod proxy_uwsgi`` -1. edit the apache config to serve the application and the static files, e.g. on a dedicated system in ``/etc/apache2/sites-enabled/000-default.conf`` within the ``VirtualHost`` tag add: +1. edit the apache config to serve the application and the static files, e.g. on a dedicated system + in ``/etc/apache2/sites-enabled/000-default.conf`` within the ``VirtualHost`` tag add: ``` Alias /static /srv/AKPlanning/static @@ -91,19 +89,25 @@ Remember to use a secret key that is not stored in any repository or similar, an ProxyPass / uwsgi://127.0.0.1:3035/ ``` - or create a new config (.conf) file (similar to ``apache-akplanning.conf``) replacing $SUBDOMAIN with the subdomain the system should be available under, and $MAILADDRESS with the e-mail address of your administrator and $PATHTO with the appropriate paths. Copy or symlink it to ``/etc/apache2/sites-available``. Then symlink it to ``sites-enabled`` e.g. by using ``ln -s /etc/apache2/sites-available/akplanning.conf /etc/apache2/sites-enabled/akplanning.conf``. + or create a new config (.conf) file (similar to ``apache-akplanning.conf``) replacing $SUBDOMAIN with the subdomain + the system should be available under, and $MAILADDRESS with the e-mail address of your administrator and $PATHTO with + the appropriate paths. Copy or symlink it to ``/etc/apache2/sites-available``. Then symlink it to ``sites-enabled`` + e.g. by using ``ln -s /etc/apache2/sites-available/akplanning.conf /etc/apache2/sites-enabled/akplanning.conf``. 1. restart Apache ``sudo systemctl restart apache2.service`` 1. create a dedicated user, e.g. ``adduser django`` 1. transfer ownership of the folder to the new user ``chown -R django:django /srv/AKPlanning`` -1. Copy or symlink the uwsgi config in ``uwsgi-akplanning.ini`` to ``/etc/uwsgi/apps-available/`` and then symlink it to ``/etc/uwsgi/apps-enabled/`` using e.g., ``ln -s /srv/AKPlanning/uwsgi-akplanning.ini /etc/uwsgi/apps-available/akplanning.ini`` and ``ln -s /etc/uwsgi/apps-available/akplanning.ini /etc/uwsgi/apps-enabled/akplanning.ini`` -start uwsgi using the configuration file ``uwsgi --ini uwsgi-akplanning.ini`` +1. Copy or symlink the uwsgi config in ``uwsgi-akplanning.ini`` to ``/etc/uwsgi/apps-available/`` and then symlink it + to ``/etc/uwsgi/apps-enabled/`` using + e.g., ``ln -s /srv/AKPlanning/uwsgi-akplanning.ini /etc/uwsgi/apps-available/akplanning.ini`` + and ``ln -s /etc/uwsgi/apps-available/akplanning.ini /etc/uwsgi/apps-enabled/akplanning.ini`` + start uwsgi using the configuration file ``uwsgi --ini uwsgi-akplanning.ini`` 1. restart uwsgi ``sudo systemctl restart uwsgi`` 1. execute the update script ``./Utils/update.sh --prod`` - ## Updates -To update the setup to the current version on the main branch of the repository use the update script ``Utils/update.sh`` or ``Utils/update.sh --prod`` in production. +To update the setup to the current version on the main branch of the repository use the update +script ``Utils/update.sh`` or ``Utils/update.sh --prod`` in production. Afterwards, you may check your setup by executing ``Utils/check.sh`` or ``Utils/check.sh --prod`` in production.