Skip to content
Snippets Groups Projects
Commit 868a5198 authored by Nadja Geisler's avatar Nadja Geisler :sunny:
Browse files

Merge branch 'scheduling-constraints' into 'main'

Constraint Violation checking & visualization

See merge request !99
parents 8d36e628 cef8782a
No related branches found
No related tags found
1 merge request!99Constraint Violation checking & visualization
Pipeline #22755 passed
...@@ -4,7 +4,7 @@ from django.contrib import admin ...@@ -4,7 +4,7 @@ from django.contrib import admin
from django.contrib.admin import SimpleListFilter from django.contrib.admin import SimpleListFilter
from django.db.models import Count, F from django.db.models import Count, F
from django.shortcuts import render, redirect 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 import timezone
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
...@@ -16,11 +16,7 @@ from AKModel.availability.forms import AvailabilitiesFormMixin ...@@ -16,11 +16,7 @@ from AKModel.availability.forms import AvailabilitiesFormMixin
from AKModel.availability.models import Availability from AKModel.availability.models import Availability
from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKTag, AKRequirement, AK, AKSlot, Room, AKOrgaMessage, \ from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKTag, AKRequirement, AK, AKSlot, Room, AKOrgaMessage, \
ConstraintViolation ConstraintViolation
from AKModel.views import EventStatusView, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, \ from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event
AKRequirementOverview, \
NewEventWizardStartView, NewEventWizardSettingsView, NewEventWizardPrepareImportView, NewEventWizardFinishView, \
NewEventWizardImportView, NewEventWizardActivateView
from AKModel.views import export_slides
@admin.register(Event) @admin.register(Event)
...@@ -32,42 +28,24 @@ class EventAdmin(admin.ModelAdmin): ...@@ -32,42 +28,24 @@ class EventAdmin(admin.ModelAdmin):
ordering = ['-start'] ordering = ['-start']
def add_view(self, request, form_url='', extra_context=None): def add_view(self, request, form_url='', extra_context=None):
# Always use wizard to create new events # Always use wizard to create new events (the built-in form wouldn't work anyways since the timezone cannot
# (the built-in form wouldn't work anyways since the timezone cannot be specified before starting to fill the form) # be specified before starting to fill the form)
return redirect("admin:new_event_wizard_start") return redirect("admin:new_event_wizard_start")
def get_urls(self): def get_urls(self):
urls = super().get_urls() urls = get_admin_urls_event_wizard(self.admin_site)
custom_urls = [ urls.extend(get_admin_urls_event(self.admin_site))
path('add/wizard/start/', self.admin_site.admin_view(NewEventWizardStartView.as_view()), if apps.is_installed("AKScheduling"):
name="new_event_wizard_start"), from AKScheduling.urls import get_admin_urls_scheduling
path('add/wizard/settings/', self.admin_site.admin_view(NewEventWizardSettingsView.as_view()), urls.extend(get_admin_urls_scheduling(self.admin_site))
name="new_event_wizard_settings"), urls.extend(super().get_urls())
path('add/wizard/created/<slug:event_slug>/', self.admin_site.admin_view(NewEventWizardPrepareImportView.as_view()), return urls
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
def status_url(self, obj): def status_url(self, obj):
return format_html("<a href='{url}'>{text}</a>", return format_html("<a href='{url}'>{text}</a>",
url=reverse_lazy('admin:event_status', kwargs={'slug': obj.slug}), text=_("Status")) 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): def get_form(self, request, obj=None, change=False, **kwargs):
# Use timezone of event # Use timezone of event
...@@ -116,18 +94,6 @@ class AKTrackAdmin(admin.ModelAdmin): ...@@ -116,18 +94,6 @@ class AKTrackAdmin(admin.ModelAdmin):
kwargs['initial'] = Event.get_next_active() kwargs['initial'] = Event.get_next_active()
return super(AKTrackAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) 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) @admin.register(AKTag)
class AKTagAdmin(admin.ModelAdmin): class AKTagAdmin(admin.ModelAdmin):
...@@ -240,7 +206,6 @@ class RoomForm(AvailabilitiesFormMixin, forms.ModelForm): ...@@ -240,7 +206,6 @@ class RoomForm(AvailabilitiesFormMixin, forms.ModelForm):
self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event) self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event)
@admin.register(Room) @admin.register(Room)
class RoomAdmin(admin.ModelAdmin): class RoomAdmin(admin.ModelAdmin):
model = Room model = Room
...@@ -281,20 +246,6 @@ class AKSlotAdmin(admin.ModelAdmin): ...@@ -281,20 +246,6 @@ class AKSlotAdmin(admin.ModelAdmin):
readonly_fields = ['ak_details_link', 'updated'] readonly_fields = ['ak_details_link', 'updated']
form = AKSlotAdminForm 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): def get_form(self, request, obj=None, change=False, **kwargs):
# Use timezone of associated event # Use timezone of associated event
if obj is not None and obj.event.timezone: if obj is not None and obj.event.timezone:
...@@ -310,10 +261,11 @@ class AKSlotAdmin(admin.ModelAdmin): ...@@ -310,10 +261,11 @@ class AKSlotAdmin(admin.ModelAdmin):
return super(AKSlotAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) return super(AKSlotAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
def ak_details_link(self, akslot): 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>" link = f"<a href={reverse('submit:ak_detail', args=[akslot.event.slug, akslot.ak.pk])}>{str(akslot.ak)}</a>"
return mark_safe(link) return mark_safe(link)
return "-" return "-"
ak_details_link.short_description = _('AK Details') ak_details_link.short_description = _('AK Details')
......
...@@ -7,6 +7,7 @@ from django_tex.environment import environment ...@@ -7,6 +7,7 @@ from django_tex.environment import environment
# and would hence cause compilation errors # and would hence cause compilation errors
utf8_replace_pattern = re.compile(u'[^\u0000-\u206F]', re.UNICODE) utf8_replace_pattern = re.compile(u'[^\u0000-\u206F]', re.UNICODE)
def latex_escape_utf8(value): def latex_escape_utf8(value):
""" """
Escape latex special chars and remove invalid utf-8 values Escape latex special chars and remove invalid utf-8 values
...@@ -16,7 +17,10 @@ def latex_escape_utf8(value): ...@@ -16,7 +17,10 @@ def latex_escape_utf8(value):
:return: escaped string :return: escaped string
:rtype: str :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): def improved_tex_environment(**options):
env = environment(**options) env = environment(**options)
......
...@@ -360,7 +360,8 @@ class AKSlot(models.Model): ...@@ -360,7 +360,8 @@ class AKSlot(models.Model):
duration = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Duration'), duration = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Duration'),
help_text=_('Length in hours')) 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'), event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event')) help_text=_('Associated event'))
...@@ -423,6 +424,9 @@ class AKSlot(models.Model): ...@@ -423,6 +424,9 @@ class AKSlot(models.Model):
""" """
return (timezone.now() - self.updated).total_seconds() 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): class AKOrgaMessage(models.Model):
ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'), ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'),
...@@ -494,7 +498,12 @@ class ConstraintViolation(models.Model): ...@@ -494,7 +498,12 @@ class ConstraintViolation(models.Model):
help_text=_('Mark this violation manually as resolved')) help_text=_('Mark this violation manually as resolved'))
fields = ['ak_owner', 'room', 'requirement', 'category'] 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): def get_details(self):
""" """
...@@ -502,10 +511,10 @@ class ConstraintViolation(models.Model): ...@@ -502,10 +511,10 @@ class ConstraintViolation(models.Model):
:return: string of details :return: string of details
:rtype: str :rtype: str
""" """
output = [] # Stringify aks and ak slots fields (m2m)
# Stringify all ManyToMany fields output = [f"{_('AKs')}: {self._aks_str}",
for field_mm in self.fields_mm: f"{_('AK Slots')}: {self._ak_slots_str}"]
output.append(f"{field_mm}: {', '.join(str(a) for a in getattr(self, field_mm).all())}")
# Stringify all other fields # Stringify all other fields
for field in self.fields: for field in self.fields:
a = getattr(self, field, None) a = getattr(self, field, None)
...@@ -515,5 +524,102 @@ class ConstraintViolation(models.Model): ...@@ -515,5 +524,102 @@ class ConstraintViolation(models.Model):
get_details.short_description = _('Details') 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): def __str__(self):
return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]" 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
...@@ -74,6 +74,10 @@ ...@@ -74,6 +74,10 @@
<a class="btn btn-success" <a class="btn btn-success"
href="{% url 'admin:schedule' event_slug=event.slug %}">{% trans "Scheduling" %}</a> 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" <a class="btn btn-success"
href="{% url 'admin:tracks_manage' event_slug=event.slug %}">{% trans "Manage ak tracks" %}</a> href="{% url 'admin:tracks_manage' event_slug=event.slug %}">{% trans "Manage ak tracks" %}</a>
<a class="btn btn-success" <a class="btn btn-success"
......
...@@ -3,6 +3,9 @@ from django.urls import include, path ...@@ -3,6 +3,9 @@ from django.urls import include, path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from AKModel import views 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 = DefaultRouter()
api_router.register('akowner', views.AKOwnerViewSet, basename='AKOwner') api_router.register('akowner', views.AKOwnerViewSet, basename='AKOwner')
...@@ -12,29 +15,29 @@ api_router.register('ak', views.AKViewSet, basename='AK') ...@@ -12,29 +15,29 @@ api_router.register('ak', views.AKViewSet, basename='AK')
api_router.register('room', views.RoomViewSet, basename='Room') api_router.register('room', views.RoomViewSet, basename='Room')
api_router.register('akslot', views.AKSlotViewSet, basename='AKSlot') api_router.register('akslot', views.AKSlotViewSet, basename='AKSlot')
extra_paths = [] extra_paths = []
if apps.is_installed("AKScheduling"): 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-resources', ResourcesViewSet, basename='scheduling-resources')
api_router.register('scheduling-event', EventsViewSet, basename='scheduling-event') api_router.register('scheduling-event', EventsViewSet, basename='scheduling-event')
api_router.register('scheduling-constraint-violations', ConstraintViolationsViewSet,
basename='scheduling-constraint-violations')
extra_paths = [ extra_paths = [
path('api/scheduling-events/', EventsView.as_view(), name='scheduling-events'), 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 = [ 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) event_specific_paths.extend(extra_paths)
app_name = 'model' app_name = 'model'
urlpatterns = [ urlpatterns = [
path( path(
'<slug:event_slug>/', '<slug:event_slug>/',
...@@ -42,3 +45,39 @@ urlpatterns = [ ...@@ -42,3 +45,39 @@ urlpatterns = [
), ),
path('user/', views.UserView.as_view(), name="user"), 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"),
]
...@@ -7,8 +7,8 @@ from django.shortcuts import get_object_or_404, redirect ...@@ -7,8 +7,8 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView, DetailView, ListView, DeleteView, CreateView, FormView, UpdateView 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 django_tex.shortcuts import render_to_pdf
from rest_framework import viewsets, permissions, mixins
from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \ from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \
NewEventWizardImportForm, NewEventWizardActivateForm NewEventWizardImportForm, NewEventWizardActivateForm
......
...@@ -17,7 +17,7 @@ DATABASES = { ...@@ -17,7 +17,7 @@ DATABASES = {
'OPTIONS': { 'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'" 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
}, },
'TEST' : { 'TEST': {
'NAME': 'test', 'NAME': 'test',
}, },
} }
......
...@@ -7,7 +7,7 @@ from django.views.generic import ListView ...@@ -7,7 +7,7 @@ from django.views.generic import ListView
from rest_framework import viewsets, mixins, serializers, permissions from rest_framework import viewsets, mixins, serializers, permissions
from AKModel.availability.models import Availability from AKModel.availability.models import Availability
from AKModel.models import Room, AKSlot from AKModel.models import Room, AKSlot, ConstraintViolation
from AKModel.views import EventSlugMixin from AKModel.views import EventSlugMixin
...@@ -109,3 +109,20 @@ class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet): ...@@ -109,3 +109,20 @@ class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet):
def get_queryset(self): def get_queryset(self):
return AKSlot.objects.filter(event=self.event) 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)
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -17,49 +17,92 @@ msgstr "" ...@@ -17,49 +17,92 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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/manage_tracks.html:11
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:10 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:10
msgid "Scheduling for" msgid "Scheduling for"
msgstr "Scheduling für" 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" msgid "Name of new ak track"
msgstr "Name des neuen AK-Tracks" 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" msgid "Could not create ak track"
msgstr "Konnte neuen AK-Track nicht anlegen" 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" msgid "Could not update ak track name"
msgstr "Konnte Namen des AK-Tracks nicht ändern" 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?" msgid "Do you really want to delete this ak track?"
msgstr "Soll dieser AK-Track wirklich gelöscht werden?" 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" msgid "Could not delete ak track"
msgstr "AK-Track konnte nicht gelöscht werden" 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" msgid "Manage AK Tracks"
msgstr "AK-Tracks verwalten" msgstr "AK-Tracks verwalten"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:201 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:204
msgid "Add ak track" msgid "Add ak track"
msgstr "AK-Track hinzufügen" 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" msgid "AKs without track"
msgstr "AKs ohne 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 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:87
msgid "Day (Horizontal)" msgid "Day (Horizontal)"
msgstr "Tag (horizontal)" msgstr "Tag (horizontal)"
......
This diff is collapsed.
{% 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>
&middot;
<a href="{% url 'admin:schedule' event.slug %}">{% trans "Scheduling" %}</a>
{% endblock %}
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"),
]
from django.views.generic import ListView
from django.utils.translation import gettext_lazy as _ 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 from AKModel.views import AdminViewMixin, FilterByEventSlugMixin
...@@ -47,3 +47,14 @@ class TrackAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView): ...@@ -47,3 +47,14 @@ class TrackAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
context = super().get_context_data(object_list=object_list, **kwargs) context = super().get_context_data(object_list=object_list, **kwargs)
context["aks_without_track"] = self.event.ak_set.filter(track=None) context["aks_without_track"] = self.event.ak_set.filter(track=None)
return context 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
...@@ -2,42 +2,39 @@ ...@@ -2,42 +2,39 @@
This repository contains a Django project with several apps. This repository contains a Django project with several apps.
## Requirements ## 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 ### System Requirements
* Python 3.7 incl. development tools * Python 3.7 incl. development tools
* Virtualenv * 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: * for production using uwsgi:
* C compiler e.g. gcc * C compiler e.g. gcc
* uwsgi * uwsgi
* uwsgi Python3 plugin * uwsgi Python3 plugin
* for production using Apache (in addition to uwsgi) * 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
Python requirements are listed in ``requirements.txt``. They can be installed with pip using ``-r requirements.txt``. Python requirements are listed in ``requirements.txt``. They can be installed with pip using ``-r requirements.txt``.
## Development Setup ## Development Setup
* create a new directory that should contain the files in future, e.g. ``mkdir AKPlanning`` * create a new directory that should contain the files in future, e.g. ``mkdir AKPlanning``
* change into that directory ``cd AKPlanning`` * change into that directory ``cd AKPlanning``
* clone this repository ``git clone URL .`` * clone this repository ``git clone URL .``
### Automatic Setup ### Automatic Setup
1. execute the setup bash script ``Utils/setup.sh`` 1. execute the setup bash script ``Utils/setup.sh``
### Manual Setup ### Manual Setup
1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.7`` 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 ...@@ -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. create a priviledged user, credentials are entered interactively on CLI ``python manage.py createsuperuser``
1. deactivate virtualenv ``deactivate`` 1. deactivate virtualenv ``deactivate``
### Development Server ### Development Server
**Do not use this for deployment!** **Do not use this for deployment!**
...@@ -60,11 +56,10 @@ To start the application for development, in the root directory, ...@@ -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. 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. 1. In your browser, access ``http://127.0.0.1:8000/admin/`` and continue from there.
## Deployment Setup ## Deployment Setup
This application can be deployed using a web server as any other Django application. This application can be deployed using a web server as any other Django application. Remember to use a secret key that
Remember to use a secret key that is not stored in any repository or similar, and disable DEBUG mode (``settings.py``). is not stored in any repository or similar, and disable DEBUG mode (``settings.py``).
**Step-by-Step Instructions** **Step-by-Step Instructions**
...@@ -77,9 +72,12 @@ Remember to use a secret key that is not stored in any repository or similar, an ...@@ -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. activate virtualenv ``source venv/bin/activate``
1. update tools ``pip install --upgrade setuptools pip wheel`` 1. update tools ``pip install --upgrade setuptools pip wheel``
1. install python requirements ``pip install -r requirements.txt`` 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. 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 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 ...@@ -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/ 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. restart Apache ``sudo systemctl restart apache2.service``
1. create a dedicated user, e.g. ``adduser django`` 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. 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`` 1. Copy or symlink the uwsgi config in ``uwsgi-akplanning.ini`` to ``/etc/uwsgi/apps-available/`` and then symlink it
start uwsgi using the configuration file ``uwsgi --ini uwsgi-akplanning.ini`` 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. restart uwsgi ``sudo systemctl restart uwsgi``
1. execute the update script ``./Utils/update.sh --prod`` 1. execute the update script ``./Utils/update.sh --prod``
## Updates ## 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. Afterwards, you may check your setup by executing ``Utils/check.sh`` or ``Utils/check.sh --prod`` in production.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment