Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • konstantin/akplanning
  • matedealer/akplanning
  • kif/akplanning
  • mirco/akplanning
  • lordofthevoid/akplanning
  • voidptr/akplanning
  • xayomer/akplanning-fork
  • mollux/akplanning
  • neumantm/akplanning
  • mmarx/akplanning
  • nerf/akplanning
  • felix_bonn/akplanning
  • sebastian.uschmann/akplanning
13 results
Show changes
Showing
with 537 additions and 219 deletions
{% extends "AKPlan/plan_base.html" %} {% extends "AKPlan/plan_base.html" %}
{% load fontawesome_5 %} {% load fontawesome_6 %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load tz %} {% load tz %}
...@@ -22,7 +22,11 @@ ...@@ -22,7 +22,11 @@
center: 'title', center: 'title',
right: 'resourceTimelineDay,resourceTimelineEvent' right: 'resourceTimelineDay,resourceTimelineEvent'
}, },
themeSystem: 'bootstrap', themeSystem: 'bootstrap5',
buttonIcons: {
prev: 'ignore fa-solid fa-angle-left',
next: 'ignore fa-solid fa-angle-right',
},
// Adapt to user selected locale // Adapt to user selected locale
locale: '{{ LANGUAGE_CODE }}', locale: '{{ LANGUAGE_CODE }}',
initialView: 'resourceTimelineEvent', initialView: 'resourceTimelineEvent',
...@@ -79,11 +83,11 @@ ...@@ -79,11 +83,11 @@
{% block content %} {% block content %}
<div class="float-right"> <div class="float-end">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
{% if rooms|length > 0 %} {% if rooms|length > 0 %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button"
aria-haspopup="true" aria-haspopup="true"
aria-expanded="false">{% trans "Rooms" %}</a> aria-expanded="false">{% trans "Rooms" %}</a>
<div class="dropdown-menu" style=""> <div class="dropdown-menu" style="">
...@@ -96,7 +100,7 @@ ...@@ -96,7 +100,7 @@
{% endif %} {% endif %}
{% if tracks|length > 0 %} {% if tracks|length > 0 %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button"
aria-haspopup="true" aria-haspopup="true"
aria-expanded="false">{% trans "Tracks" %}</a> aria-expanded="false">{% trans "Tracks" %}</a>
<div class="dropdown-menu"> <div class="dropdown-menu">
...@@ -110,7 +114,7 @@ ...@@ -110,7 +114,7 @@
{% if event.active %} {% if event.active %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" <a class="nav-link active"
href="{% url 'plan:plan_wall' event_slug=event.slug %}">{% fa5_icon 'desktop' 'fas' %}&nbsp;&nbsp;{% trans "AK Wall" %}</a> href="{% url 'plan:plan_wall' event_slug=event.slug %}">{% fa6_icon 'desktop' 'fas' %}&nbsp;&nbsp;{% trans "AK Wall" %}</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
......
{% extends "AKPlan/plan_detail.html" %} {% extends "AKPlan/plan_detail.html" %}
{% load fontawesome_5 %} {% load fontawesome_6 %}
{% load tags_AKModel %} {% load tags_AKModel %}
{% load tz %} {% load tz %}
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
{'title': '{{ slot.ak }}', {'title': '{{ slot.ak }}',
'start': '{{ slot.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}', 'start': '{{ slot.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
'end': '{{ slot.end | timezone:event.timezone | date:"Y-m-d H:i:s" }}', 'end': '{{ slot.end | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
'url': '{% url 'submit:ak_detail' event_slug=event.slug pk=slot.ak.pk %}', 'url': '{{ slot.ak.detail_url }}',
'borderColor': '{{ slot.ak.track.color }}', 'borderColor': '{{ slot.ak.track.color }}',
'color': '{{ slot.ak.category.color }}', 'color': '{{ slot.ak.category.color }}',
}, },
...@@ -43,10 +43,10 @@ ...@@ -43,10 +43,10 @@
{% block content %} {% block content %}
<div class="float-right"> <div class="float-end">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "Rooms" %}</a> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "Rooms" %}</a>
<div class="dropdown-menu" style=""> <div class="dropdown-menu" style="">
{% for r in event.room_set.all %} {% for r in event.room_set.all %}
<a class="dropdown-item" href="{% url "plan:plan_room" event_slug=event.slug pk=r.pk %}">{{ r }}</a> <a class="dropdown-item" href="{% url "plan:plan_room" event_slug=event.slug pk=r.pk %}">{{ r }}</a>
...@@ -58,9 +58,9 @@ ...@@ -58,9 +58,9 @@
<h1>{% trans "Room" %}: {{ room.name }} {% if room.location != '' %}({{ room.location }}){% endif %}</h1> <h1>{% trans "Room" %}: {{ room.name }} {% if room.location != '' %}({{ room.location }}){% endif %}</h1>
{% if "AKOnline"|check_app_installed and room.virtualroom and room.virtualroom.url != '' %} {% if "AKOnline"|check_app_installed and room.virtual and room.virtual.url != '' %}
<a class="btn btn-success" href="{{ room.virtualroom.url }}"> <a class="btn btn-success" target="_parent" href="{{ room.virtual.url }}">
{% fa5_icon 'external-link-alt' 'fas' %} {% trans "Go to virtual room" %} {% fa6_icon 'external-link-alt' 'fas' %} {% trans "Go to virtual room" %}
</a> </a>
{% endif %} {% endif %}
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
{'title': '{{ slot.ak }} @ {{ slot.room }}', {'title': '{{ slot.ak }} @ {{ slot.room }}',
'start': '{{ slot.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}', 'start': '{{ slot.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
'end': '{{ slot.end | timezone:event.timezone | date:"Y-m-d H:i:s" }}', 'end': '{{ slot.end | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
'url': '{% url 'submit:ak_detail' event_slug=event.slug pk=slot.ak.pk %}', 'url': '{{ slot.ak.detail_url }}',
'color': '{{ track.color }}', 'color': '{{ track.color }}',
'borderColor': '{{ slot.ak.category.color }}', 'borderColor': '{{ slot.ak.category.color }}',
}, },
...@@ -30,10 +30,10 @@ ...@@ -30,10 +30,10 @@
{% block content %} {% block content %}
<div class="float-right"> <div class="float-end">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "Tracks" %}</a> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "Tracks" %}</a>
<div class="dropdown-menu"> <div class="dropdown-menu">
{% for t in event.aktrack_set.all %} {% for t in event.aktrack_set.all %}
<a class="dropdown-item" href="{% url "plan:plan_track" event_slug=event.slug pk=t.pk %}">{{ t }}</a> <a class="dropdown-item" href="{% url "plan:plan_track" event_slug=event.slug pk=t.pk %}">{{ t }}</a>
......
{% load compress %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load bootstrap4 %} {% load django_bootstrap5 %}
{% load fontawesome_5 %} {% load fontawesome_6 %}
{% load tags_AKModel %} {% load tags_AKModel %}
{% load tags_AKPlan %} {% load tags_AKPlan %}
{% load tz %} {% load tz %}
...@@ -14,11 +15,17 @@ ...@@ -14,11 +15,17 @@
<title>{% block title %}AK Planning{% endblock %}</title> <title>{% block title %}AK Planning{% endblock %}</title>
{# Load Bootstrap CSS and JavaScript as well as font awesome #} {# Load Bootstrap CSS and JavaScript as well as font awesome #}
{% bootstrap_css %} {% compress css %}
{% bootstrap_javascript jquery='slim' %} <link rel="stylesheet" type="text/x-scss" href="{% static 'common/vendor/bootswatch-lumen/theme.scss' %}">
{% fontawesome_5_static %} {% fontawesome_6_css %}
<link rel="stylesheet" href="{% static 'common/css/custom.css' %}">
<link rel="stylesheet" href="{% static 'common/css/custom.css' %}"> {% endcompress %}
{% compress js %}
{% bootstrap_javascript %}
<script src="{% static 'common/vendor/jquery/jquery-3.6.3.min.js' %}"></script>
{% fontawesome_6_js %}
{% endcompress %}
{% include "AKModel/load_fullcalendar.html" %} {% include "AKModel/load_fullcalendar.html" %}
...@@ -31,7 +38,11 @@ ...@@ -31,7 +38,11 @@
var plan = new FullCalendar.Calendar(planEl, { var plan = new FullCalendar.Calendar(planEl, {
timeZone: '{{ event.timezone }}', timeZone: '{{ event.timezone }}',
headerToolbar: false, headerToolbar: false,
themeSystem: 'bootstrap', themeSystem: 'bootstrap5',
buttonIcons: {
prev: 'ignore fa-solid fa-angle-left',
next: 'ignore fa-solid fa-angle-right',
},
// Adapt to user selected locale // Adapt to user selected locale
locale: '{{ LANGUAGE_CODE }}', locale: '{{ LANGUAGE_CODE }}',
slotDuration: '01:00', slotDuration: '01:00',
...@@ -40,6 +51,8 @@ ...@@ -40,6 +51,8 @@
start: '{{ start | timezone:event.timezone | date:"Y-m-d H:i:s" }}', start: '{{ start | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
end: '{{ end | timezone:event.timezone | date:"Y-m-d H:i:s"}}', end: '{{ end | timezone:event.timezone | date:"Y-m-d H:i:s"}}',
}, },
slotMinTime: '{{ earliest_start_hour }}:00:00',
slotMaxTime: '{{ latest_end_hour }}:00:00',
eventDidMount: function(info) { eventDidMount: function(info) {
$(info.el).tooltip({title: info.event.extendedProps.description}); $(info.el).tooltip({title: info.event.extendedProps.description});
}, },
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<table class="table table-striped"> <table class="table table-striped">
{% for akslot in slots %} {% for akslot in slots %}
<tr> <tr>
<td class="breakWord"><b><a href="{% url 'submit:ak_detail' event_slug=event.slug pk=akslot.ak.pk %}">{{ akslot.ak.name }}</a></b></td> <td class="breakWord"><b><a href="{{ akslot.ak.detail_url }}">{{ akslot.ak.name }}</a></b></td>
<td>{{ akslot.start | time:"H:i" }} - {{ akslot.end | time:"H:i" }}</td> <td>{{ akslot.start | time:"H:i" }} - {{ akslot.end | time:"H:i" }}</td>
<td class="breakWord">{% if akslot.room and akslot.room.pk != '' %} <td class="breakWord">{% if akslot.room and akslot.room.pk != '' %}
<a href="{% url 'plan:plan_room' event_slug=event.slug pk=akslot.room.pk %}">{{ akslot.room }}</a> <a href="{% url 'plan:plan_room' event_slug=event.slug pk=akslot.room.pk %}">{{ akslot.room }}</a>
......
# gradients based on http://bsou.io/posts/color-gradients-with-python # gradients based on http://bsou.io/posts/color-gradients-with-python
def hex_to_rgb(hex): def hex_to_rgb(hex): #pylint: disable=redefined-builtin
""" """
Convert hex color to RGB color code Convert hex color to RGB color code
:param hex: hex encoded color :param hex: hex encoded color
...@@ -23,8 +23,7 @@ def rgb_to_hex(rgb): ...@@ -23,8 +23,7 @@ def rgb_to_hex(rgb):
""" """
# Components need to be integers for hex to make sense # Components need to be integers for hex to make sense
rgb = [int(x) for x in rgb] rgb = [int(x) for x in rgb]
return "#"+"".join(["0{0:x}".format(v) if v < 16 else return "#"+"".join([f"0{v:x}" if v < 16 else f"{v:x}" for v in rgb])
"{0:x}".format(v) for v in rgb])
def linear_blend(start_hex, end_hex, position): def linear_blend(start_hex, end_hex, position):
......
...@@ -11,6 +11,14 @@ register = template.Library() ...@@ -11,6 +11,14 @@ register = template.Library()
@register.filter @register.filter
def highlight_change_colors(akslot): def highlight_change_colors(akslot):
"""
Adjust color to highlight recent changes if needed
:param akslot: akslot to determine color for
:type akslot: AKSlot
:return: color that should be used (either default color of the category or some kind of red)
:rtype: str
"""
# Do not highlight in preview mode or when changes occurred before the plan was published # Do not highlight in preview mode or when changes occurred before the plan was published
if akslot.event.plan_hidden or (akslot.event.plan_published_at is not None if akslot.event.plan_hidden or (akslot.event.plan_published_at is not None
and akslot.event.plan_published_at > akslot.updated): and akslot.event.plan_published_at > akslot.updated):
...@@ -25,9 +33,14 @@ def highlight_change_colors(akslot): ...@@ -25,9 +33,14 @@ def highlight_change_colors(akslot):
# Recent change? Calculate gradient blend between red and # Recent change? Calculate gradient blend between red and
recentness = seconds_since_update / settings.PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS recentness = seconds_since_update / settings.PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS
return darken("#b71540", recentness) return darken("#b71540", recentness)
# return linear_blend("#b71540", "#000000", recentness)
@register.simple_tag @register.simple_tag
def timestamp_now(tz): def timestamp_now(tz):
"""
Get the current timestamp for the given timezone
:param tz: timezone to be used for the timestamp
:return: current timestamp in given timezone
"""
return date_format(datetime.now().astimezone(tz), "c") return date_format(datetime.now().astimezone(tz), "c")
# Create your tests here. from django.test import TestCase
from AKModel.tests import BasicViewTests
class PlanViewTests(BasicViewTests, TestCase):
"""
Tests for AKPlan
"""
fixtures = ['model.json']
APP_NAME = 'plan'
VIEWS = [
('plan_overview', {'event_slug': 'kif42'}),
('plan_wall', {'event_slug': 'kif42'}),
('plan_room', {'event_slug': 'kif42', 'pk': 2}),
('plan_track', {'event_slug': 'kif42', 'pk': 1}),
]
def test_plan_hidden(self):
"""
Test correct handling of plan visibility
"""
_, url = self._name_and_url(('plan_overview', {'event_slug': 'kif23'}))
self.client.logout()
response = self.client.get(url)
self.assertContains(response, "Plan is not visible (yet).",
msg_prefix="Plan is visible even though it shouldn't be")
self.client.force_login(self.staff_user)
response = self.client.get(url)
self.assertNotContains(response, "Plan is not visible (yet).",
msg_prefix="Plan is not visible for staff user")
def test_wall_redirect(self):
"""
Test: Make sure that user is redirected from wall to overview when plan is hidden
"""
_, url_wall = self._name_and_url(('plan_wall', {'event_slug': 'kif23'}))
_, url_plan = self._name_and_url(('plan_overview', {'event_slug': 'kif23'}))
response = self.client.get(url_wall)
self.assertRedirects(response, url_plan,
msg_prefix=f"Redirect away from wall not working ({url_wall} -> {url_plan})")
from datetime import timedelta from datetime import datetime, timedelta
from django.conf import settings from django.conf import settings
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.datetime_safe import datetime from django.views.generic import DetailView, ListView
from django.views.generic import ListView, DetailView
from AKModel.models import AKSlot, Room, AKTrack from AKModel.metaviews.admin import FilterByEventSlugMixin
from AKModel.views import FilterByEventSlugMixin from AKModel.models import AKSlot, AKTrack, Room
class PlanIndexView(FilterByEventSlugMixin, ListView): class PlanIndexView(FilterByEventSlugMixin, ListView):
"""
Default plan view
Shows two lists of current and upcoming AKs and a graphical full plan below
"""
model = AKSlot model = AKSlot
template_name = "AKPlan/plan_index.html" template_name = "AKPlan/plan_index.html"
context_object_name = "akslots" context_object_name = "akslots"
...@@ -60,6 +64,15 @@ class PlanIndexView(FilterByEventSlugMixin, ListView): ...@@ -60,6 +64,15 @@ class PlanIndexView(FilterByEventSlugMixin, ListView):
class PlanScreenView(PlanIndexView): class PlanScreenView(PlanIndexView):
"""
Plan view optimized for screens and projectors
This again shows current and upcoming AKs as well as a graphical plan,
but no navigation elements and trys to use the available space as best as possible
such that no scrolling is needed.
The view contains a frontend functionality for auto-reload.
"""
template_name = "AKPlan/plan_wall.html" template_name = "AKPlan/plan_wall.html"
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
...@@ -69,11 +82,12 @@ class PlanScreenView(PlanIndexView): ...@@ -69,11 +82,12 @@ class PlanScreenView(PlanIndexView):
return redirect(reverse_lazy("plan:plan_overview", kwargs={"event_slug": self.event.slug})) return redirect(reverse_lazy("plan:plan_overview", kwargs={"event_slug": self.event.slug}))
return s return s
""" # pylint: disable=attribute-defined-outside-init
def get_queryset(self): def get_queryset(self):
# Determine interesting range (some hours ago until some hours in the future as specified in the settings)
now = datetime.now().astimezone(self.event.timezone) now = datetime.now().astimezone(self.event.timezone)
# Wall during event: Adjust, show only parts in the future
if self.event.start < now < self.event.end: if self.event.start < now < self.event.end:
# Determine interesting range (some hours ago until some hours in the future as specified in the settings)
self.start = now - timedelta(hours=settings.PLAN_WALL_HOURS_RETROSPECT) self.start = now - timedelta(hours=settings.PLAN_WALL_HOURS_RETROSPECT)
else: else:
self.start = self.event.start self.start = self.event.start
...@@ -81,33 +95,63 @@ class PlanScreenView(PlanIndexView): ...@@ -81,33 +95,63 @@ class PlanScreenView(PlanIndexView):
# Restrict AK slots to relevant ones # Restrict AK slots to relevant ones
# This will automatically filter all rooms not needed for the selected range in the orginal get_context method # This will automatically filter all rooms not needed for the selected range in the orginal get_context method
return super().get_queryset().filter(start__gt=self.start) akslots = super().get_queryset().filter(start__gt=self.start)
"""
# Find the earliest hour AKs start and end (handle 00:00 as 24:00)
self.earliest_start_hour = 23
self.latest_end_hour = 1
for akslot in akslots.all():
start_hour = akslot.start.astimezone(self.event.timezone).hour
if start_hour < self.earliest_start_hour:
# Use hour - 1 to improve visibility of date change
self.earliest_start_hour = max(start_hour - 1, 0)
end_hour = akslot.end.astimezone(self.event.timezone).hour
# Special case: AK starts before but ends after midnight -- show until midnight
if end_hour < start_hour:
self.latest_end_hour = 24
elif end_hour > self.latest_end_hour:
# Always use hour + 1, since AK may end at :xy and not always at :00
self.latest_end_hour = min(end_hour + 1, 24)
return akslots
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs) context = super().get_context_data(object_list=object_list, **kwargs)
context["start"] = self.event.start context["start"] = self.start
context["end"] = self.event.end context["end"] = self.event.end
context["earliest_start_hour"] = self.earliest_start_hour
context["latest_end_hour"] = self.latest_end_hour
return context return context
class PlanRoomView(FilterByEventSlugMixin, DetailView): class PlanRoomView(FilterByEventSlugMixin, DetailView):
"""
Plan view for a single room
"""
template_name = "AKPlan/plan_room.html" template_name = "AKPlan/plan_room.html"
model = Room model = Room
context_object_name = "room" context_object_name = "room"
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs) context = super().get_context_data(object_list=object_list, **kwargs)
# Restrict AKSlot list to the given room
# while joining AK, room and category information to reduce the amount of necessary SQL queries
context["slots"] = AKSlot.objects.filter(room=context['room']).select_related('ak', 'ak__category', 'ak__track') context["slots"] = AKSlot.objects.filter(room=context['room']).select_related('ak', 'ak__category', 'ak__track')
return context return context
class PlanTrackView(FilterByEventSlugMixin, DetailView): class PlanTrackView(FilterByEventSlugMixin, DetailView):
"""
Plan view for a single track
"""
template_name = "AKPlan/plan_track.html" template_name = "AKPlan/plan_track.html"
model = AKTrack model = AKTrack
context_object_name = "track" context_object_name = "track"
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs) context = super().get_context_data(object_list=object_list, **kwargs)
context["slots"] = AKSlot.objects.filter(event=self.event, ak__track=context['track']).select_related('ak', 'room', 'ak__category') # Restrict AKSlot list to given track
# while joining AK, room and category information to reduce the amount of necessary SQL queries
context["slots"] = AKSlot.objects. \
filter(event=self.event, ak__track=context['track']). \
select_related('ak', 'room', 'ak__category')
return context return context
...@@ -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-10-29 13:36+0000\n" "POT-Creation-Date: 2023-08-16 16:30+0200\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,10 +17,10 @@ msgstr "" ...@@ -17,10 +17,10 @@ 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"
#: AKPlanning/settings.py:144 #: AKPlanning/settings.py:148
msgid "German" msgid "German"
msgstr "Deutsch" msgstr "Deutsch"
#: AKPlanning/settings.py:145 #: AKPlanning/settings.py:149
msgid "English" msgid "English"
msgstr "Englisch" msgstr "Englisch"
...@@ -45,17 +45,20 @@ INSTALLED_APPS = [ ...@@ -45,17 +45,20 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'debug_toolbar', 'debug_toolbar',
'bootstrap4', 'django_bootstrap5',
'fontawesome_5', 'fontawesomefree',
'fontawesome_6',
'timezone_field', 'timezone_field',
'rest_framework', 'rest_framework',
'simple_history', 'simple_history',
'registration', 'registration',
'bootstrap_datepicker_plus',
'django_tex', 'django_tex',
'compressor',
'docs',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.gzip.GZipMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
...@@ -138,8 +141,6 @@ TIME_ZONE = 'UTC' ...@@ -138,8 +141,6 @@ TIME_ZONE = 'UTC'
USE_I18N = True USE_I18N = True
USE_L10N = True
USE_TZ = True USE_TZ = True
LANGUAGES = [ LANGUAGES = [
...@@ -161,29 +162,38 @@ STATICFILES_DIRS = ( ...@@ -161,29 +162,38 @@ STATICFILES_DIRS = (
'static_common', 'static_common',
) )
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
)
# Settings for Bootstrap # Settings for Bootstrap
BOOTSTRAP4 = { BOOTSTRAP5 = {
# Use custom CSS
"css_url": {
"href": STATIC_URL + "common/css/bootstrap.css",
},
"javascript_url": { "javascript_url": {
"url": STATIC_URL + "common/vendor/bootstrap/bootstrap-4.3.1.min.js", "url": STATIC_URL + "common/vendor/bootstrap/bootstrap-5.0.2.bundle.min.js",
},
"jquery_url": {
"url": STATIC_URL + "common/vendor/jquery/jquery-3.3.1.min.js",
},
"jquery_slim_url": {
"url": STATIC_URL + "common/vendor/jquery/jquery-3.3.1.slim.min.js",
},
"popper_url": {
"url": STATIC_URL + "common/vendor/popper/popper-1.14.7.min.js",
}, },
} }
# Settings for FontAwesome # Settings for FontAwesome
FONTAWESOME_5_CSS_URL = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.css" FONTAWESOME_6_CSS_URL = STATIC_URL + "fontawesomefree/css/all.min.css"
FONTAWESOME_5_PREFIX = "fa" FONTAWESOME_6_PREFIX = "fa"
# Compressor and minifier config
COMPRESS_ENABLED = True
COMPRESS_CSS_HASHING_METHOD = 'content'
COMPRESS_PRECOMPILERS = (
('text/x-scss', 'django_libsass.SassCompiler'),
)
COMPRESS_FILTERS = {
'css': [
'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.cssmin.rCSSMinFilter',
],
'js': [
'compressor.filters.jsmin.JSMinFilter',
]
}
# Treat wishes as seperate category in submission views? # Treat wishes as seperate category in submission views?
WISHES_AS_CATEGORY = True WISHES_AS_CATEGORY = True
...@@ -207,6 +217,9 @@ PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS = 2 * 60 * 60 ...@@ -207,6 +217,9 @@ PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS = 2 * 60 * 60
DASHBOARD_SHOW_RECENT = True DASHBOARD_SHOW_RECENT = True
# How many entries max? # How many entries max?
DASHBOARD_RECENT_MAX = 25 DASHBOARD_RECENT_MAX = 25
# How many events should be featured in the dashboard
# (active events will always be featured, even if their number is higher than this threshold)
DASHBOARD_MAX_FEATURED_EVENTS = 3
# Registration/login behavior # Registration/login behavior
SIMPLE_BACKEND_REDIRECT_URL = "/user/" SIMPLE_BACKEND_REDIRECT_URL = "/user/"
...@@ -214,7 +227,7 @@ LOGIN_REDIRECT_URL = SIMPLE_BACKEND_REDIRECT_URL ...@@ -214,7 +227,7 @@ LOGIN_REDIRECT_URL = SIMPLE_BACKEND_REDIRECT_URL
# Content Security Policy # Content Security Policy
CSP_DEFAULT_SRC = ("'self'",) CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'") CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", "'unsafe-eval'")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "fonts.googleapis.com") CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "fonts.googleapis.com")
CSP_IMG_SRC = ("'self'", "data:") CSP_IMG_SRC = ("'self'", "data:")
CSP_FRAME_SRC = ("'self'", ) CSP_FRAME_SRC = ("'self'", )
...@@ -224,4 +237,9 @@ CSP_FONT_SRC = ("'self'", "data:", "fonts.gstatic.com") ...@@ -224,4 +237,9 @@ CSP_FONT_SRC = ("'self'", "data:", "fonts.gstatic.com")
SEND_MAILS = True SEND_MAILS = True
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Documentation
DOCS_ROOT = os.path.join(BASE_DIR, 'docs/_build/html')
DOCS_ACCESS = 'public'
include(optional("settings/*.py")) include(optional("settings/*.py"))
...@@ -16,10 +16,14 @@ DATABASES = { ...@@ -16,10 +16,14 @@ DATABASES = {
'PASSWORD': 'mysql', 'PASSWORD': 'mysql',
'OPTIONS': { 'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
'charset': "utf8mb4",
}, },
'TEST': { 'TEST': {
'NAME': 'test', 'NAME': 'tests',
'CHARSET': "utf8mb4",
'COLLATION': 'utf8mb4_unicode_ci',
}, },
} }
} }
TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
TEST_OUTPUT_FILE_NAME = 'unit.xml'
"""AKPlanning URL Configuration """
AKPlanning URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see: The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.2/topics/http/urls/ https://docs.djangoproject.com/en/2.2/topics/http/urls/
...@@ -13,13 +14,15 @@ Including another URLconf ...@@ -13,13 +14,15 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
import debug_toolbar import debug_toolbar
from django.apps import apps from django.apps import apps
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include, re_path
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
re_path(r'^docs/', include('docs.urls')),
path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django.contrib.auth.urls')),
path('accounts/', include('registration.backends.simple.urls')), path('accounts/', include('registration.backends.simple.urls')),
path('', include('AKModel.urls', namespace='model')), path('', include('AKModel.urls', namespace='model')),
......
# Register your models here.
...@@ -8,47 +8,69 @@ from rest_framework import viewsets, mixins, serializers, permissions ...@@ -8,47 +8,69 @@ 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, ConstraintViolation, DefaultSlot from AKModel.models import Room, AKSlot, ConstraintViolation, DefaultSlot
from AKModel.views import EventSlugMixin from AKModel.metaviews.admin import EventSlugMixin
class ResourceSerializer(serializers.ModelSerializer): class ResourceSerializer(serializers.ModelSerializer):
"""
REST Framework Serializer for Rooms to produce format required for fullcalendar resources
"""
class Meta: class Meta:
model = Room model = Room
fields = ['id', 'title'] fields = ['id', 'title']
title = serializers.SerializerMethodField('transform_title') title = serializers.SerializerMethodField('transform_title')
def transform_title(self, obj): @staticmethod
def transform_title(obj):
"""
Adapt title, add capacity information if room has a restriction (capacity is not -1)
"""
if obj.capacity > 0: if obj.capacity > 0:
return f"{obj.title} [{obj.capacity}]" return f"{obj.title} [{obj.capacity}]"
return obj.title return obj.title
class ResourcesViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): class ResourcesViewSet(EventSlugMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,) """
API View: Rooms (resources to schedule for in fullcalendar)
Read-only, adaption to fullcalendar format through :class:`ResourceSerializer`
"""
permission_classes = (permissions.DjangoModelPermissions,)
serializer_class = ResourceSerializer serializer_class = ResourceSerializer
def get_queryset(self): def get_queryset(self):
return Room.objects.filter(event=self.event) return Room.objects.filter(event=self.event).order_by('location', 'name')
class EventsView(LoginRequiredMixin, EventSlugMixin, ListView): class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
"""
API View: Slots (events to schedule in fullcalendar)
Read-only, JSON formatted response is created manually since it requires a bunch of "custom" fields that have
different names compared to the normal model or are not present at all and need to be computed to create the
required format for fullcalendar.
"""
model = AKSlot model = AKSlot
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter(event=self.event, room__isnull=False) return super().get_queryset().select_related('ak').filter(event=self.event, room__isnull=False)
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
return JsonResponse( return JsonResponse(
[{ [{
"slotID": slot.pk, "slotID": slot.pk,
"title": f'{slot.ak.short_name}: \n{slot.ak.owners_list}', "title": f'{slot.ak.short_name}:\n{slot.ak.owners_list}',
"description": slot.ak.details, "description": slot.ak.details,
"resourceId": slot.room.id, "resourceId": slot.room.id,
"start": timezone.localtime(slot.start, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"), "start": timezone.localtime(slot.start, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"),
"end": timezone.localtime(slot.end, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"), "end": timezone.localtime(slot.end, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"),
"backgroundColor": slot.ak.category.color, "backgroundColor": slot.ak.category.color,
"borderColor": "#2c3e50" if slot.fixed else '#e74c3c' if slot.constraintviolation_set.count() > 0 else slot.ak.category.color, "borderColor":
"#2c3e50" if slot.fixed
else '#e74c3c' if slot.constraintviolation_set.count() > 0
else slot.ak.category.color,
"constraint": 'roomAvailable', "constraint": 'roomAvailable',
"editable": not slot.fixed, "editable": not slot.fixed,
'url': str(reverse('admin:AKModel_akslot_change', args=[slot.pk])), 'url': str(reverse('admin:AKModel_akslot_change', args=[slot.pk])),
...@@ -59,6 +81,13 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView): ...@@ -59,6 +81,13 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView): class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView):
"""
API view: Availabilities of rooms
Read-only, JSON formatted response is created manually since it requires a bunch of "custom" fields that have
different names compared to the normal model or are not present at all and need to be computed to create the
required format for fullcalendar.
"""
model = Availability model = Availability
context_object_name = "availabilities" context_object_name = "availabilities"
...@@ -81,6 +110,13 @@ class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView): ...@@ -81,6 +110,13 @@ class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView):
class DefaultSlotsView(LoginRequiredMixin, EventSlugMixin, ListView): class DefaultSlotsView(LoginRequiredMixin, EventSlugMixin, ListView):
"""
API view: default slots
Read-only, JSON formatted response is created manually since it requires a bunch of "custom" fields that have
different names compared to the normal model or are not present at all and need to be computed to create the
required format for fullcalendar.
"""
model = DefaultSlot model = DefaultSlot
context_object_name = "default_slots" context_object_name = "default_slots"
...@@ -105,6 +141,9 @@ class DefaultSlotsView(LoginRequiredMixin, EventSlugMixin, ListView): ...@@ -105,6 +141,9 @@ class DefaultSlotsView(LoginRequiredMixin, EventSlugMixin, ListView):
class EventSerializer(serializers.ModelSerializer): class EventSerializer(serializers.ModelSerializer):
"""
REST framework serializer to adapt between AKSlot model and the event format of fullcalendar
"""
class Meta: class Meta:
model = AKSlot model = AKSlot
fields = ['id', 'start', 'end', 'roomId'] fields = ['id', 'start', 'end', 'roomId']
...@@ -114,17 +153,31 @@ class EventSerializer(serializers.ModelSerializer): ...@@ -114,17 +153,31 @@ class EventSerializer(serializers.ModelSerializer):
roomId = serializers.IntegerField(source='room.pk') roomId = serializers.IntegerField(source='room.pk')
def update(self, instance, validated_data): def update(self, instance, validated_data):
# Ignore timezone of input (treat it as timezone-less) and set the event timezone
# By working like this, the client does not need to know about timezones, since every timestamp it deals with
# has the timezone offsets already applied
start = timezone.make_aware(timezone.make_naive(validated_data.get('start')), instance.event.timezone) start = timezone.make_aware(timezone.make_naive(validated_data.get('start')), instance.event.timezone)
end = timezone.make_aware(timezone.make_naive(validated_data.get('end')), instance.event.timezone) end = timezone.make_aware(timezone.make_naive(validated_data.get('end')), instance.event.timezone)
instance.start = start instance.start = start
instance.room = get_object_or_404(Room, pk=validated_data.get('room')["pk"]) # Also, adapt from start & end format of fullcalendar to our start & duration model
diff = end - start diff = end - start
instance.duration = round(diff.days * 24 + (diff.seconds / 3600), 2) instance.duration = round(diff.days * 24 + (diff.seconds / 3600), 2)
# Updated room if needed (pk changed -- otherwise, no need for an additional database lookup)
new_room_id = validated_data.get('room')["pk"]
if instance.room is None or instance.room.pk != new_room_id:
instance.room = get_object_or_404(Room, pk=new_room_id)
instance.save() instance.save()
return instance return instance
class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet): class EventsViewSet(EventSlugMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
"""
API view: Update scheduling of a slot (event in fullcalendar format)
Write-only (will however reply with written values to PUT request)
"""
permission_classes = (permissions.DjangoModelPermissions,) permission_classes = (permissions.DjangoModelPermissions,)
serializer_class = EventSerializer serializer_class = EventSerializer
...@@ -136,17 +189,26 @@ class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet): ...@@ -136,17 +189,26 @@ class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet):
class ConstraintViolationSerializer(serializers.ModelSerializer): class ConstraintViolationSerializer(serializers.ModelSerializer):
"""
REST Framework Serializer for constraint violations
"""
class Meta: class Meta:
model = ConstraintViolation model = ConstraintViolation
fields = ['pk', 'type_display', 'aks', 'ak_slots', 'ak_owner', 'room', 'requirement', 'category', 'comment', 'timestamp_display', 'manually_resolved', 'level_display', 'details', 'edit_url'] fields = ['pk', 'type_display', 'aks', 'ak_slots', 'ak_owner', 'room', 'requirement', 'category', 'comment',
'timestamp_display', 'manually_resolved', 'level_display', 'details', 'edit_url']
class ConstraintViolationsViewSet(EventSlugMixin, viewsets.ModelViewSet): class ConstraintViolationsViewSet(EventSlugMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
API View: Constraint Violations of an event
Read-only, fields and model selected in :class:`ConstraintViolationSerializer`
"""
permission_classes = (permissions.DjangoModelPermissions,) permission_classes = (permissions.DjangoModelPermissions,)
serializer_class = ConstraintViolationSerializer serializer_class = ConstraintViolationSerializer
def get_object(self):
return get_object_or_404(ConstraintViolation, pk=self.kwargs["pk"])
def get_queryset(self): def get_queryset(self):
return ConstraintViolation.objects.filter(event=self.event).order_by('manually_resolved', '-type', '-timestamp') # Optimize query to reduce database load
return (ConstraintViolation.objects.select_related('event', 'room')
.prefetch_related('aks', 'ak_slots', 'ak_owner', 'requirement', 'category')
.filter(event=self.event).order_by('manually_resolved', '-type', '-timestamp'))
...@@ -2,4 +2,7 @@ from django.apps import AppConfig ...@@ -2,4 +2,7 @@ from django.apps import AppConfig
class AkschedulingConfig(AppConfig): class AkschedulingConfig(AppConfig):
"""
App configuration (default, only specifies name of the app)
"""
name = 'AKScheduling' name = 'AKScheduling'
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _
from AKModel.models import AK from AKModel.models import AK
class AKInterestForm(forms.ModelForm): class AKInterestForm(forms.ModelForm):
"""
Form for quickly changing the interest count and notes of an AK
"""
required_css_class = 'required' required_css_class = 'required'
class Meta: class Meta:
...@@ -11,3 +15,18 @@ class AKInterestForm(forms.ModelForm): ...@@ -11,3 +15,18 @@ class AKInterestForm(forms.ModelForm):
fields = ['interest', fields = ['interest',
'notes', 'notes',
] ]
class AKAddSlotForm(forms.Form):
"""
Form to create a new slot for an existing AK directly from scheduling view
"""
start = forms.CharField(label=_("Start"), disabled=True)
end = forms.CharField(label=_("End"), disabled=True)
duration = forms.CharField(label=_("Duration"), disabled=True)
room = forms.IntegerField(label=_("Room"), disabled=True, widget=forms.HiddenInput())
room_name = forms.CharField(label=_("Room"), disabled=True)
def __init__(self, event):
super().__init__()
self.fields['ak'] = forms.ModelChoiceField(event.ak_set.all(), label=_("AK"))
...@@ -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: 2022-11-29 00:13+0100\n" "POT-Creation-Date: 2024-04-25 00:24+0200\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,7 +17,28 @@ msgstr "" ...@@ -17,7 +17,28 @@ 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/models.py:80 #: AKScheduling/forms.py:24
msgid "Start"
msgstr "Start"
#: AKScheduling/forms.py:25
msgid "End"
msgstr "Ende"
#: AKScheduling/forms.py:26
msgid "Duration"
msgstr ""
#: AKScheduling/forms.py:27
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:171
msgid "Room"
msgstr "Raum"
#: AKScheduling/forms.py:31
msgid "AK"
msgstr "AK"
#: AKScheduling/models.py:92
#, python-format #, python-format
msgid "" msgid ""
"Not enough space for AK interest (Interest: %(interest)d, Capacity: " "Not enough space for AK interest (Interest: %(interest)d, Capacity: "
...@@ -26,7 +47,7 @@ msgstr "" ...@@ -26,7 +47,7 @@ msgstr ""
"Nicht genug Platz für AK-Interesse (Interesse: %(interest)d, Kapazität: " "Nicht genug Platz für AK-Interesse (Interesse: %(interest)d, Kapazität: "
"%(capacity)d)" "%(capacity)d)"
#: AKScheduling/models.py:92 #: AKScheduling/models.py:106
#, python-format #, python-format
msgid "" msgid ""
"Space is too close to AK interest (Interest: %(interest)d, Capacity: " "Space is too close to AK interest (Interest: %(interest)d, Capacity: "
...@@ -40,122 +61,138 @@ msgid "Constraint Violations for" ...@@ -40,122 +61,138 @@ msgid "Constraint Violations for"
msgstr "" msgstr ""
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:44 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:44
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:100 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:105
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:214 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:240
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:293 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:375
msgid "No violations" msgid "No violations"
msgstr "Keine Verletzungen" msgstr "Keine Verletzungen"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:77 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:82
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:264 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:346
msgid "Violation(s)" msgid "Violation(s)"
msgstr "Verletzung(en)" msgstr "Verletzung(en)"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:80 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:85
msgid "Auto reload?" msgid "Auto reload?"
msgstr "Automatisch neu laden?" msgstr "Automatisch neu laden?"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:84 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:89
msgid "Reload now" msgid "Reload now"
msgstr "Jetzt neu laden" msgstr "Jetzt neu laden"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:90 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:95
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:200 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:228
msgid "Violation" msgid "Violation"
msgstr "Verletzung" msgstr "Verletzung"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:91 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:96
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:287 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:369
msgid "Problem" msgid "Problem"
msgstr "Problem" msgstr "Problem"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:92 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:97
msgid "Details" msgid "Details"
msgstr "Details" msgstr "Details"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:93 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:98
msgid "Since" msgid "Since"
msgstr "Seit" msgstr "Seit"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:106 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:111
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:243 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:256
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:250 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:332
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:48 #: AKScheduling/templates/admin/AKScheduling/special_attention.html:48
#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34 #: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34
msgid "Event Status" msgid "Event Status"
msgstr "Event-Status" msgstr "Event-Status"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:108 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:113
msgid "Scheduling" msgid "Scheduling"
msgstr "Scheduling" msgstr "Scheduling"
#: AKScheduling/templates/admin/AKScheduling/interest.html:33 #: AKScheduling/templates/admin/AKScheduling/interest.html:32
msgid "Submit" msgid "Submit"
msgstr "Abschicken" msgstr "Abschicken"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:11 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:11
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:19 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:21
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:247 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:329
msgid "Scheduling for" msgid "Scheduling for"
msgstr "Scheduling für" msgstr "Scheduling für"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:129 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:135
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:145 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:151
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:171 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:177
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:177 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:183
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:191 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:197
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:203 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:216
msgid "Manage AK Tracks" msgid "Manage AK Tracks"
msgstr "AK-Tracks verwalten" msgstr "AK-Tracks verwalten"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:204 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:217
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:209 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:222
msgid "AKs without track" msgid "AKs without track"
msgstr "AKs ohne Track" msgstr "AKs ohne Track"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:92 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:106
msgid "Day (Horizontal)" msgid "Day (Horizontal)"
msgstr "Tag (horizontal)" msgstr "Tag (horizontal)"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:99 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:113
msgid "Day (Vertical)" msgid "Day (Vertical)"
msgstr "Tag (vertikal)" msgstr "Tag (vertikal)"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:110 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:124
msgid "Event (Horizontal)" msgid "Event (Horizontal)"
msgstr "Event (horizontal)" msgstr "Event (horizontal)"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:119 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:133
msgid "Event (Vertical)" msgid "Event (Vertical)"
msgstr "Event (vertikal)" msgstr "Event (vertikal)"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:147 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:271
msgid "Room" msgid "Please choose AK"
msgstr "Raum" msgstr "Bitte AK auswählen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:291
msgid "Could not create slot"
msgstr "Konnte Slot nicht anlegen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:307
msgid "Add slot"
msgstr "Slot hinzufügen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:315
msgid "Add"
msgstr "Hinzufügen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:261 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:316
msgid "Cancel"
msgstr "Abbrechen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:343
msgid "Unscheduled" msgid "Unscheduled"
msgstr "Micht gescheduled" msgstr "Nicht gescheduled"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:286 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:368
msgid "Level" msgid "Level"
msgstr "Level" msgstr "Level"
...@@ -183,6 +220,24 @@ msgstr "" ...@@ -183,6 +220,24 @@ msgstr ""
msgid "AKs without slots" msgid "AKs without slots"
msgstr "AKs ohne Slots" msgstr "AKs ohne Slots"
#: AKScheduling/templates/admin/AKScheduling/status/cvs.html:6
msgid ""
"\n"
" <h3>Constraint Violation</h3>\n"
" "
msgid_plural ""
"\n"
" <h3>Constraint Violations</h3>\n"
" "
msgstr[0] ""
"\n"
" <h3>Constraintverletzung</h3>\n"
" "
msgstr[1] ""
"\n"
" <h3>Constraintverletzungen</h3>\n"
" "
#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:7 #: AKScheduling/templates/admin/AKScheduling/unscheduled.html:7
msgid "Unscheduled AK Slots" msgid "Unscheduled AK Slots"
msgstr "Noch nicht geschedulte AK-Slots" msgstr "Noch nicht geschedulte AK-Slots"
...@@ -191,19 +246,19 @@ msgstr "Noch nicht geschedulte AK-Slots" ...@@ -191,19 +246,19 @@ msgstr "Noch nicht geschedulte AK-Slots"
msgid "Count" msgid "Count"
msgstr "Anzahl" msgstr "Anzahl"
#: AKScheduling/views.py:109 #: AKScheduling/views.py:150
msgid "Interest updated" msgid "Interest updated"
msgstr "Interesse aktualisiert" msgstr "Interesse aktualisiert"
#: AKScheduling/views.py:154 #: AKScheduling/views.py:201
msgid "Wishes" msgid "Wishes"
msgstr "Wünsche" msgstr "Wünsche"
#: AKScheduling/views.py:162 #: AKScheduling/views.py:219
msgid "Cleanup: Delete unscheduled slots for wishes" msgid "Cleanup: Delete unscheduled slots for wishes"
msgstr "Aufräumen: Noch nicht geplante Slots für Wünsche löschen" msgstr "Aufräumen: Noch nicht geplante Slots für Wünsche löschen"
#: AKScheduling/views.py:169 #: AKScheduling/views.py:226
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"The following {count} unscheduled slots of wishes will be deleted:\n" "The following {count} unscheduled slots of wishes will be deleted:\n"
...@@ -215,15 +270,15 @@ msgstr "" ...@@ -215,15 +270,15 @@ msgstr ""
"\n" "\n"
" {slots}" " {slots}"
#: AKScheduling/views.py:176 #: AKScheduling/views.py:233
msgid "Unscheduled slots for wishes successfully deleted" msgid "Unscheduled slots for wishes successfully deleted"
msgstr "Noch nicht geplante Slots für Wünsche erfolgreich gelöscht" msgstr "Noch nicht geplante Slots für Wünsche erfolgreich gelöscht"
#: AKScheduling/views.py:181 #: AKScheduling/views.py:247
msgid "Create default availabilities for AKs" msgid "Create default availabilities for AKs"
msgstr "Standardverfügbarkeiten für AKs anlegen" msgstr "Standardverfügbarkeiten für AKs anlegen"
#: AKScheduling/views.py:188 #: AKScheduling/views.py:254
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"The following {count} AKs don't have any availability information. Create " "The following {count} AKs don't have any availability information. Create "
...@@ -236,15 +291,22 @@ msgstr "" ...@@ -236,15 +291,22 @@ msgstr ""
"\n" "\n"
" {aks}" " {aks}"
#: AKScheduling/views.py:206 #: AKScheduling/views.py:274
#, python-brace-format #, python-brace-format
msgid "Could not create default availabilities for AK: {ak}" msgid "Could not create default availabilities for AK: {ak}"
msgstr "Konnte keine Verfügbarkeit anlegen für AK: {ak}" msgstr "Konnte keine Verfügbarkeit anlegen für AK: {ak}"
#: AKScheduling/views.py:211 #: AKScheduling/views.py:279
#, python-brace-format #, python-brace-format
msgid "Created default availabilities for {count} AKs" msgid "Created default availabilities for {count} AKs"
msgstr "Standardverfügbarkeiten für {count} AKs angelegt" msgstr "Standardverfügbarkeiten für {count} AKs angelegt"
#: AKScheduling/views.py:290
msgid "Constraint Violations"
msgstr "Constraintverletzungen"
#~ msgid "Bitte AK auswählen"
#~ msgstr "Please sel"
#~ msgid "Cannot load current violations from server" #~ msgid "Cannot load current violations from server"
#~ msgstr "Kann die aktuellen Verletzungen nicht vom Server laden" #~ msgstr "Kann die aktuellen Verletzungen nicht vom Server laden"
# This file mainly contains signal receivers, which follow a very strong interface, having e.g., a sender attribute
# that is hardly used by us. Nevertheless, to follow the django receiver coding style and since changes might
# cause issues when loading fixtures or model dumps, it is not wise to replace that attribute with "_".
# Therefore, the check that finds unused arguments is disabled for this whole file:
# pylint: disable=unused-argument
from django.db.models.signals import post_save, m2m_changed, pre_delete from django.db.models.signals import post_save, m2m_changed, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from AKModel.availability.models import Availability from AKModel.availability.models import Availability
from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation from AKModel.models import AK, AKSlot, Room, Event, ConstraintViolation
def update_constraint_violations(new_violations, existing_violations_to_check): def update_constraint_violations(new_violations, existing_violations_to_check):
...@@ -43,11 +49,15 @@ def update_cv_reso_deadline_for_slot(slot): ...@@ -43,11 +49,15 @@ def update_cv_reso_deadline_for_slot(slot):
:type slot: AKSlot :type slot: AKSlot
""" """
event = slot.event event = slot.event
# Update only if reso_deadline exists
# if event was changed and reso_deadline is removed, CVs will be deleted by event changed handler
# Update only has to be done for already scheduled slots with reso intention
if slot.ak.reso and slot.event.reso_deadline and slot.start: if slot.ak.reso and slot.event.reso_deadline and slot.start:
# Update only if reso_deadline exists
# if event was changed and reso_deadline is removed, CVs will be deleted by event changed handler
violation_type = ConstraintViolation.ViolationType.AK_AFTER_RESODEADLINE violation_type = ConstraintViolation.ViolationType.AK_AFTER_RESODEADLINE
new_violations = [] new_violations = []
# Violation?
if slot.end > event.reso_deadline: if slot.end > event.reso_deadline:
c = ConstraintViolation( c = ConstraintViolation(
type=violation_type, type=violation_type,
...@@ -69,38 +79,47 @@ def check_capacity_for_slot(slot: AKSlot): ...@@ -69,38 +79,47 @@ def check_capacity_for_slot(slot: AKSlot):
:return: Violation (if any) or None :return: Violation (if any) or None
:rtype: ConstraintViolation or None :rtype: ConstraintViolation or None
""" """
if slot.room:
if slot.room.capacity >= 0: # If slot is scheduled in a room and interest was specified
if slot.room.capacity < slot.ak.interest: if slot.room and slot.room.capacity >= 0 and slot.ak.interest >= 0:
c = ConstraintViolation( # Create a violation if interest exceeds room capacity
type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED, if slot.room.capacity < slot.ak.interest:
level=ConstraintViolation.ViolationLevel.VIOLATION, c = ConstraintViolation(
event=slot.event, type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED,
room=slot.room, level=ConstraintViolation.ViolationLevel.VIOLATION,
comment=_("Not enough space for AK interest (Interest: %(interest)d, Capacity: %(capacity)d)") event=slot.event,
% {'interest': slot.ak.interest, 'capacity': slot.room.capacity}, room=slot.room,
) comment=_("Not enough space for AK interest (Interest: %(interest)d, Capacity: %(capacity)d)")
c.ak_slots_tmp.add(slot) % {'interest': slot.ak.interest, 'capacity': slot.room.capacity},
c.aks_tmp.add(slot.ak) )
return c c.ak_slots_tmp.add(slot)
elif slot.room.capacity < slot.ak.interest + 5 or slot.room.capacity < slot.ak.interest * 1.25: c.aks_tmp.add(slot.ak)
c = ConstraintViolation( return c
type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED,
level=ConstraintViolation.ViolationLevel.WARNING, # Create a warning if interest is close to room capacity
event=slot.event, if slot.room.capacity < slot.ak.interest + 5 or slot.room.capacity < slot.ak.interest * 1.25:
room=slot.room, c = ConstraintViolation(
comment=_("Space is too close to AK interest (Interest: %(interest)d, Capacity: %(capacity)d)") type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED,
% {'interest': slot.ak.interest, 'capacity': slot.room.capacity} level=ConstraintViolation.ViolationLevel.WARNING,
) event=slot.event,
c.ak_slots_tmp.add(slot) room=slot.room,
c.aks_tmp.add(slot.ak) comment=_("Space is too close to AK interest (Interest: %(interest)d, Capacity: %(capacity)d)")
return c % {'interest': slot.ak.interest, 'capacity': slot.room.capacity}
return None )
c.ak_slots_tmp.add(slot)
c.aks_tmp.add(slot.ak)
return c
return None
@receiver(post_save, sender=AK) @receiver(post_save, sender=AK)
def ak_changed_handler(sender, instance: AK, **kwargs): def ak_changed_handler(sender, instance: AK, **kwargs):
# Changes might affect: Reso intention, Category, Interest """
Signal receiver: Check for violations after AK changed
Changes might affect: Reso intention, Category, Interest
"""
# TODO Reso intention changes # TODO Reso intention changes
# Check room capacities # Check room capacities
...@@ -118,14 +137,12 @@ def ak_changed_handler(sender, instance: AK, **kwargs): ...@@ -118,14 +137,12 @@ def ak_changed_handler(sender, instance: AK, **kwargs):
@receiver(m2m_changed, sender=AK.owners.through) @receiver(m2m_changed, sender=AK.owners.through)
def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs): def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs):
""" """
Owners of AK changed Signal receiver: Owners of AK changed
""" """
# Only signal after change (post_add, post_delete, post_clear) are relevant # Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"): if not action.startswith("post"):
return return
# print(f"{instance} changed")
event = instance.event event = instance.event
# Owner(s) changed: Might affect multiple AKs by the same owner(s) at the same time # Owner(s) changed: Might affect multiple AKs by the same owner(s) at the same time
...@@ -157,8 +174,6 @@ def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs): ...@@ -157,8 +174,6 @@ def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs):
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
#print(f"{owner} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -169,14 +184,12 @@ def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs): ...@@ -169,14 +184,12 @@ def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs):
@receiver(m2m_changed, sender=AK.conflicts.through) @receiver(m2m_changed, sender=AK.conflicts.through)
def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs): def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
""" """
Conflicts of AK changed Signal receiver: Conflicts of AK changed
""" """
# Only signal after change (post_add, post_delete, post_clear) are relevant # Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"): if not action.startswith("post"):
return return
# print(f"{instance} changed")
event = instance.event event = instance.event
# Conflict(s) changed: Might affect multiple AKs that are conflicts of each other # Conflict(s) changed: Might affect multiple AKs that are conflicts of each other
...@@ -186,6 +199,7 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs): ...@@ -186,6 +199,7 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False) slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
conflicts_of_this_ak: [AK] = instance.conflicts.all() conflicts_of_this_ak: [AK] = instance.conflicts.all()
# Loop over all existing conflicts
for ak in conflicts_of_this_ak: for ak in conflicts_of_this_ak:
if ak != instance: if ak != instance:
for other_slot in ak.akslot_set.filter(start__isnull=False): for other_slot in ak.akslot_set.filter(start__isnull=False):
...@@ -203,8 +217,6 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs): ...@@ -203,8 +217,6 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
# print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -215,23 +227,22 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs): ...@@ -215,23 +227,22 @@ def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
@receiver(m2m_changed, sender=AK.prerequisites.through) @receiver(m2m_changed, sender=AK.prerequisites.through)
def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs): def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs):
""" """
Prerequisites of AK changed Signal receiver: Prerequisites of AK changed
""" """
# Only signal after change (post_add, post_delete, post_clear) are relevant # Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"): if not action.startswith("post"):
return return
# print(f"{instance} changed")
event = instance.event event = instance.event
# Conflict(s) changed: Might affect multiple AKs that are conflicts of each other # Prerequisite(s) changed: Might affect multiple AKs that should have a certain order
violation_type = ConstraintViolation.ViolationType.AK_BEFORE_PREREQUISITE violation_type = ConstraintViolation.ViolationType.AK_BEFORE_PREREQUISITE
new_violations = [] new_violations = []
slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False) slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
prerequisites_of_this_ak: [AK] = instance.prerequisites.all() prerequisites_of_this_ak: [AK] = instance.prerequisites.all()
# Loop over all prerequisites
for ak in prerequisites_of_this_ak: for ak in prerequisites_of_this_ak:
if ak != instance: if ak != instance:
for other_slot in ak.akslot_set.filter(start__isnull=False): for other_slot in ak.akslot_set.filter(start__isnull=False):
...@@ -249,8 +260,6 @@ def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs ...@@ -249,8 +260,6 @@ def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
# print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -261,14 +270,12 @@ def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs ...@@ -261,14 +270,12 @@ def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs
@receiver(m2m_changed, sender=AK.requirements.through) @receiver(m2m_changed, sender=AK.requirements.through)
def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs): def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs):
""" """
Requirements of AK changed Signal receiver: Requirements of AK changed
""" """
# Only signal after change (post_add, post_delete, post_clear) are relevant # Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"): if not action.startswith("post"):
return return
# print(f"{instance} changed")
event = instance.event event = instance.event
# Requirement(s) changed: Might affect slots and rooms # Requirement(s) changed: Might affect slots and rooms
...@@ -298,8 +305,6 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs) ...@@ -298,8 +305,6 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs)
c.ak_slots_tmp.add(slot) c.ak_slots_tmp.add(slot)
new_violations.append(c) new_violations.append(c)
# print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -309,8 +314,13 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs) ...@@ -309,8 +314,13 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs)
@receiver(post_save, sender=AKSlot) @receiver(post_save, sender=AKSlot)
def akslot_changed_handler(sender, instance: AKSlot, **kwargs): def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
# Changes might affect: Duplicate parallel, Two in room, Resodeadline """
# print(f"{sender} changed") Signal receiver: AKSlot changed
Changes might affect: Duplicate parallel, Two in room, Resodeadline
"""
# TODO Consider rewriting this very long and complex method to resolve several (style) issues:
# pylint: disable=too-many-nested-blocks,too-many-locals,too-many-branches,too-many-statements
event = instance.event event = instance.event
# == Check for two parallel slots by one of the owners == # == Check for two parallel slots by one of the owners ==
...@@ -341,8 +351,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -341,8 +351,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
# print(f"{owner} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -373,8 +381,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -373,8 +381,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
# print(f"Multiple slots in room {instance.room}: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the slot that was recently changed (important!) # belonging to the slot that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -437,8 +443,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -437,8 +443,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
c.ak_slots_tmp.add(instance) c.ak_slots_tmp.add(instance)
new_violations.append(c) new_violations.append(c)
# print(f"{instance.ak} has the following slots outside availabilities: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -470,8 +474,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -470,8 +474,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
c.ak_slots_tmp.add(instance) c.ak_slots_tmp.add(instance)
new_violations.append(c) new_violations.append(c)
# print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
...@@ -502,8 +504,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -502,8 +504,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
# print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
...@@ -534,8 +534,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -534,8 +534,6 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
c.ak_slots_tmp.add(other_slot) c.ak_slots_tmp.add(other_slot)
new_violations.append(c) new_violations.append(c)
# print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
...@@ -547,15 +545,21 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -547,15 +545,21 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
new_violations = [cv] if cv is not None else [] new_violations = [cv] if cv is not None else []
# Compare to/update list of existing violations of this type for this slot # Compare to/update list of existing violations of this type for this slot
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED)) existing_violations_to_check = list(
instance.constraintviolation_set.filter(type=ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED)
)
update_constraint_violations(new_violations, existing_violations_to_check) update_constraint_violations(new_violations, existing_violations_to_check)
@receiver(pre_delete, sender=AKSlot) @receiver(pre_delete, sender=AKSlot)
def akslot_deleted_handler(sender, instance: AKSlot, **kwargs): def akslot_deleted_handler(sender, instance: AKSlot, **kwargs):
# Manually clean up or remove constraint violations that belong to this slot since there is no cascade deletion """
# for many2many relationships. Explicitly listening for AK deletion signals is not necessary since they will Signal receiver: AKSlot deleted
# transitively trigger this signal and we always set both AK and AKSlot references in a constraint violation
Manually clean up or remove constraint violations that belong to this slot since there is no cascade deletion
for many2many relationships. Explicitly listening for AK deletion signals is not necessary since they will
transitively trigger this signal and we always set both AK and AKSlot references in a constraint violation
"""
# print(f"{instance} deleted") # print(f"{instance} deleted")
for cv in instance.constraintviolation_set.all(): for cv in instance.constraintviolation_set.all():
...@@ -566,8 +570,11 @@ def akslot_deleted_handler(sender, instance: AKSlot, **kwargs): ...@@ -566,8 +570,11 @@ def akslot_deleted_handler(sender, instance: AKSlot, **kwargs):
@receiver(post_save, sender=Room) @receiver(post_save, sender=Room)
def room_changed_handler(sender, instance: Room, **kwargs): def room_changed_handler(sender, instance: Room, **kwargs):
# Changes might affect: Room size """
Signal receiver: Room changed
Changes might affect: Room size
"""
# Check room capacities # Check room capacities
violation_type = ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED violation_type = ConstraintViolation.ViolationType.ROOM_CAPACITY_EXCEEDED
new_violations = [] new_violations = []
...@@ -583,24 +590,23 @@ def room_changed_handler(sender, instance: Room, **kwargs): ...@@ -583,24 +590,23 @@ def room_changed_handler(sender, instance: Room, **kwargs):
@receiver(m2m_changed, sender=Room.properties.through) @receiver(m2m_changed, sender=Room.properties.through)
def room_requirements_changed_handler(sender, instance: Room, action: str, **kwargs): def room_requirements_changed_handler(sender, instance: Room, action: str, **kwargs):
""" """
Requirements of room changed Signal Receiver: Requirements of room changed
""" """
# Only signal after change (post_add, post_delete, post_clear) are relevant # Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"): if not action.startswith("post"):
return return
# print(f"{instance} changed") # event = instance.event
event = instance.event
# TODO React to changes # TODO React to changes
@receiver(post_save, sender=Availability) @receiver(post_save, sender=Availability)
def availability_changed_handler(sender, instance: Availability, **kwargs): def availability_changed_handler(sender, instance: Availability, **kwargs):
# Changes might affect: category availability, AK availability, Room availability """
# print(f"{instance} changed") Signal receiver: Availalability changed
Changes might affect: category availability, AK availability, Room availability
"""
event = instance.event event = instance.event
# An AK's availability changed: Might affect AK slots scheduled outside the permitted time # An AK's availability changed: Might affect AK slots scheduled outside the permitted time
...@@ -627,8 +633,6 @@ def availability_changed_handler(sender, instance: Availability, **kwargs): ...@@ -627,8 +633,6 @@ def availability_changed_handler(sender, instance: Availability, **kwargs):
c.ak_slots_tmp.add(slot) c.ak_slots_tmp.add(slot)
new_violations.append(c) new_violations.append(c)
# print(f"{instance.ak} has the following slots outside availabilities: {new_violations}")
# ... and compare to/update list of existing violations of this type # ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!) # belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type)) existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
...@@ -638,7 +642,12 @@ def availability_changed_handler(sender, instance: Availability, **kwargs): ...@@ -638,7 +642,12 @@ def availability_changed_handler(sender, instance: Availability, **kwargs):
@receiver(post_save, sender=Event) @receiver(post_save, sender=Event)
def event_changed_handler(sender, instance: Event, **kwargs): def event_changed_handler(sender, instance: Event, **kwargs):
# == Check for reso ak after reso deadline (which might have changed) == """
Signal receiver: Event changed
Changes might affect: Reso deadline
"""
# Check for reso ak after reso deadline (which might have changed)
if instance.reso_deadline: if instance.reso_deadline:
for slot in instance.akslot_set.filter(start__isnull=False, ak__reso=True): for slot in instance.akslot_set.filter(start__isnull=False, ak__reso=True):
update_cv_reso_deadline_for_slot(slot) update_cv_reso_deadline_for_slot(slot)
......
.gu-mirror {
position: fixed !important;
margin: 0 !important;
z-index: 9999 !important;
opacity: 0.8;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
filter: alpha(opacity=80);
}
.gu-hide {
display: none !important;
}
.gu-unselectable {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
.gu-transit {
opacity: 0.2;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
filter: alpha(opacity=20);
}