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
Commits on Source (123)
Showing
with 2304 additions and 329 deletions
image: python:3.9 image: python:3.10
services: services:
- mysql - mysql
...@@ -38,7 +38,7 @@ test: ...@@ -38,7 +38,7 @@ test:
script: script:
- source venv/bin/activate - source venv/bin/activate
- echo "GRANT ALL on *.* to '${MYSQL_USER}';"| mysql -u root --password="${MYSQL_ROOT_PASSWORD}" -h mysql - echo "GRANT ALL on *.* to '${MYSQL_USER}';"| mysql -u root --password="${MYSQL_ROOT_PASSWORD}" -h mysql
- pip install pytest-cov unittest-xml-reporting - pip install pytest-cov unittest-xml-reporting beautifulsoup4
- coverage run --source='.' manage.py test --settings AKPlanning.settings_ci - coverage run --source='.' manage.py test --settings AKPlanning.settings_ci
after_script: after_script:
- source venv/bin/activate - source venv/bin/activate
...@@ -56,6 +56,8 @@ lint: ...@@ -56,6 +56,8 @@ lint:
extends: .before_script_template extends: .before_script_template
stage: test stage: test
script: script:
- source venv/bin/activate
- pip install beautifulsoup4
- pylint --load-plugins pylint_django --django-settings-module=AKPlanning.settings_ci --rcfile pylintrc --exit-zero --output-format=text AK* | tee /tmp/pylint.txt - pylint --load-plugins pylint_django --django-settings-module=AKPlanning.settings_ci --rcfile pylintrc --exit-zero --output-format=text AK* | tee /tmp/pylint.txt
- sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' /tmp/pylint.txt > public/badges/$CI_JOB_NAME.score - sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' /tmp/pylint.txt > public/badges/$CI_JOB_NAME.score
- pylint --load-plugins pylint_django --django-settings-module=AKPlanning.settings_ci --rcfile pylintrc --exit-zero --output-format=pylint_gitlab.GitlabCodeClimateReporter AK* > codeclimate.json - pylint --load-plugins pylint_django --django-settings-module=AKPlanning.settings_ci --rcfile pylintrc --exit-zero --output-format=pylint_gitlab.GitlabCodeClimateReporter AK* > codeclimate.json
......
...@@ -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: 2025-01-01 17:28+0100\n" "POT-Creation-Date: 2025-02-27 15:13+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"
...@@ -113,22 +113,22 @@ msgstr "AK-Einreichung" ...@@ -113,22 +113,22 @@ msgstr "AK-Einreichung"
msgid "AK History" msgid "AK History"
msgstr "AK-Verlauf" msgstr "AK-Verlauf"
#: AKDashboard/views.py:69 #: AKDashboard/views.py:70
#, python-format #, python-format
msgid "New AK: %(ak)s." msgid "New AK: %(ak)s."
msgstr "Neuer AK: %(ak)s." msgstr "Neuer AK: %(ak)s."
#: AKDashboard/views.py:72 #: AKDashboard/views.py:73
#, python-format #, python-format
msgid "AK \"%(ak)s\" edited." msgid "AK \"%(ak)s\" edited."
msgstr "AK \"%(ak)s\" bearbeitet." msgstr "AK \"%(ak)s\" bearbeitet."
#: AKDashboard/views.py:75 #: AKDashboard/views.py:76
#, python-format #, python-format
msgid "AK \"%(ak)s\" deleted." msgid "AK \"%(ak)s\" deleted."
msgstr "AK \"%(ak)s\" gelöscht." msgstr "AK \"%(ak)s\" gelöscht."
#: AKDashboard/views.py:90 #: AKDashboard/views.py:91
#, python-format #, python-format
msgid "AK \"%(ak)s\" (re-)scheduled." msgid "AK \"%(ak)s\" (re-)scheduled."
msgstr "AK \"%(ak)s\" (um-)geplant." msgstr "AK \"%(ak)s\" (um-)geplant."
...@@ -6,7 +6,7 @@ from django.utils.timezone import now ...@@ -6,7 +6,7 @@ from django.utils.timezone import now
from AKDashboard.models import DashboardButton from AKDashboard.models import DashboardButton
from AKModel.models import Event, AK, AKCategory from AKModel.models import Event, AK, AKCategory
from AKModel.tests import BasicViewTests from AKModel.tests.test_views import BasicViewTests
class DashboardTests(TestCase): class DashboardTests(TestCase):
......
...@@ -183,7 +183,7 @@ class AvailabilitiesFormMixin(forms.Form): ...@@ -183,7 +183,7 @@ class AvailabilitiesFormMixin(forms.Form):
for avail in availabilities: for avail in availabilities:
setattr(avail, reference_name, instance.id) setattr(avail, reference_name, instance.id)
def _replace_availabilities(self, instance, availabilities: [Availability]): def _replace_availabilities(self, instance, availabilities: list[Availability]):
""" """
Replace the existing list of availabilities belonging to an entity with a new, updated one Replace the existing list of availabilities belonging to an entity with a new, updated one
......
...@@ -151,9 +151,12 @@ class Availability(models.Model): ...@@ -151,9 +151,12 @@ class Availability(models.Model):
if not other.overlaps(self, strict=False): if not other.overlaps(self, strict=False):
raise Exception('Only overlapping Availabilities can be merged.') raise Exception('Only overlapping Availabilities can be merged.')
return Availability( avail = Availability(
start=min(self.start, other.start), end=max(self.end, other.end) start=min(self.start, other.start), end=max(self.end, other.end)
) )
if self.event == other.event:
avail.event = self.event
return avail
def __or__(self, other: 'Availability') -> 'Availability': def __or__(self, other: 'Availability') -> 'Availability':
"""Performs the merge operation: ``availability1 | availability2``""" """Performs the merge operation: ``availability1 | availability2``"""
...@@ -168,9 +171,12 @@ class Availability(models.Model): ...@@ -168,9 +171,12 @@ class Availability(models.Model):
if not other.overlaps(self, False): if not other.overlaps(self, False):
raise Exception('Only overlapping Availabilities can be intersected.') raise Exception('Only overlapping Availabilities can be intersected.')
return Availability( avail = Availability(
start=max(self.start, other.start), end=min(self.end, other.end) start=max(self.start, other.start), end=min(self.end, other.end)
) )
if self.event == other.event:
avail.event = self.event
return avail
def __and__(self, other: 'Availability') -> 'Availability': def __and__(self, other: 'Availability') -> 'Availability':
"""Performs the intersect operation: ``availability1 & """Performs the intersect operation: ``availability1 &
...@@ -247,7 +253,14 @@ class Availability(models.Model): ...@@ -247,7 +253,14 @@ class Availability(models.Model):
f'{self.end.astimezone(self.event.timezone).strftime("%a %H:%M")}') f'{self.end.astimezone(self.event.timezone).strftime("%a %H:%M")}')
@classmethod @classmethod
def with_event_length(cls, event, person=None, room=None, ak=None, ak_category=None): def with_event_length(
cls,
event: Event,
person: AKOwner | None = None,
room: Room | None = None,
ak: AK | None = None,
ak_category: AKCategory | None = None,
) -> "Availability":
""" """
Create an availability covering exactly the time between event start and event end. Create an availability covering exactly the time between event start and event end.
Can e.g., be used to create default availabilities. Can e.g., be used to create default availabilities.
...@@ -267,6 +280,30 @@ class Availability(models.Model): ...@@ -267,6 +280,30 @@ class Availability(models.Model):
return Availability(start=timeframe_start, end=timeframe_end, event=event, person=person, return Availability(start=timeframe_start, end=timeframe_end, event=event, person=person,
room=room, ak=ak, ak_category=ak_category) room=room, ak=ak, ak_category=ak_category)
def is_covered(self, availabilities: List['Availability']):
"""Check if list of availibilities cover this object.
:param availabilities: availabilities to check.
:return: whether the availabilities cover full event.
:rtype: bool
"""
avail_union = Availability.union(availabilities)
return any(avail.contains(self) for avail in avail_union)
@classmethod
def is_event_covered(cls, event: Event, availabilities: List['Availability']) -> bool:
"""Check if list of availibilities cover whole event.
:param event: event to check.
:param availabilities: availabilities to check.
:return: whether the availabilities cover full event.
:rtype: bool
"""
# NOTE: Cannot use `Availability.with_event_length` as its end is the
# event end + 1 day
full_event = Availability(event=event, start=event.start, end=event.end)
return full_event.is_covered(availabilities)
class Meta: class Meta:
verbose_name = _('Availability') verbose_name = _('Availability')
verbose_name_plural = _('Availabilities') verbose_name_plural = _('Availabilities')
......
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
"model": "AKModel.akcategory", "model": "AKModel.akcategory",
"pk": 1, "pk": 1,
"fields": { "fields": {
"name": "Spa", "name": "Spaß",
"color": "275246", "color": "275246",
"description": "", "description": "",
"present_by_default": true, "present_by_default": true,
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
"model": "AKModel.akcategory", "model": "AKModel.akcategory",
"pk": 3, "pk": 3,
"fields": { "fields": {
"name": "Spa/Kultur", "name": "Spaß/Kultur",
"color": "333333", "color": "333333",
"description": "", "description": "",
"present_by_default": true, "present_by_default": true,
...@@ -437,6 +437,62 @@ ...@@ -437,6 +437,62 @@
] ]
} }
}, },
{
"model": "AKModel.ak",
"pk": 4,
"fields": {
"name": "Test AK fixed slots",
"short_name": "testfixed",
"description": "",
"link": "",
"protocol_link": "",
"category": 4,
"track": null,
"reso": false,
"present": true,
"notes": "",
"interest": -1,
"interest_counter": 0,
"include_in_export": false,
"event": 2,
"owners": [
1
],
"requirements": [
3
],
"conflicts": [],
"prerequisites": []
}
},
{
"model": "AKModel.ak",
"pk": 5,
"fields": {
"name": "Test AK Ernst",
"short_name": "testernst",
"description": "",
"link": "",
"protocol_link": "",
"category": 2,
"track": null,
"reso": false,
"present": true,
"notes": "",
"interest": -1,
"interest_counter": 0,
"include_in_export": false,
"event": 1,
"owners": [
3
],
"requirements": [
2
],
"conflicts": [],
"prerequisites": []
}
},
{ {
"model": "AKModel.room", "model": "AKModel.room",
"pk": 1, "pk": 1,
...@@ -461,6 +517,19 @@ ...@@ -461,6 +517,19 @@
"properties": [] "properties": []
} }
}, },
{
"model": "AKModel.room",
"pk": 3,
"fields": {
"name": "BBB Session 1",
"location": "",
"capacity": -1,
"event": 1,
"properties": [
2
]
}
},
{ {
"model": "AKModel.akslot", "model": "AKModel.akslot",
"pk": 1, "pk": 1,
...@@ -526,6 +595,58 @@ ...@@ -526,6 +595,58 @@
"updated": "2022-12-02T12:23:11.856Z" "updated": "2022-12-02T12:23:11.856Z"
} }
}, },
{
"model": "AKModel.akslot",
"pk": 6,
"fields": {
"ak": 4,
"room": null,
"start": "2020-11-08T18:30:00Z",
"duration": "2.00",
"fixed": true,
"event": 2,
"updated": "2022-12-02T12:23:11.856Z"
}
},
{
"model": "AKModel.akslot",
"pk": 7,
"fields": {
"ak": 4,
"room": 2,
"start": null,
"duration": "2.00",
"fixed": true,
"event": 2,
"updated": "2022-12-02T12:23:11.856Z"
}
},
{
"model": "AKModel.akslot",
"pk": 8,
"fields": {
"ak": 4,
"room": 2,
"start": "2020-11-07T16:00:00Z",
"duration": "2.00",
"fixed": true,
"event": 2,
"updated": "2022-12-02T12:23:11.856Z"
}
},
{
"model": "AKModel.akslot",
"pk": 9,
"fields": {
"ak": 5,
"room": null,
"start": null,
"duration": "2.00",
"fixed": false,
"event": 1,
"updated": "2022-12-02T12:23:11.856Z"
}
},
{ {
"model": "AKModel.constraintviolation", "model": "AKModel.constraintviolation",
"pk": 1, "pk": 1,
...@@ -669,5 +790,71 @@ ...@@ -669,5 +790,71 @@
"start": "2020-11-07T18:30:00Z", "start": "2020-11-07T18:30:00Z",
"end": "2020-11-07T21:30:00Z" "end": "2020-11-07T21:30:00Z"
} }
},
{
"model": "AKModel.availability",
"pk": 7,
"fields": {
"event": 1,
"person": null,
"room": null,
"ak": 5,
"ak_category": null,
"start": "2020-10-01T17:41:22Z",
"end": "2020-10-04T17:41:30Z"
}
},
{
"model": "AKModel.availability",
"pk": 8,
"fields": {
"event": 1,
"person": null,
"room": 3,
"ak": null,
"ak_category": null,
"start": "2020-10-01T17:41:22Z",
"end": "2020-10-04T17:41:30Z"
}
},
{
"model": "AKModel.defaultslot",
"pk": 1,
"fields": {
"event": 2,
"start": "2020-11-07T08:00:00Z",
"end": "2020-11-07T12:00:00Z",
"primary_categories": [5]
}
},
{
"model": "AKModel.defaultslot",
"pk": 2,
"fields": {
"event": 2,
"start": "2020-11-07T14:00:00Z",
"end": "2020-11-07T17:00:00Z",
"primary_categories": [4]
}
},
{
"model": "AKModel.defaultslot",
"pk": 3,
"fields": {
"event": 2,
"start": "2020-11-08T08:00:00Z",
"end": "2020-11-08T19:00:00Z",
"primary_categories": [4, 5]
}
},
{
"model": "AKModel.defaultslot",
"pk": 4,
"fields": {
"event": 2,
"start": "2020-11-09T17:00:00Z",
"end": "2020-11-10T01:00:00Z",
"primary_categories": [4, 5, 3]
}
} }
] ]
...@@ -4,8 +4,10 @@ Central and admin forms ...@@ -4,8 +4,10 @@ Central and admin forms
import csv import csv
import io import io
import json
from django import forms from django import forms
from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
...@@ -281,3 +283,61 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm): ...@@ -281,3 +283,61 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm):
# Filter possible values for m2m when event is specified # Filter possible values for m2m when event is specified
if hasattr(self.instance, "event") and self.instance.event is not None: if hasattr(self.instance, "event") and self.instance.event is not None:
self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event) self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event)
class JSONScheduleImportForm(AdminIntermediateForm):
"""Form to import an AK schedule from a json file."""
json_data = forms.CharField(
required=False,
widget=forms.Textarea,
label=_("JSON data"),
help_text=_("JSON data from the scheduling solver"),
)
json_file = forms.FileField(
required=False,
label=_("File with JSON data"),
help_text=_("File with JSON data from the scheduling solver"),
)
def _check_json_data(self, data: str):
try:
schedule = json.loads(data)
except json.JSONDecodeError as ex:
raise ValidationError(_("Cannot decode as JSON"), "invalid") from ex
for field in ["input", "scheduled_aks"]:
if not field in schedule:
raise ValidationError(
_("Invalid JSON format: field '%(field)s' is missing"),
"invalid",
params={"field": field}
)
# TODO: Add further checks on json input
return schedule
def clean(self):
cleaned_data = super().clean()
if cleaned_data.get("json_file") and cleaned_data.get("json_data"):
err = ValidationError(
_("Please enter data as a file OR via text, not both."), "invalid"
)
self.add_error("json_data", err)
self.add_error("json_file", err)
elif not (cleaned_data.get("json_file") or cleaned_data.get("json_data")):
err = ValidationError(
_("No data entered. Please enter data as a file or via text."), "invalid"
)
self.add_error("json_data", err)
self.add_error("json_file", err)
else:
source_field = "json_data"
data = cleaned_data.get(source_field)
if not data:
source_field = "json_file"
with cleaned_data.get(source_field).open() as ff:
data = ff.read()
try:
cleaned_data["data"] = self._check_json_data(data)
except ValidationError as ex:
self.add_error(source_field, ex)
return cleaned_data
...@@ -2,7 +2,7 @@ msgid "" ...@@ -2,7 +2,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: 2025-02-26 16:35+0100\n" "POT-Creation-Date: 2025-03-03 00:44+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"
...@@ -25,18 +25,18 @@ msgstr "Status" ...@@ -25,18 +25,18 @@ msgstr "Status"
msgid "Toggle plan visibility" msgid "Toggle plan visibility"
msgstr "Plansichtbarkeit ändern" msgstr "Plansichtbarkeit ändern"
#: AKModel/admin.py:112 AKModel/admin.py:123 AKModel/views/manage.py:138 #: AKModel/admin.py:112 AKModel/admin.py:123 AKModel/views/manage.py:140
msgid "Publish plan" msgid "Publish plan"
msgstr "Plan veröffentlichen" msgstr "Plan veröffentlichen"
#: AKModel/admin.py:115 AKModel/admin.py:131 AKModel/views/manage.py:151 #: AKModel/admin.py:115 AKModel/admin.py:131 AKModel/views/manage.py:153
msgid "Unpublish plan" msgid "Unpublish plan"
msgstr "Plan verbergen" msgstr "Plan verbergen"
#: AKModel/admin.py:170 AKModel/models.py:381 AKModel/models.py:707 #: AKModel/admin.py:170 AKModel/models.py:878 AKModel/models.py:1332
#: AKModel/templates/admin/AKModel/aks_by_user.html:12 #: AKModel/models.py:1368 AKModel/templates/admin/AKModel/aks_by_user.html:12
#: AKModel/templates/admin/AKModel/status/event_aks.html:10 #: AKModel/templates/admin/AKModel/status/event_aks.html:10
#: AKModel/views/manage.py:73 AKModel/views/status.py:102 #: AKModel/views/manage.py:75 AKModel/views/status.py:102
msgid "AKs" msgid "AKs"
msgstr "AKs" msgstr "AKs"
...@@ -60,11 +60,11 @@ msgstr "In Wiki-Syntax exportieren" ...@@ -60,11 +60,11 @@ msgstr "In Wiki-Syntax exportieren"
msgid "Cannot export AKs from more than one event at the same time." msgid "Cannot export AKs from more than one event at the same time."
msgstr "Kann nicht AKs von mehreren Events zur selben Zeit exportieren." msgstr "Kann nicht AKs von mehreren Events zur selben Zeit exportieren."
#: AKModel/admin.py:335 AKModel/views/ak.py:99 #: AKModel/admin.py:335 AKModel/views/ak.py:123
msgid "Reset interest in AKs" msgid "Reset interest in AKs"
msgstr "Interesse an AKs zurücksetzen" msgstr "Interesse an AKs zurücksetzen"
#: AKModel/admin.py:345 AKModel/views/ak.py:114 #: AKModel/admin.py:345 AKModel/views/ak.py:138
msgid "Reset AKs' interest counters" msgid "Reset AKs' interest counters"
msgstr "Interessenszähler der AKs zurücksetzen" msgstr "Interessenszähler der AKs zurücksetzen"
...@@ -72,35 +72,35 @@ msgstr "Interessenszähler der AKs zurücksetzen" ...@@ -72,35 +72,35 @@ msgstr "Interessenszähler der AKs zurücksetzen"
msgid "AK Details" msgid "AK Details"
msgstr "AK-Details" msgstr "AK-Details"
#: AKModel/admin.py:520 AKModel/views/manage.py:99 #: AKModel/admin.py:520 AKModel/views/manage.py:101
msgid "Mark Constraint Violations as manually resolved" msgid "Mark Constraint Violations as manually resolved"
msgstr "Markiere Constraintverletzungen als manuell behoben" msgstr "Markiere Constraintverletzungen als manuell behoben"
#: AKModel/admin.py:529 AKModel/views/manage.py:112 #: AKModel/admin.py:529 AKModel/views/manage.py:114
msgid "Set Constraint Violations to level \"violation\"" msgid "Set Constraint Violations to level \"violation\""
msgstr "Constraintverletzungen auf Level \"Violation\" setzen" msgstr "Constraintverletzungen auf Level \"Violation\" setzen"
#: AKModel/admin.py:538 AKModel/views/manage.py:125 #: AKModel/admin.py:538 AKModel/views/manage.py:127
msgid "Set Constraint Violations to level \"warning\"" msgid "Set Constraint Violations to level \"warning\""
msgstr "Constraintverletzungen auf Level \"Warning\" setzen" msgstr "Constraintverletzungen auf Level \"Warning\" setzen"
#: AKModel/admin.py:580 #: AKModel/admin.py:585
msgid "Activate selected users" msgid "Activate selected users"
msgstr "Ausgewählte Benutzer*innen aktivieren" msgstr "Ausgewählte Benutzer*innen aktivieren"
#: AKModel/admin.py:583 #: AKModel/admin.py:594
msgid "The selected users have been activated." msgid "The selected users have been activated."
msgstr "Benutzer*innen aktiviert" msgstr "Benutzer*innen aktiviert"
#: AKModel/admin.py:585 #: AKModel/admin.py:596
msgid "Deactivate selected users" msgid "Deactivate selected users"
msgstr "Ausgewählte Benutzer*innen deaktivieren" msgstr "Ausgewählte Benutzer*innen deaktivieren"
#: AKModel/admin.py:588 #: AKModel/admin.py:605
msgid "The selected users have been deactivated." msgid "The selected users have been deactivated."
msgstr "Benutzer*innen deaktiviert" msgstr "Benutzer*innen deaktiviert"
#: AKModel/availability/forms.py:25 AKModel/availability/models.py:271 #: AKModel/availability/forms.py:25 AKModel/availability/models.py:308
msgid "Availability" msgid "Availability"
msgstr "Verfügbarkeit" msgstr "Verfügbarkeit"
...@@ -125,19 +125,19 @@ msgstr "Die eingegebene Verfügbarkeit enthält ein ungültiges Datum." ...@@ -125,19 +125,19 @@ msgstr "Die eingegebene Verfügbarkeit enthält ein ungültiges Datum."
msgid "Please fill in your availabilities!" msgid "Please fill in your availabilities!"
msgstr "Bitte Verfügbarkeiten eintragen!" msgstr "Bitte Verfügbarkeiten eintragen!"
#: AKModel/availability/models.py:43 AKModel/models.py:60 AKModel/models.py:174 #: AKModel/availability/models.py:43 AKModel/models.py:166
#: AKModel/models.py:251 AKModel/models.py:270 AKModel/models.py:296 #: AKModel/models.py:657 AKModel/models.py:734 AKModel/models.py:767
#: AKModel/models.py:315 AKModel/models.py:371 AKModel/models.py:517 #: AKModel/models.py:793 AKModel/models.py:812 AKModel/models.py:868
#: AKModel/models.py:556 AKModel/models.py:646 AKModel/models.py:703 #: AKModel/models.py:1026 AKModel/models.py:1103 AKModel/models.py:1271
#: AKModel/models.py:894 #: AKModel/models.py:1328 AKModel/models.py:1519
msgid "Event" msgid "Event"
msgstr "Event" msgstr "Event"
#: AKModel/availability/models.py:44 AKModel/models.py:175 #: AKModel/availability/models.py:44 AKModel/models.py:658
#: AKModel/models.py:252 AKModel/models.py:271 AKModel/models.py:297 #: AKModel/models.py:735 AKModel/models.py:768 AKModel/models.py:794
#: AKModel/models.py:316 AKModel/models.py:372 AKModel/models.py:518 #: AKModel/models.py:813 AKModel/models.py:869 AKModel/models.py:1027
#: AKModel/models.py:557 AKModel/models.py:647 AKModel/models.py:704 #: AKModel/models.py:1104 AKModel/models.py:1272 AKModel/models.py:1329
#: AKModel/models.py:895 #: AKModel/models.py:1520
msgid "Associated event" msgid "Associated event"
msgstr "Zugehöriges Event" msgstr "Zugehöriges Event"
...@@ -149,8 +149,8 @@ msgstr "Person" ...@@ -149,8 +149,8 @@ msgstr "Person"
msgid "Person whose availability this is" msgid "Person whose availability this is"
msgstr "Person deren Verfügbarkeit hier abgebildet wird" msgstr "Person deren Verfügbarkeit hier abgebildet wird"
#: AKModel/availability/models.py:61 AKModel/models.py:521 #: AKModel/availability/models.py:61 AKModel/models.py:1030
#: AKModel/models.py:546 AKModel/models.py:713 #: AKModel/models.py:1093 AKModel/models.py:1338
msgid "Room" msgid "Room"
msgstr "Raum" msgstr "Raum"
...@@ -158,8 +158,8 @@ msgstr "Raum" ...@@ -158,8 +158,8 @@ msgstr "Raum"
msgid "Room whose availability this is" msgid "Room whose availability this is"
msgstr "Raum dessen Verfügbarkeit hier abgebildet wird" msgstr "Raum dessen Verfügbarkeit hier abgebildet wird"
#: AKModel/availability/models.py:70 AKModel/models.py:380 #: AKModel/availability/models.py:70 AKModel/models.py:877
#: AKModel/models.py:545 AKModel/models.py:641 #: AKModel/models.py:1092 AKModel/models.py:1266
msgid "AK" msgid "AK"
msgstr "AK" msgstr "AK"
...@@ -167,8 +167,8 @@ msgstr "AK" ...@@ -167,8 +167,8 @@ msgstr "AK"
msgid "AK whose availability this is" msgid "AK whose availability this is"
msgstr "Verfügbarkeiten" msgstr "Verfügbarkeiten"
#: AKModel/availability/models.py:79 AKModel/models.py:255 #: AKModel/availability/models.py:79 AKModel/models.py:738
#: AKModel/models.py:719 #: AKModel/models.py:1344
msgid "AK Category" msgid "AK Category"
msgstr "AK-Kategorie" msgstr "AK-Kategorie"
...@@ -176,64 +176,64 @@ msgstr "AK-Kategorie" ...@@ -176,64 +176,64 @@ msgstr "AK-Kategorie"
msgid "AK Category whose availability this is" msgid "AK Category whose availability this is"
msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird" msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird"
#: AKModel/availability/models.py:272 #: AKModel/availability/models.py:309 AKModel/models.py:926
msgid "Availabilities" msgid "Availabilities"
msgstr "Verfügbarkeiten" msgstr "Verfügbarkeiten"
#: AKModel/forms.py:78 #: AKModel/forms.py:80
msgid "Copy ak requirements and ak categories of existing event" msgid "Copy ak requirements and ak categories of existing event"
msgstr "AK-Anforderungen und AK-Kategorien eines existierenden Events kopieren" msgstr "AK-Anforderungen und AK-Kategorien eines existierenden Events kopieren"
#: AKModel/forms.py:79 #: AKModel/forms.py:81
msgid "You can choose what to copy in the next step" msgid "You can choose what to copy in the next step"
msgstr "" msgstr ""
"Im nächsten Schritt kann ausgewählt werden, was genau kopiert werden soll" "Im nächsten Schritt kann ausgewählt werden, was genau kopiert werden soll"
#: AKModel/forms.py:93 #: AKModel/forms.py:95
msgid "Copy ak categories" msgid "Copy ak categories"
msgstr "AK-Kategorien kopieren" msgstr "AK-Kategorien kopieren"
#: AKModel/forms.py:100 #: AKModel/forms.py:102
msgid "Copy ak requirements" msgid "Copy ak requirements"
msgstr "AK-Anforderungen kopieren" msgstr "AK-Anforderungen kopieren"
#: AKModel/forms.py:107 #: AKModel/forms.py:109
msgid "Copy types" msgid "Copy types"
msgstr "Typen kopieren" msgstr "Typen kopieren"
#: AKModel/forms.py:133 #: AKModel/forms.py:135
msgid "Copy dashboard buttons" msgid "Copy dashboard buttons"
msgstr "Dashboard-Buttons kopieren" msgstr "Dashboard-Buttons kopieren"
#: AKModel/forms.py:174 #: AKModel/forms.py:176
msgid "# next AKs" msgid "# next AKs"
msgstr "# nächste AKs" msgstr "# nächste AKs"
#: AKModel/forms.py:175 #: AKModel/forms.py:177
msgid "How many next AKs should be shown on a slide?" msgid "How many next AKs should be shown on a slide?"
msgstr "Wie viele nächste AKs sollen auf einer Folie angezeigt werden?" msgstr "Wie viele nächste AKs sollen auf einer Folie angezeigt werden?"
#: AKModel/forms.py:178 #: AKModel/forms.py:180
msgid "Presentation only?" msgid "Presentation only?"
msgstr "Nur Vorstellung?" msgstr "Nur Vorstellung?"
#: AKModel/forms.py:180 AKModel/forms.py:187 #: AKModel/forms.py:182 AKModel/forms.py:189
msgid "Yes" msgid "Yes"
msgstr "Ja" msgstr "Ja"
#: AKModel/forms.py:180 AKModel/forms.py:187 #: AKModel/forms.py:182 AKModel/forms.py:189
msgid "No" msgid "No"
msgstr "Nein" msgstr "Nein"
#: AKModel/forms.py:182 #: AKModel/forms.py:184
msgid "Restrict AKs to those that asked for chance to be presented?" msgid "Restrict AKs to those that asked for chance to be presented?"
msgstr "AKs auf solche, die um eine Vorstellung gebeten haben, einschränken?" msgstr "AKs auf solche, die um eine Vorstellung gebeten haben, einschränken?"
#: AKModel/forms.py:185 #: AKModel/forms.py:187
msgid "Space for notes in wishes?" msgid "Space for notes in wishes?"
msgstr "Platz für Notizen bei den Wünschen?" msgstr "Platz für Notizen bei den Wünschen?"
#: AKModel/forms.py:189 #: AKModel/forms.py:191
msgid "" msgid ""
"Create symbols indicating space to note down owners and timeslots for " "Create symbols indicating space to note down owners and timeslots for "
"wishes, e.g., to be filled out on a touch screen while presenting?" "wishes, e.g., to be filled out on a touch screen while presenting?"
...@@ -242,11 +242,11 @@ msgstr "" ...@@ -242,11 +242,11 @@ msgstr ""
"fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen " "fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen "
"ausgefüllt zu werden?" "ausgefüllt zu werden?"
#: AKModel/forms.py:198 AKModel/models.py:888 #: AKModel/forms.py:200 AKModel/models.py:1513
msgid "Default Slots" msgid "Default Slots"
msgstr "Standardslots" msgstr "Standardslots"
#: AKModel/forms.py:200 #: AKModel/forms.py:202
msgid "" msgid ""
"Click and drag to add default slots, double-click to delete. Or use the " "Click and drag to add default slots, double-click to delete. Or use the "
"start and end inputs to add entries to the calendar view." "start and end inputs to add entries to the calendar view."
...@@ -255,11 +255,11 @@ msgstr "" ...@@ -255,11 +255,11 @@ msgstr ""
"Einträge zu löschen. Oder Start- und End-Eingabe verwenden, um der " "Einträge zu löschen. Oder Start- und End-Eingabe verwenden, um der "
"Kalenderansicht neue Einträge hinzuzufügen." "Kalenderansicht neue Einträge hinzuzufügen."
#: AKModel/forms.py:216 #: AKModel/forms.py:218
msgid "New rooms" msgid "New rooms"
msgstr "Neue Räume" msgstr "Neue Räume"
#: AKModel/forms.py:217 #: AKModel/forms.py:219
msgid "" msgid ""
"Enter room details in CSV format. Required colum is \"name\", optional " "Enter room details in CSV format. Required colum is \"name\", optional "
"colums are \"location\", \"capacity\", and \"url\" for online/hybrid rooms. " "colums are \"location\", \"capacity\", and \"url\" for online/hybrid rooms. "
...@@ -269,19 +269,52 @@ msgstr "" ...@@ -269,19 +269,52 @@ msgstr ""
"Spalten sind \"location\", \"capacity\", und \"url\" for Online-/" "Spalten sind \"location\", \"capacity\", und \"url\" for Online-/"
"HybridräumeTrennzeichen: Semikolon" "HybridräumeTrennzeichen: Semikolon"
#: AKModel/forms.py:223 #: AKModel/forms.py:225
msgid "Default availabilities?" msgid "Default availabilities?"
msgstr "Standardverfügbarkeiten?" msgstr "Standardverfügbarkeiten?"
#: AKModel/forms.py:224 #: AKModel/forms.py:226
msgid "Create default availabilities for all rooms?" msgid "Create default availabilities for all rooms?"
msgstr "Standardverfügbarkeiten für alle Räume anlegen?" msgstr "Standardverfügbarkeiten für alle Räume anlegen?"
#: AKModel/forms.py:240 #: AKModel/forms.py:242
msgid "CSV must contain a name column" msgid "CSV must contain a name column"
msgstr "CSV muss eine name-Spalte enthalten" msgstr "CSV muss eine name-Spalte enthalten"
#: AKModel/metaviews/admin.py:156 AKModel/models.py:29 #: AKModel/forms.py:293
msgid "JSON data"
msgstr "JSON-Daten"
#: AKModel/forms.py:294
msgid "JSON data from the scheduling solver"
msgstr "JSON-Daten, die der scheduling-solver produziert hat"
#: AKModel/forms.py:299
msgid "File with JSON data"
msgstr "Datei mit JSON-Daten"
#: AKModel/forms.py:300
msgid "File with JSON data from the scheduling solver"
msgstr "Datei mit JSON-Daten, die der scheduling-solver produziert hat"
#: AKModel/forms.py:307
msgid "Cannot decode as JSON"
msgstr "Dekodierung als JSON fehlgeschlagen"
#: AKModel/forms.py:311
#, python-format
msgid "Invalid JSON format: field '%(field)s' is missing"
msgstr "Ungültige JSON-Eingabe: das Feld '%(field)s' fehlt"
#: AKModel/forms.py:321
msgid "Please enter data as a file OR via text, not both."
msgstr "Gib die Daten bitte als Datei oder als Text ein, nicht beides."
#: AKModel/forms.py:327
msgid "No data entered. Please enter data as a file or via text."
msgstr "Keine Daten eingegeben. Gib die Daten bitte als Datei oder als Text ein."
#: AKModel/metaviews/admin.py:156 AKModel/models.py:129
msgid "Start" msgid "Start"
msgstr "Start" msgstr "Start"
...@@ -306,67 +339,67 @@ msgstr "Aktivieren?" ...@@ -306,67 +339,67 @@ msgstr "Aktivieren?"
msgid "Finish" msgid "Finish"
msgstr "Abschluss" msgstr "Abschluss"
#: AKModel/models.py:20 AKModel/models.py:243 AKModel/models.py:267 #: AKModel/models.py:120 AKModel/models.py:726 AKModel/models.py:764
#: AKModel/models.py:294 AKModel/models.py:313 AKModel/models.py:331 #: AKModel/models.py:791 AKModel/models.py:810 AKModel/models.py:828
#: AKModel/models.py:507 #: AKModel/models.py:1018
msgid "Name" msgid "Name"
msgstr "Name" msgstr "Name"
#: AKModel/models.py:21 #: AKModel/models.py:121
msgid "Name or iteration of the event" msgid "Name or iteration of the event"
msgstr "Name oder Iteration des Events" msgstr "Name oder Iteration des Events"
#: AKModel/models.py:22 #: AKModel/models.py:122
msgid "Short Form" msgid "Short Form"
msgstr "Kurzer Name" msgstr "Kurzer Name"
#: AKModel/models.py:23 #: AKModel/models.py:123
msgid "Short name of letters/numbers/dots/dashes/underscores used in URLs." msgid "Short name of letters/numbers/dots/dashes/underscores used in URLs."
msgstr "" msgstr ""
"Kurzname bestehend aus Buchstaben, Nummern, Punkten und Unterstrichen zur " "Kurzname bestehend aus Buchstaben, Nummern, Punkten und Unterstrichen zur "
"Nutzung in URLs" "Nutzung in URLs"
#: AKModel/models.py:25 #: AKModel/models.py:125
msgid "Place" msgid "Place"
msgstr "Ort" msgstr "Ort"
#: AKModel/models.py:26 #: AKModel/models.py:126
msgid "City etc. the event takes place in" msgid "City etc. the event takes place in"
msgstr "Stadt o.ä. in der das Event stattfindet" msgstr "Stadt o.ä. in der das Event stattfindet"
#: AKModel/models.py:28 #: AKModel/models.py:128
msgid "Time Zone" msgid "Time Zone"
msgstr "Zeitzone" msgstr "Zeitzone"
#: AKModel/models.py:28 #: AKModel/models.py:128
msgid "Time Zone where this event takes place in" msgid "Time Zone where this event takes place in"
msgstr "Zeitzone in der das Event stattfindet" msgstr "Zeitzone in der das Event stattfindet"
#: AKModel/models.py:29 #: AKModel/models.py:129
msgid "Time the event begins" msgid "Time the event begins"
msgstr "Zeit zu der das Event beginnt" msgstr "Zeit zu der das Event beginnt"
#: AKModel/models.py:30 #: AKModel/models.py:130
msgid "End" msgid "End"
msgstr "Ende" msgstr "Ende"
#: AKModel/models.py:30 #: AKModel/models.py:130
msgid "Time the event ends" msgid "Time the event ends"
msgstr "Zeit zu der das Event endet" msgstr "Zeit zu der das Event endet"
#: AKModel/models.py:31 #: AKModel/models.py:131
msgid "Resolution Deadline" msgid "Resolution Deadline"
msgstr "Resolutionsdeadline" msgstr "Resolutionsdeadline"
#: AKModel/models.py:32 #: AKModel/models.py:132
msgid "When should AKs with intention to submit a resolution be done?" msgid "When should AKs with intention to submit a resolution be done?"
msgstr "Wann sollen AKs mit Resolutionsabsicht stattgefunden haben?" msgstr "Wann sollen AKs mit Resolutionsabsicht stattgefunden haben?"
#: AKModel/models.py:34 #: AKModel/models.py:134
msgid "Interest Window Start" msgid "Interest Window Start"
msgstr "Beginn Interessensbekundung" msgstr "Beginn Interessensbekundung"
#: AKModel/models.py:36 #: AKModel/models.py:136
msgid "" msgid ""
"Opening time for expression of interest. When left blank, no interest " "Opening time for expression of interest. When left blank, no interest "
"indication will be possible." "indication will be possible."
...@@ -374,71 +407,83 @@ msgstr "" ...@@ -374,71 +407,83 @@ msgstr ""
"Öffnungszeitpunkt für die Angabe von Interesse an AKs.Wenn das Feld leer " "Öffnungszeitpunkt für die Angabe von Interesse an AKs.Wenn das Feld leer "
"bleibt, wird keine Abgabe von Interesse möglich sein." "bleibt, wird keine Abgabe von Interesse möglich sein."
#: AKModel/models.py:38 #: AKModel/models.py:138
msgid "Interest Window End" msgid "Interest Window End"
msgstr "Ende Interessensbekundung" msgstr "Ende Interessensbekundung"
#: AKModel/models.py:39 #: AKModel/models.py:139
msgid "Closing time for expression of interest." msgid "Closing time for expression of interest."
msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs." msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs."
#: AKModel/models.py:41 #: AKModel/models.py:141
msgid "Public event" msgid "Public event"
msgstr "Öffentliches Event" msgstr "Öffentliches Event"
#: AKModel/models.py:42 #: AKModel/models.py:142
msgid "Show this event on overview page." msgid "Show this event on overview page."
msgstr "Zeige dieses Event auf der Übersichtseite an" msgstr "Zeige dieses Event auf der Übersichtseite an"
#: AKModel/models.py:44 #: AKModel/models.py:144
msgid "Active State" msgid "Active State"
msgstr "Aktiver Status" msgstr "Aktiver Status"
#: AKModel/models.py:44 #: AKModel/models.py:144
msgid "Marks currently active events" msgid "Marks currently active events"
msgstr "Markiert aktuell aktive Events" msgstr "Markiert aktuell aktive Events"
#: AKModel/models.py:45 #: AKModel/models.py:145
msgid "Plan Hidden" msgid "Plan Hidden"
msgstr "Plan verborgen" msgstr "Plan verborgen"
#: AKModel/models.py:45 #: AKModel/models.py:145
msgid "Hides plan for non-staff users" msgid "Hides plan for non-staff users"
msgstr "Verbirgt den Plan für Nutzer*innen ohne erweiterte Rechte" msgstr "Verbirgt den Plan für Nutzer*innen ohne erweiterte Rechte"
#: AKModel/models.py:47 #: AKModel/models.py:147
msgid "Plan published at" msgid "Plan published at"
msgstr "Plan veröffentlicht am/um" msgstr "Plan veröffentlicht am/um"
#: AKModel/models.py:48 #: AKModel/models.py:148
msgid "Timestamp at which the plan was published" msgid "Timestamp at which the plan was published"
msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde" msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde"
#: AKModel/models.py:50 #: AKModel/models.py:150
msgid "Base URL" msgid "Base URL"
msgstr "URL-Prefix" msgstr "URL-Prefix"
#: AKModel/models.py:50 #: AKModel/models.py:150
msgid "Prefix for wiki link construction" msgid "Prefix for wiki link construction"
msgstr "Prefix für die automatische Generierung von Wiki-Links" msgstr "Prefix für die automatische Generierung von Wiki-Links"
#: AKModel/models.py:51 #: AKModel/models.py:151
msgid "Wiki Export Template Name" msgid "Wiki Export Template Name"
msgstr "Wiki-Export Templatename" msgstr "Wiki-Export Templatename"
#: AKModel/models.py:52 #: AKModel/models.py:152
msgid "Default Slot Length" msgid "Default Slot Length"
msgstr "Standardslotlänge" msgstr "Standardslotlänge"
#: AKModel/models.py:53 #: AKModel/models.py:153
msgid "Default length in hours that is assumed for AKs in this event." msgid "Default length in hours that is assumed for AKs in this event."
msgstr "Standardlänge von Slots (in Stunden) für dieses Event" msgstr "Standardlänge von Slots (in Stunden) für dieses Event"
#: AKModel/models.py:55 #: AKModel/models.py:154
msgid "Export Slot Length"
msgstr "Export-Slotlänge"
#: AKModel/models.py:156
msgid ""
"Slot duration in hours that is used in the timeslot discretization, when "
"this event is exported for the solver."
msgstr ""
"Länge von Slots (in Stunden) in der Zeitslot-Diskretisierung beim JSON-"
"Export dieses Events."
#: AKModel/models.py:161
msgid "Contact email address" msgid "Contact email address"
msgstr "E-Mail Kontaktadresse" msgstr "E-Mail Kontaktadresse"
#: AKModel/models.py:56 #: AKModel/models.py:162
msgid "" msgid ""
"An email address that is displayed on every page and can be used for all " "An email address that is displayed on every page and can be used for all "
"kinds of questions" "kinds of questions"
...@@ -446,75 +491,116 @@ msgstr "" ...@@ -446,75 +491,116 @@ msgstr ""
"Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von " "Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von "
"Fragen genutzt werden kann" "Fragen genutzt werden kann"
#: AKModel/models.py:61 #: AKModel/models.py:167
msgid "Events" msgid "Events"
msgstr "Events" msgstr "Events"
#: AKModel/models.py:169 #: AKModel/models.py:427
msgid "Cannot parse malformed JSON input."
msgstr "Kann fehlerhafte JSON-Eingabe nicht verarbeiten"
#: AKModel/models.py:430
msgid "Data has changed since the export. Reexport and run the solver again."
msgstr "Seit dem Export wurden die Daten verändert. Wiederhole den Export und führe den Solver erneut aus."
#: AKModel/models.py:447
#, python-brace-format
msgid "AK {ak_name} is not assigned any timeslot by the solver"
msgstr "Dem AK {ak_name} wurde vom Solver kein Zeitslot zugewiesen"
#: AKModel/models.py:457
#, python-brace-format
msgid ""
"Duration of AK {ak_name} assigned by solver ({solver_duration} hours) is "
"less than the duration required by the slot ({slot_duration} hours)"
msgstr ""
"Die dem AK {ak_name} vom Solver zugewiesene Dauer ({solver_duration} "
"Stunden) ist kürzer als die aktuell vorgesehene Dauer des Slots "
"({slot_duration} Stunden)"
#: AKModel/models.py:471
#, python-brace-format
msgid ""
"Fixed AK {ak_name} assigned by solver to room {solver_room} is fixed to room "
"{slot_room}"
msgstr ""
"Dem fix geplanten AK {ak_name} wurde vom Solver Raum {solver_room} "
"zugewiesen, dabei ist der AK bereits fix in Raum {slot_room} eingeplant."
#: AKModel/models.py:482
#, python-brace-format
msgid ""
"Fixed AK {ak_name} assigned by solver to start at {solver_start} is fixed to "
"start at {slot_start}"
msgstr ""
"Dem fix geplanten AK {ak_name} wurde vom Solver die Startzeit {solver_start} "
"zugewiesen, dabei ist der AK bereits für {slot_start} eingeplant."
#: AKModel/models.py:652
msgid "Nickname" msgid "Nickname"
msgstr "Spitzname" msgstr "Spitzname"
#: AKModel/models.py:169 #: AKModel/models.py:652
msgid "Name to identify an AK owner by" msgid "Name to identify an AK owner by"
msgstr "Name, durch den eine AK-Leitung identifiziert wird" msgstr "Name, durch den eine AK-Leitung identifiziert wird"
#: AKModel/models.py:170 #: AKModel/models.py:653
msgid "Slug" msgid "Slug"
msgstr "Slug" msgstr "Slug"
#: AKModel/models.py:170 #: AKModel/models.py:653
msgid "Slug for URL generation" msgid "Slug for URL generation"
msgstr "Slug für URL-Generierung" msgstr "Slug für URL-Generierung"
#: AKModel/models.py:171 #: AKModel/models.py:654
msgid "Institution" msgid "Institution"
msgstr "Instutution" msgstr "Instutution"
#: AKModel/models.py:171 #: AKModel/models.py:654
msgid "Uni etc." msgid "Uni etc."
msgstr "Universität o.ä." msgstr "Universität o.ä."
#: AKModel/models.py:172 AKModel/models.py:340 #: AKModel/models.py:655 AKModel/models.py:837
msgid "Web Link" msgid "Web Link"
msgstr "Internet Link" msgstr "Internet Link"
#: AKModel/models.py:172 #: AKModel/models.py:655
msgid "Link to Homepage" msgid "Link to Homepage"
msgstr "Link zu Homepage oder Webseite" msgstr "Link zu Homepage oder Webseite"
#: AKModel/models.py:178 AKModel/models.py:712 #: AKModel/models.py:661 AKModel/models.py:1337
msgid "AK Owner" msgid "AK Owner"
msgstr "AK-Leitung" msgstr "AK-Leitung"
#: AKModel/models.py:179 #: AKModel/models.py:662
msgid "AK Owners" msgid "AK Owners"
msgstr "AK-Leitungen" msgstr "AK-Leitungen"
#: AKModel/models.py:243 #: AKModel/models.py:726
msgid "Name of the AK Category" msgid "Name of the AK Category"
msgstr "Name der AK-Kategorie" msgstr "Name der AK-Kategorie"
#: AKModel/models.py:244 AKModel/models.py:268 #: AKModel/models.py:727 AKModel/models.py:765
msgid "Color" msgid "Color"
msgstr "Farbe" msgstr "Farbe"
#: AKModel/models.py:244 AKModel/models.py:268 #: AKModel/models.py:727 AKModel/models.py:765
msgid "Color for displaying" msgid "Color for displaying"
msgstr "Farbe für die Anzeige" msgstr "Farbe für die Anzeige"
#: AKModel/models.py:245 AKModel/models.py:334 #: AKModel/models.py:728 AKModel/models.py:831
msgid "Description" msgid "Description"
msgstr "Beschreibung" msgstr "Beschreibung"
#: AKModel/models.py:246 #: AKModel/models.py:729
msgid "Short description of this AK Category" msgid "Short description of this AK Category"
msgstr "Beschreibung der AK-Kategorie" msgstr "Beschreibung der AK-Kategorie"
#: AKModel/models.py:247 #: AKModel/models.py:730
msgid "Present by default" msgid "Present by default"
msgstr "Defaultmäßig präsentieren" msgstr "Defaultmäßig präsentieren"
#: AKModel/models.py:248 #: AKModel/models.py:731
msgid "" msgid ""
"Present AKs of this category by default if AK owner did not specify whether " "Present AKs of this category by default if AK owner did not specify whether "
"this AK should be presented?" "this AK should be presented?"
...@@ -522,152 +608,152 @@ msgstr "" ...@@ -522,152 +608,152 @@ msgstr ""
"AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für " "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für "
"ihren AK nicht explizit spezifiziert haben?" "ihren AK nicht explizit spezifiziert haben?"
#: AKModel/models.py:256 #: AKModel/models.py:739
msgid "AK Categories" msgid "AK Categories"
msgstr "AK-Kategorien" msgstr "AK-Kategorien"
#: AKModel/models.py:267 #: AKModel/models.py:764
msgid "Name of the AK Track" msgid "Name of the AK Track"
msgstr "Name des AK-Tracks" msgstr "Name des AK-Tracks"
#: AKModel/models.py:274 #: AKModel/models.py:771
msgid "AK Track" msgid "AK Track"
msgstr "AK-Track" msgstr "AK-Track"
#: AKModel/models.py:275 #: AKModel/models.py:772
msgid "AK Tracks" msgid "AK Tracks"
msgstr "AK-Tracks" msgstr "AK-Tracks"
#: AKModel/models.py:294 #: AKModel/models.py:791
msgid "Name of the Requirement" msgid "Name of the Requirement"
msgstr "Name der Anforderung" msgstr "Name der Anforderung"
#: AKModel/models.py:300 AKModel/models.py:716 #: AKModel/models.py:797 AKModel/models.py:1341
msgid "AK Requirement" msgid "AK Requirement"
msgstr "AK-Anforderung" msgstr "AK-Anforderung"
#: AKModel/models.py:301 #: AKModel/models.py:798
msgid "AK Requirements" msgid "AK Requirements"
msgstr "AK-Anforderungen" msgstr "AK-Anforderungen"
#: AKModel/models.py:313 #: AKModel/models.py:810
msgid "Name describing the type" msgid "Name describing the type"
msgstr "Name, der den Typ beschreibt" msgstr "Name, der den Typ beschreibt"
#: AKModel/models.py:319 #: AKModel/models.py:816
msgid "AK Type" msgid "AK Type"
msgstr "AK Typ" msgstr "AK Typ"
#: AKModel/models.py:320 #: AKModel/models.py:817
msgid "AK Types" msgid "AK Types"
msgstr "AK-Typen" msgstr "AK-Typen"
#: AKModel/models.py:331 #: AKModel/models.py:828
msgid "Name of the AK" msgid "Name of the AK"
msgstr "Name des AKs" msgstr "Name des AKs"
#: AKModel/models.py:332 #: AKModel/models.py:829
msgid "Short Name" msgid "Short Name"
msgstr "Kurzer Name" msgstr "Kurzer Name"
#: AKModel/models.py:333 #: AKModel/models.py:830
msgid "Name displayed in the schedule" msgid "Name displayed in the schedule"
msgstr "Name zur Anzeige im AK-Plan" msgstr "Name zur Anzeige im AK-Plan"
#: AKModel/models.py:334 #: AKModel/models.py:831
msgid "Description of the AK" msgid "Description of the AK"
msgstr "Beschreibung des AKs" msgstr "Beschreibung des AKs"
#: AKModel/models.py:336 #: AKModel/models.py:833
msgid "Owners" msgid "Owners"
msgstr "Leitungen" msgstr "Leitungen"
#: AKModel/models.py:337 #: AKModel/models.py:834
msgid "Those organizing the AK" msgid "Those organizing the AK"
msgstr "Menschen, die den AK organisieren und halten" msgstr "Menschen, die den AK organisieren und halten"
#: AKModel/models.py:340 #: AKModel/models.py:837
msgid "Link to wiki page" msgid "Link to wiki page"
msgstr "Link zur Wiki Seite" msgstr "Link zur Wiki Seite"
#: AKModel/models.py:341 #: AKModel/models.py:838
msgid "Protocol Link" msgid "Protocol Link"
msgstr "Protokolllink" msgstr "Protokolllink"
#: AKModel/models.py:341 #: AKModel/models.py:838
msgid "Link to protocol" msgid "Link to protocol"
msgstr "Link zum Protokoll" msgstr "Link zum Protokoll"
#: AKModel/models.py:343 #: AKModel/models.py:840
msgid "Category" msgid "Category"
msgstr "Kategorie" msgstr "Kategorie"
#: AKModel/models.py:344 #: AKModel/models.py:841
msgid "Category of the AK" msgid "Category of the AK"
msgstr "Kategorie des AKs" msgstr "Kategorie des AKs"
#: AKModel/models.py:345 #: AKModel/models.py:842 AKModel/models.py:907
msgid "Types" msgid "Types"
msgstr "Typen" msgstr "Typen"
#: AKModel/models.py:346 #: AKModel/models.py:843
msgid "This AK is" msgid "This AK is"
msgstr "Dieser AK ist" msgstr "Dieser AK ist"
#: AKModel/models.py:347 #: AKModel/models.py:844
msgid "Track" msgid "Track"
msgstr "Track" msgstr "Track"
#: AKModel/models.py:348 #: AKModel/models.py:845
msgid "Track the AK belongs to" msgid "Track the AK belongs to"
msgstr "Track zu dem der AK gehört" msgstr "Track zu dem der AK gehört"
#: AKModel/models.py:350 #: AKModel/models.py:847
msgid "Resolution Intention" msgid "Resolution Intention"
msgstr "Resolutionsabsicht" msgstr "Resolutionsabsicht"
#: AKModel/models.py:351 #: AKModel/models.py:848
msgid "Intends to submit a resolution" msgid "Intends to submit a resolution"
msgstr "Beabsichtigt eine Resolution einzureichen" msgstr "Beabsichtigt eine Resolution einzureichen"
#: AKModel/models.py:352 #: AKModel/models.py:849
msgid "Present this AK" msgid "Present this AK"
msgstr "AK präsentieren" msgstr "AK präsentieren"
#: AKModel/models.py:353 #: AKModel/models.py:850
msgid "Present results of this AK" msgid "Present results of this AK"
msgstr "Die Ergebnisse dieses AKs vorstellen" msgstr "Die Ergebnisse dieses AKs vorstellen"
#: AKModel/models.py:355 AKModel/views/status.py:167 #: AKModel/models.py:852 AKModel/models.py:905 AKModel/views/status.py:175
msgid "Requirements" msgid "Requirements"
msgstr "Anforderungen" msgstr "Anforderungen"
#: AKModel/models.py:356 #: AKModel/models.py:853
msgid "AK's Requirements" msgid "AK's Requirements"
msgstr "Anforderungen des AKs" msgstr "Anforderungen des AKs"
#: AKModel/models.py:358 #: AKModel/models.py:855
msgid "Conflicting AKs" msgid "Conflicting AKs"
msgstr "AK-Konflikte" msgstr "AK-Konflikte"
#: AKModel/models.py:359 #: AKModel/models.py:856
msgid "AKs that conflict and thus must not take place at the same time" msgid "AKs that conflict and thus must not take place at the same time"
msgstr "" msgstr ""
"AKs, die Konflikte haben und deshalb nicht gleichzeitig stattfinden dürfen" "AKs, die Konflikte haben und deshalb nicht gleichzeitig stattfinden dürfen"
#: AKModel/models.py:360 #: AKModel/models.py:857
msgid "Prerequisite AKs" msgid "Prerequisite AKs"
msgstr "Vorausgesetzte AKs" msgstr "Vorausgesetzte AKs"
#: AKModel/models.py:361 #: AKModel/models.py:858
msgid "AKs that should precede this AK in the schedule" msgid "AKs that should precede this AK in the schedule"
msgstr "AKs die im AK-Plan vor diesem AK stattfinden müssen" msgstr "AKs die im AK-Plan vor diesem AK stattfinden müssen"
#: AKModel/models.py:363 #: AKModel/models.py:860
msgid "Organizational Notes" msgid "Organizational Notes"
msgstr "Notizen zur Organisation" msgstr "Notizen zur Organisation"
#: AKModel/models.py:364 #: AKModel/models.py:861
msgid "" msgid ""
"Notes to organizers. These are public. For private notes, please use the " "Notes to organizers. These are public. For private notes, please use the "
"button for private messages on the detail page of this AK (after creation/" "button for private messages on the detail page of this AK (after creation/"
...@@ -677,295 +763,303 @@ msgstr "" ...@@ -677,295 +763,303 @@ msgstr ""
"Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem " "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem "
"Anlegen/Bearbeiten)." "Anlegen/Bearbeiten)."
#: AKModel/models.py:367 #: AKModel/models.py:864 AKModel/models.py:903
msgid "Interest" msgid "Interest"
msgstr "Interesse" msgstr "Interesse"
#: AKModel/models.py:367 #: AKModel/models.py:864
msgid "Expected number of people" msgid "Expected number of people"
msgstr "Erwartete Personenzahl" msgstr "Erwartete Personenzahl"
#: AKModel/models.py:368 #: AKModel/models.py:865
msgid "Interest Counter" msgid "Interest Counter"
msgstr "Interessenszähler" msgstr "Interessenszähler"
#: AKModel/models.py:369 #: AKModel/models.py:866
msgid "People who have indicated interest online" msgid "People who have indicated interest online"
msgstr "Anzahl Personen, die online Interesse bekundet haben" msgstr "Anzahl Personen, die online Interesse bekundet haben"
#: AKModel/models.py:374 #: AKModel/models.py:871
msgid "Export?" msgid "Export?"
msgstr "Export?" msgstr "Export?"
#: AKModel/models.py:375 #: AKModel/models.py:872
msgid "Include AK in wiki export?" msgid "Include AK in wiki export?"
msgstr "AK bei Wiki-Export berücksichtigen?" msgstr "AK bei Wiki-Export berücksichtigen?"
#: AKModel/models.py:507 #: AKModel/models.py:922
msgid "Conflicts"
msgstr "Konflikte"
#: AKModel/models.py:925
msgid "Prerequisites"
msgstr "Voraussetzungen"
#: AKModel/models.py:1018
msgid "Name or number of the room" msgid "Name or number of the room"
msgstr "Name oder Nummer des Raums" msgstr "Name oder Nummer des Raums"
#: AKModel/models.py:508 #: AKModel/models.py:1019
msgid "Location" msgid "Location"
msgstr "Ort" msgstr "Ort"
#: AKModel/models.py:509 #: AKModel/models.py:1020
msgid "Name or number of the location" msgid "Name or number of the location"
msgstr "Name oder Nummer des Ortes" msgstr "Name oder Nummer des Ortes"
#: AKModel/models.py:510 #: AKModel/models.py:1021
msgid "Capacity" msgid "Capacity"
msgstr "Kapazität" msgstr "Kapazität"
#: AKModel/models.py:511 #: AKModel/models.py:1022
msgid "Maximum number of people (-1 for unlimited)." msgid "Maximum number of people (-1 for unlimited)."
msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)." msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)."
#: AKModel/models.py:512 #: AKModel/models.py:1023
msgid "Properties" msgid "Properties"
msgstr "Eigenschaften" msgstr "Eigenschaften"
#: AKModel/models.py:513 #: AKModel/models.py:1024
msgid "AK requirements fulfilled by the room" msgid "AK requirements fulfilled by the room"
msgstr "AK-Anforderungen, die dieser Raum erfüllt" msgstr "AK-Anforderungen, die dieser Raum erfüllt"
#: AKModel/models.py:522 AKModel/views/status.py:59 #: AKModel/models.py:1031 AKModel/views/status.py:59
msgid "Rooms" msgid "Rooms"
msgstr "Räume" msgstr "Räume"
#: AKModel/models.py:545 #: AKModel/models.py:1092
msgid "AK being mapped" msgid "AK being mapped"
msgstr "AK, der zugeordnet wird" msgstr "AK, der zugeordnet wird"
#: AKModel/models.py:547 #: AKModel/models.py:1094
msgid "Room the AK will take place in" msgid "Room the AK will take place in"
msgstr "Raum in dem der AK stattfindet" msgstr "Raum in dem der AK stattfindet"
#: AKModel/models.py:548 AKModel/models.py:891 #: AKModel/models.py:1095 AKModel/models.py:1516
msgid "Slot Begin" msgid "Slot Begin"
msgstr "Beginn des Slots" msgstr "Beginn des Slots"
#: AKModel/models.py:548 AKModel/models.py:891 #: AKModel/models.py:1095 AKModel/models.py:1516
msgid "Time and date the slot begins" msgid "Time and date the slot begins"
msgstr "Zeit und Datum zu der der AK beginnt" msgstr "Zeit und Datum zu der der AK beginnt"
#: AKModel/models.py:550 #: AKModel/models.py:1097
msgid "Duration" msgid "Duration"
msgstr "Dauer" msgstr "Dauer"
#: AKModel/models.py:551 #: AKModel/models.py:1098
msgid "Length in hours" msgid "Length in hours"
msgstr "Länge in Stunden" msgstr "Länge in Stunden"
#: AKModel/models.py:553 #: AKModel/models.py:1100
msgid "Scheduling fixed" msgid "Scheduling fixed"
msgstr "Planung fix" msgstr "Planung fix"
#: AKModel/models.py:554 #: AKModel/models.py:1101
msgid "Length and time of this AK should not be changed" msgid "Length and time of this AK should not be changed"
msgstr "Dauer und Zeit dieses AKs sollten nicht verändert werden" msgstr "Dauer und Zeit dieses AKs sollten nicht verändert werden"
#: AKModel/models.py:559 #: AKModel/models.py:1106
msgid "Last update" msgid "Last update"
msgstr "Letzte Aktualisierung" msgstr "Letzte Aktualisierung"
#: AKModel/models.py:562 #: AKModel/models.py:1109
msgid "AK Slot" msgid "AK Slot"
msgstr "AK-Slot" msgstr "AK-Slot"
#: AKModel/models.py:563 AKModel/models.py:709 #: AKModel/models.py:1110 AKModel/models.py:1334 AKModel/models.py:1369
msgid "AK Slots" msgid "AK Slots"
msgstr "AK-Slot" msgstr "AK-Slot"
#: AKModel/models.py:585 AKModel/models.py:594 #: AKModel/models.py:1132 AKModel/models.py:1141
msgid "Not scheduled yet" msgid "Not scheduled yet"
msgstr "Noch nicht geplant" msgstr "Noch nicht geplant"
#: AKModel/models.py:642 #: AKModel/models.py:1267
msgid "AK this message belongs to" msgid "AK this message belongs to"
msgstr "AK zu dem die Nachricht gehört" msgstr "AK zu dem die Nachricht gehört"
#: AKModel/models.py:643 #: AKModel/models.py:1268
msgid "Message text" msgid "Message text"
msgstr "Nachrichtentext" msgstr "Nachrichtentext"
#: AKModel/models.py:644 #: AKModel/models.py:1269
msgid "Message to the organizers. This is not publicly visible." msgid "Message to the organizers. This is not publicly visible."
msgstr "" msgstr ""
"Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar." "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar."
#: AKModel/models.py:648 #: AKModel/models.py:1273
msgid "Resolved" msgid "Resolved"
msgstr "Erledigt" msgstr "Erledigt"
#: AKModel/models.py:649 #: AKModel/models.py:1274
msgid "This message has been resolved (no further action needed)" msgid "This message has been resolved (no further action needed)"
msgstr "" msgstr ""
"Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen " "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen "
"notwendig)" "notwendig)"
#: AKModel/models.py:652 #: AKModel/models.py:1277
msgid "AK Orga Message" msgid "AK Orga Message"
msgstr "AK-Organachricht" msgstr "AK-Organachricht"
#: AKModel/models.py:653 #: AKModel/models.py:1278
msgid "AK Orga Messages" msgid "AK Orga Messages"
msgstr "AK-Organachrichten" msgstr "AK-Organachrichten"
#: AKModel/models.py:670 #: AKModel/models.py:1295
msgid "Constraint Violation" msgid "Constraint Violation"
msgstr "Constraintverletzung" msgstr "Constraintverletzung"
#: AKModel/models.py:671 #: AKModel/models.py:1296
msgid "Constraint Violations" msgid "Constraint Violations"
msgstr "Constraintverletzungen" msgstr "Constraintverletzungen"
#: AKModel/models.py:678 #: AKModel/models.py:1303
msgid "Owner has two parallel slots" msgid "Owner has two parallel slots"
msgstr "Leitung hat zwei Slots parallel" msgstr "Leitung hat zwei Slots parallel"
#: AKModel/models.py:679 #: AKModel/models.py:1304
msgid "AK Slot was scheduled outside the AK's availabilities" msgid "AK Slot was scheduled outside the AK's availabilities"
msgstr "AK Slot wurde außerhalb der Verfügbarkeit des AKs platziert" msgstr "AK Slot wurde außerhalb der Verfügbarkeit des AKs platziert"
#: AKModel/models.py:680 #: AKModel/models.py:1305
msgid "Room has two AK slots scheduled at the same time" msgid "Room has two AK slots scheduled at the same time"
msgstr "Raum hat zwei AK Slots gleichzeitig" msgstr "Raum hat zwei AK Slots gleichzeitig"
#: AKModel/models.py:681 #: AKModel/models.py:1306
msgid "Room does not satisfy the requirement of the scheduled AK" msgid "Room does not satisfy the requirement of the scheduled AK"
msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht" msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht"
#: AKModel/models.py:682 #: AKModel/models.py:1307
msgid "AK Slot is scheduled at the same time as an AK listed as a conflict" msgid "AK Slot is scheduled at the same time as an AK listed as a conflict"
msgstr "" msgstr ""
"AK Slot wurde wurde zur gleichen Zeit wie ein Konflikt des AKs platziert" "AK Slot wurde wurde zur gleichen Zeit wie ein Konflikt des AKs platziert"
#: AKModel/models.py:683 #: AKModel/models.py:1308
msgid "AK Slot is scheduled before an AK listed as a prerequisite" msgid "AK Slot is scheduled before an AK listed as a prerequisite"
msgstr "AK Slot wurde vor einem als Voraussetzung gelisteten AK platziert" msgstr "AK Slot wurde vor einem als Voraussetzung gelisteten AK platziert"
#: AKModel/models.py:685 #: AKModel/models.py:1310
msgid "" msgid ""
"AK Slot for AK with intention to submit a resolution is scheduled after " "AK Slot for AK with intention to submit a resolution is scheduled after "
"resolution deadline" "resolution deadline"
msgstr "" msgstr ""
"AK Slot eines AKs mit Resoabsicht wurde nach der Resodeadline platziert" "AK Slot eines AKs mit Resoabsicht wurde nach der Resodeadline platziert"
#: AKModel/models.py:686 #: AKModel/models.py:1311
msgid "AK Slot in a category is outside that categories availabilities" msgid "AK Slot in a category is outside that categories availabilities"
msgstr "AK Slot wurde außerhalb der Verfügbarkeiten seiner Kategorie" msgstr "AK Slot wurde außerhalb der Verfügbarkeiten seiner Kategorie"
#: AKModel/models.py:687 #: AKModel/models.py:1312
msgid "Two AK Slots for the same AK scheduled at the same time" msgid "Two AK Slots for the same AK scheduled at the same time"
msgstr "Zwei AK Slots eines AKs wurden zur selben Zeit platziert" msgstr "Zwei AK Slots eines AKs wurden zur selben Zeit platziert"
#: AKModel/models.py:688 #: AKModel/models.py:1313
msgid "Room does not have enough space for interest in scheduled AK Slot" msgid "Room does not have enough space for interest in scheduled AK Slot"
msgstr "Room hat nicht genug Platz für das Interesse am geplanten AK-Slot" msgstr "Room hat nicht genug Platz für das Interesse am geplanten AK-Slot"
#: AKModel/models.py:689 #: AKModel/models.py:1314
msgid "AK Slot is scheduled outside the event's availabilities" msgid "AK Slot is scheduled outside the event's availabilities"
msgstr "AK Slot wurde außerhalb der Verfügbarkeit des Events platziert" msgstr "AK Slot wurde außerhalb der Verfügbarkeit des Events platziert"
#: AKModel/models.py:695 #: AKModel/models.py:1320
msgid "Warning" msgid "Warning"
msgstr "Warnung" msgstr "Warnung"
#: AKModel/models.py:696 #: AKModel/models.py:1321
msgid "Violation" msgid "Violation"
msgstr "Verletzung" msgstr "Verletzung"
#: AKModel/models.py:698 #: AKModel/models.py:1323
msgid "Type" msgid "Type"
msgstr "Art" msgstr "Art"
#: AKModel/models.py:699 #: AKModel/models.py:1324
msgid "Type of violation, i.e. what kind of constraint was violated" msgid "Type of violation, i.e. what kind of constraint was violated"
msgstr "Art der Verletzung, gibt an welche Art Constraint verletzt wurde" msgstr "Art der Verletzung, gibt an welche Art Constraint verletzt wurde"
#: AKModel/models.py:700 #: AKModel/models.py:1325
msgid "Level" msgid "Level"
msgstr "Level" msgstr "Level"
#: AKModel/models.py:701 #: AKModel/models.py:1326
msgid "Severity level of the violation" msgid "Severity level of the violation"
msgstr "Schweregrad der Verletzung" msgstr "Schweregrad der Verletzung"
#: AKModel/models.py:708 #: AKModel/models.py:1333
msgid "AK(s) belonging to this constraint" msgid "AK(s) belonging to this constraint"
msgstr "AK(s), die zu diesem Constraint gehören" msgstr "AK(s), die zu diesem Constraint gehören"
#: AKModel/models.py:710 #: AKModel/models.py:1335
msgid "AK Slot(s) belonging to this constraint" msgid "AK Slot(s) belonging to this constraint"
msgstr "AK Slot(s), die zu diesem Constraint gehören" msgstr "AK Slot(s), die zu diesem Constraint gehören"
#: AKModel/models.py:712 #: AKModel/models.py:1337
msgid "AK Owner belonging to this constraint" msgid "AK Owner belonging to this constraint"
msgstr "AK Leitung(en), die zu diesem Constraint gehören" msgstr "AK Leitung(en), die zu diesem Constraint gehören"
#: AKModel/models.py:714 #: AKModel/models.py:1339
msgid "Room belonging to this constraint" msgid "Room belonging to this constraint"
msgstr "Raum, der zu diesem Constraint gehört" msgstr "Raum, der zu diesem Constraint gehört"
#: AKModel/models.py:717 #: AKModel/models.py:1342
msgid "AK Requirement belonging to this constraint" msgid "AK Requirement belonging to this constraint"
msgstr "AK Anforderung, die zu diesem Constraint gehört" msgstr "AK Anforderung, die zu diesem Constraint gehört"
#: AKModel/models.py:719 #: AKModel/models.py:1344
msgid "AK Category belonging to this constraint" msgid "AK Category belonging to this constraint"
msgstr "AK Kategorie, di zu diesem Constraint gehört" msgstr "AK Kategorie, di zu diesem Constraint gehört"
#: AKModel/models.py:721 #: AKModel/models.py:1346
msgid "Comment" msgid "Comment"
msgstr "Kommentar" msgstr "Kommentar"
#: AKModel/models.py:721 #: AKModel/models.py:1346
msgid "Comment or further details for this violation" msgid "Comment or further details for this violation"
msgstr "Kommentar oder weitere Details zu dieser Vereletzung" msgstr "Kommentar oder weitere Details zu dieser Vereletzung"
#: AKModel/models.py:724 #: AKModel/models.py:1349
msgid "Timestamp" msgid "Timestamp"
msgstr "Timestamp" msgstr "Timestamp"
#: AKModel/models.py:724 #: AKModel/models.py:1349
msgid "Time of creation" msgid "Time of creation"
msgstr "Zeitpunkt der ERstellung" msgstr "Zeitpunkt der ERstellung"
#: AKModel/models.py:725 #: AKModel/models.py:1350
msgid "Manually Resolved" msgid "Manually Resolved"
msgstr "Manuell behoben" msgstr "Manuell behoben"
#: AKModel/models.py:726 #: AKModel/models.py:1351
msgid "Mark this violation manually as resolved" msgid "Mark this violation manually as resolved"
msgstr "Markiere diese Verletzung manuell als behoben" msgstr "Markiere diese Verletzung manuell als behoben"
#: AKModel/models.py:753 AKModel/templates/admin/AKModel/aks_by_user.html:22 #: AKModel/models.py:1378 AKModel/templates/admin/AKModel/aks_by_user.html:22
#: AKModel/templates/admin/AKModel/requirements_overview.html:27 #: AKModel/templates/admin/AKModel/requirements_overview.html:27
msgid "Details" msgid "Details"
msgstr "Details" msgstr "Details"
#: AKModel/models.py:887 #: AKModel/models.py:1512
msgid "Default Slot" msgid "Default Slot"
msgstr "Standardslot" msgstr "Standardslot"
#: AKModel/models.py:892 #: AKModel/models.py:1517
msgid "Slot End" msgid "Slot End"
msgstr "Ende des Slots" msgstr "Ende des Slots"
#: AKModel/models.py:892 #: AKModel/models.py:1517
msgid "Time and date the slot ends" msgid "Time and date the slot ends"
msgstr "Zeit und Datum zu der der Slot endet" msgstr "Zeit und Datum zu der der Slot endet"
#: AKModel/models.py:897 #: AKModel/models.py:1522
msgid "Primary categories" msgid "Primary categories"
msgstr "Primäre Kategorien" msgstr "Primäre Kategorien"
#: AKModel/models.py:898 #: AKModel/models.py:1523
msgid "Categories that should be assigned to this slot primarily" msgid "Categories that should be assigned to this slot primarily"
msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen" msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen"
#: AKModel/site.py:14 #: AKModel/site.py:13 AKModel/site.py:14
msgid "Administration" msgid "Administration"
msgstr "Verwaltung" msgstr "Verwaltung"
...@@ -987,6 +1081,7 @@ msgid "Logout" ...@@ -987,6 +1081,7 @@ msgid "Logout"
msgstr "Ausloggen" msgstr "Ausloggen"
#: AKModel/templates/admin/AKModel/action_intermediate.html:23 #: AKModel/templates/admin/AKModel/action_intermediate.html:23
#: AKModel/templates/admin/AKModel/import_json.html:23
msgid "Confirm" msgid "Confirm"
msgstr "Bestätigen" msgstr "Bestätigen"
...@@ -994,6 +1089,7 @@ msgstr "Bestätigen" ...@@ -994,6 +1089,7 @@ msgstr "Bestätigen"
#: AKModel/templates/admin/AKModel/event_wizard/import.html:27 #: AKModel/templates/admin/AKModel/event_wizard/import.html:27
#: AKModel/templates/admin/AKModel/event_wizard/settings.html:32 #: AKModel/templates/admin/AKModel/event_wizard/settings.html:32
#: AKModel/templates/admin/AKModel/event_wizard/start.html:28 #: AKModel/templates/admin/AKModel/event_wizard/start.html:28
#: AKModel/templates/admin/AKModel/import_json.html:27
#: AKModel/templates/admin/AKModel/room_create.html:30 #: AKModel/templates/admin/AKModel/room_create.html:30
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
...@@ -1082,7 +1178,7 @@ msgid "No AKs with this requirement" ...@@ -1082,7 +1178,7 @@ msgid "No AKs with this requirement"
msgstr "Kein AK mit dieser Anforderung" msgstr "Kein AK mit dieser Anforderung"
#: AKModel/templates/admin/AKModel/requirements_overview.html:45 #: AKModel/templates/admin/AKModel/requirements_overview.html:45
#: AKModel/views/status.py:183 #: AKModel/views/status.py:191
msgid "Add Requirement" msgid "Add Requirement"
msgstr "Anforderung hinzufügen" msgstr "Anforderung hinzufügen"
...@@ -1168,43 +1264,47 @@ msgstr "Login" ...@@ -1168,43 +1264,47 @@ msgstr "Login"
msgid "Register" msgid "Register"
msgstr "Registrieren" msgstr "Registrieren"
#: AKModel/views/ak.py:17 #: AKModel/views/ak.py:19
msgid "Requirements for Event" msgid "Requirements for Event"
msgstr "Anforderungen für das Event" msgstr "Anforderungen für das Event"
#: AKModel/views/ak.py:34 #: AKModel/views/ak.py:36
msgid "AK CSV Export" msgid "AK CSV Export"
msgstr "AK-CSV-Export" msgstr "AK-CSV-Export"
#: AKModel/views/ak.py:48 #: AKModel/views/ak.py:49
msgid "AK JSON Export"
msgstr "AK-JSON-Export"
#: AKModel/views/ak.py:72
msgid "AK Wiki Export" msgid "AK Wiki Export"
msgstr "AK-Wiki-Export" msgstr "AK-Wiki-Export"
#: AKModel/views/ak.py:59 AKModel/views/manage.py:53 #: AKModel/views/ak.py:83 AKModel/views/manage.py:55
msgid "Wishes" msgid "Wishes"
msgstr "Wünsche" msgstr "Wünsche"
#: AKModel/views/ak.py:71 #: AKModel/views/ak.py:95
msgid "Delete AK Orga Messages" msgid "Delete AK Orga Messages"
msgstr "AK-Organachrichten löschen" msgstr "AK-Organachrichten löschen"
#: AKModel/views/ak.py:89 #: AKModel/views/ak.py:113
msgid "AK Orga Messages successfully deleted" msgid "AK Orga Messages successfully deleted"
msgstr "AK-Organachrichten erfolgreich gelöscht" msgstr "AK-Organachrichten erfolgreich gelöscht"
#: AKModel/views/ak.py:101 #: AKModel/views/ak.py:125
msgid "Interest of the following AKs will be set to not filled (-1):" msgid "Interest of the following AKs will be set to not filled (-1):"
msgstr "Interesse an den folgenden AKs wird auf nicht ausgefüllt (-1) gesetzt:" msgstr "Interesse an den folgenden AKs wird auf nicht ausgefüllt (-1) gesetzt:"
#: AKModel/views/ak.py:102 #: AKModel/views/ak.py:126
msgid "Reset of interest in AKs successful." msgid "Reset of interest in AKs successful."
msgstr "Interesse an AKs erfolgreich zurückgesetzt." msgstr "Interesse an AKs erfolgreich zurückgesetzt."
#: AKModel/views/ak.py:116 #: AKModel/views/ak.py:140
msgid "Interest counter of the following AKs will be set to 0:" msgid "Interest counter of the following AKs will be set to 0:"
msgstr "Interessensbekundungszähler der folgenden AKs wird auf 0 gesetzt:" msgstr "Interessensbekundungszähler der folgenden AKs wird auf 0 gesetzt:"
#: AKModel/views/ak.py:117 #: AKModel/views/ak.py:141
msgid "AKs' interest counters set back to 0." msgid "AKs' interest counters set back to 0."
msgstr "Interessenszähler der AKs zurückgesetzt" msgstr "Interessenszähler der AKs zurückgesetzt"
...@@ -1218,90 +1318,103 @@ msgstr "'%(obj)s' kopiert" ...@@ -1218,90 +1318,103 @@ msgstr "'%(obj)s' kopiert"
msgid "Could not copy '%(obj)s' (%(error)s)" msgid "Could not copy '%(obj)s' (%(error)s)"
msgstr "'%(obj)s' konnte nicht kopiert werden (%(error)s)" msgstr "'%(obj)s' konnte nicht kopiert werden (%(error)s)"
#: AKModel/views/manage.py:35 AKModel/views/status.py:150 #: AKModel/views/manage.py:37 AKModel/views/status.py:158
msgid "Export AK Slides" msgid "Export AK Slides"
msgstr "AK-Folien exportieren" msgstr "AK-Folien exportieren"
#: AKModel/views/manage.py:48 #: AKModel/views/manage.py:50
msgid "Symbols" msgid "Symbols"
msgstr "Symbole" msgstr "Symbole"
#: AKModel/views/manage.py:49 #: AKModel/views/manage.py:51
msgid "Who?" msgid "Who?"
msgstr "Wer?" msgstr "Wer?"
#: AKModel/views/manage.py:50 #: AKModel/views/manage.py:52
msgid "Duration(s)" msgid "Duration(s)"
msgstr "Dauer(n)" msgstr "Dauer(n)"
#: AKModel/views/manage.py:51 #: AKModel/views/manage.py:53
msgid "Reso intention?" msgid "Reso intention?"
msgstr "Resolutionsabsicht?" msgstr "Resolutionsabsicht?"
#: AKModel/views/manage.py:52 #: AKModel/views/manage.py:54
msgid "Category (for Wishes)" msgid "Category (for Wishes)"
msgstr "Kategorie (für Wünsche)" msgstr "Kategorie (für Wünsche)"
#: AKModel/views/manage.py:101 #: AKModel/views/manage.py:103
msgid "The following Constraint Violations will be marked as manually resolved" msgid "The following Constraint Violations will be marked as manually resolved"
msgstr "" msgstr ""
"Die folgenden Constraintverletzungen werden als manuell behoben markiert." "Die folgenden Constraintverletzungen werden als manuell behoben markiert."
#: AKModel/views/manage.py:102 #: AKModel/views/manage.py:104
msgid "Constraint Violations marked as resolved" msgid "Constraint Violations marked as resolved"
msgstr "Constraintverletzungen als manuell behoben markiert" msgstr "Constraintverletzungen als manuell behoben markiert"
#: AKModel/views/manage.py:114 #: AKModel/views/manage.py:116
msgid "The following Constraint Violations will be set to level 'violation'" msgid "The following Constraint Violations will be set to level 'violation'"
msgstr "" msgstr ""
"Die folgenden Constraintverletzungen werden auf das Level \"Violation\" " "Die folgenden Constraintverletzungen werden auf das Level \"Violation\" "
"gesetzt." "gesetzt."
#: AKModel/views/manage.py:115 #: AKModel/views/manage.py:117
msgid "Constraint Violations set to level 'violation'" msgid "Constraint Violations set to level 'violation'"
msgstr "Constraintverletzungen auf Level \"Violation\" gesetzt" msgstr "Constraintverletzungen auf Level \"Violation\" gesetzt"
#: AKModel/views/manage.py:127 #: AKModel/views/manage.py:129
msgid "The following Constraint Violations will be set to level 'warning'" msgid "The following Constraint Violations will be set to level 'warning'"
msgstr "" msgstr ""
"Die folgenden Constraintverletzungen werden auf das Level 'warning' gesetzt." "Die folgenden Constraintverletzungen werden auf das Level 'warning' gesetzt."
#: AKModel/views/manage.py:128 #: AKModel/views/manage.py:130
msgid "Constraint Violations set to level 'warning'" msgid "Constraint Violations set to level 'warning'"
msgstr "Constraintverletzungen auf Level \"Warning\" gesetzt" msgstr "Constraintverletzungen auf Level \"Warning\" gesetzt"
#: AKModel/views/manage.py:140 #: AKModel/views/manage.py:142
msgid "Publish the plan(s) of:" msgid "Publish the plan(s) of:"
msgstr "Den Plan/die Pläne veröffentlichen von:" msgstr "Den Plan/die Pläne veröffentlichen von:"
#: AKModel/views/manage.py:141 #: AKModel/views/manage.py:143
msgid "Plan published" msgid "Plan published"
msgstr "Plan veröffentlicht" msgstr "Plan veröffentlicht"
#: AKModel/views/manage.py:153 #: AKModel/views/manage.py:155
msgid "Unpublish the plan(s) of:" msgid "Unpublish the plan(s) of:"
msgstr "Den Plan/die Pläne verbergen von:" msgstr "Den Plan/die Pläne verbergen von:"
#: AKModel/views/manage.py:154 #: AKModel/views/manage.py:156
msgid "Plan unpublished" msgid "Plan unpublished"
msgstr "Plan verborgen" msgstr "Plan verborgen"
#: AKModel/views/manage.py:166 AKModel/views/status.py:134 #: AKModel/views/manage.py:168 AKModel/views/status.py:134
msgid "Edit Default Slots" msgid "Edit Default Slots"
msgstr "Standardslots bearbeiten" msgstr "Standardslots bearbeiten"
#: AKModel/views/manage.py:204 #: AKModel/views/manage.py:206
#, python-brace-format #, python-brace-format
msgid "Could not update slot {id} since it does not belong to {event}" msgid "Could not update slot {id} since it does not belong to {event}"
msgstr "" msgstr ""
"Konnte Slot {id} nicht bearbeiten, da er nicht zum Event {event} gehört" "Konnte Slot {id} nicht bearbeiten, da er nicht zum Event {event} gehört"
#: AKModel/views/manage.py:235 #: AKModel/views/manage.py:237
#, python-brace-format #, python-brace-format
msgid "Updated {u} slot(s). created {c} new slot(s) and deleted {d} slot(s)" msgid "Updated {u} slot(s). created {c} new slot(s) and deleted {d} slot(s)"
msgstr "" msgstr ""
"{u} Slot(s) aktualisiert, {c} Slot(s) hinzugefügt und {d} Slot(s) gelöscht" "{u} Slot(s) aktualisiert, {c} Slot(s) hinzugefügt und {d} Slot(s) gelöscht"
#: AKModel/views/manage.py:258
msgid "AK Schedule JSON Import"
msgstr "AK-Plan JSON-Import"
#: AKModel/views/manage.py:274
#, python-brace-format
msgid "Successfully imported {n} slot(s)"
msgstr "Erfolgreich {n} Slot(s) importiert"
#: AKModel/views/manage.py:280
msgid "Importing an AK schedule failed! Reason: "
msgstr "AK-Plan importieren fehlgeschlagen! Grund: "
#: AKModel/views/room.py:37 #: AKModel/views/room.py:37
#, python-format #, python-format
msgid "Created Room '%(room)s'" msgid "Created Room '%(room)s'"
...@@ -1354,18 +1467,26 @@ msgid "Manage ak tracks" ...@@ -1354,18 +1467,26 @@ msgid "Manage ak tracks"
msgstr "AK-Tracks verwalten" msgstr "AK-Tracks verwalten"
#: AKModel/views/status.py:142 #: AKModel/views/status.py:142
msgid "Import AK schedule from JSON"
msgstr "AK-Plan aus JSON importieren"
#: AKModel/views/status.py:146
msgid "Export AKs as CSV" msgid "Export AKs as CSV"
msgstr "AKs als CSV exportieren" msgstr "AKs als CSV exportieren"
#: AKModel/views/status.py:146 #: AKModel/views/status.py:150
msgid "Export AKs as JSON"
msgstr "AKs als JSON exportieren"
#: AKModel/views/status.py:154
msgid "Export AKs for Wiki" msgid "Export AKs for Wiki"
msgstr "AKs im Wiki-Format exportieren" msgstr "AKs im Wiki-Format exportieren"
#: AKModel/views/status.py:179 #: AKModel/views/status.py:187
msgid "Show AKs for requirements" msgid "Show AKs for requirements"
msgstr "Zu Anforderungen gehörige AKs anzeigen" msgstr "Zu Anforderungen gehörige AKs anzeigen"
#: AKModel/views/status.py:193 #: AKModel/views/status.py:201
msgid "Event Status" msgid "Event Status"
msgstr "Eventstatus" msgstr "Eventstatus"
......
# Generated by Django 4.2.13 on 2025-02-06 16:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("AKModel", "0060_orga_message_resolved"),
]
operations = [
migrations.AddField(
model_name="event",
name="export_slot",
field=models.DecimalField(
decimal_places=2,
default=1,
help_text="Slot duration in hours that is used in the timeslot discretization, when this event is exported for the solver.",
max_digits=4,
verbose_name="Export Slot Length",
),
),
]
# Generated by Django 4.2.13 on 2025-02-27 18:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("AKModel", "0061_event_export_slot"),
("AKModel", "0062_interest_no_history"),
]
operations = []
import decimal
import itertools import itertools
from datetime import timedelta import json
import math
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any, Iterable, Generator
from django.db import models from django.db import models, transaction
from django.apps import apps from django.apps import apps
from django.db.models import Count from django.db.models import Count
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.datetime_safe import datetime
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
from timezone_field import TimeZoneField from timezone_field import TimeZoneField
@dataclass
class OptimizerTimeslot:
"""Class describing a discrete timeslot. Used to interface with an optimizer."""
avail: "Availability"
"""The availability object corresponding to this timeslot."""
idx: int
"""The unique index of this optimizer timeslot."""
constraints: set[str]
"""The set of time constraints fulfilled by this object."""
def merge(self, other: "OptimizerTimeslot") -> "OptimizerTimeslot":
"""Merge with other OptimizerTimeslot.
Creates a new OptimizerTimeslot object.
Its availability is constructed by merging the availabilities of self and other,
its constraints by taking the union of both constraint sets.
As an index, the index of self is used.
"""
avail = self.avail.merge_with(other.avail)
constraints = self.constraints.union(other.constraints)
return OptimizerTimeslot(
avail=avail, idx=self.idx, constraints=constraints
)
def __repr__(self) -> str:
return f"({self.avail.simplified}, {self.idx}, {self.constraints})"
TimeslotBlock = list[OptimizerTimeslot]
def merge_blocks(
blocks: Iterable[TimeslotBlock]
) -> Iterable[TimeslotBlock]:
"""Merge iterable of blocks together.
The timeslots of all blocks are grouped into maximal blocks.
Timeslots with the same start and end are identified with each other
and merged (cf `OptimizerTimeslot.merge`).
Throws a ValueError if any timeslots are overlapping but do not
share the same start and end, i.e. partial overlap is not allowed.
:param blocks: iterable of blocks to merge.
:return: iterable of merged blocks.
:rtype: iterable over lists of OptimizerTimeslot objects
"""
if not blocks:
return []
# flatten timeslot iterables to single chain
timeslot_chain = itertools.chain.from_iterable(blocks)
# sort timeslots according to start
timeslots = sorted(
timeslot_chain,
key=lambda slot: slot.avail.start
)
if not timeslots:
return []
all_blocks = []
current_block = [timeslots[0]]
timeslots = timeslots[1:]
for slot in timeslots:
if current_block and slot.avail.overlaps(current_block[-1].avail, strict=True):
if (
slot.avail.start == current_block[-1].avail.start
and slot.avail.end == current_block[-1].avail.end
):
# the same timeslot -> merge
current_block[-1] = current_block[-1].merge(slot)
else:
# partial overlap of interiors -> not supported
# TODO: Show comprehensive message in production
raise ValueError(
"Partially overlapping timeslots are not supported!"
f" ({current_block[-1].avail.simplified}, {slot.avail.simplified})"
)
elif not current_block or slot.avail.overlaps(current_block[-1].avail, strict=False):
# only endpoints in intersection -> same block
current_block.append(slot)
else:
# no overlap at all -> new block
all_blocks.append(current_block)
current_block = [slot]
if current_block:
all_blocks.append(current_block)
return all_blocks
class Event(models.Model): class Event(models.Model):
""" """
An event supplies the frame for all Aks. An event supplies the frame for all Aks.
...@@ -51,6 +151,12 @@ class Event(models.Model): ...@@ -51,6 +151,12 @@ class Event(models.Model):
wiki_export_template_name = models.CharField(verbose_name=_("Wiki Export Template Name"), blank=True, max_length=50) wiki_export_template_name = models.CharField(verbose_name=_("Wiki Export Template Name"), blank=True, max_length=50)
default_slot = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Default Slot Length'), default_slot = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Default Slot Length'),
help_text=_('Default length in hours that is assumed for AKs in this event.')) help_text=_('Default length in hours that is assumed for AKs in this event.'))
export_slot = models.DecimalField(max_digits=4, decimal_places=2, default=1, verbose_name=_('Export Slot Length'),
help_text=_(
'Slot duration in hours that is used in the timeslot discretization, '
'when this event is exported for the solver.'
))
contact_email = models.EmailField(verbose_name=_("Contact email address"), blank=True, contact_email = models.EmailField(verbose_name=_("Contact email address"), blank=True,
help_text=_("An email address that is displayed on every page " help_text=_("An email address that is displayed on every page "
...@@ -162,6 +268,386 @@ class Event(models.Model): ...@@ -162,6 +268,386 @@ class Event(models.Model):
.filter(availabilities__count=0, owners__count__gt=0) .filter(availabilities__count=0, owners__count__gt=0)
) )
def _generate_slots_from_block(
self,
start: datetime,
end: datetime,
slot_duration: timedelta,
*,
slot_index: int = 0,
constraints: set[str] | None = None,
) -> Generator[TimeslotBlock, None, int]:
"""Discretize a time range into timeslots.
Uses a uniform discretization into discrete slots of length `slot_duration`,
starting at `start`. No incomplete timeslots are generated, i.e.
if (`end` - `start`) is not a whole number multiple of `slot_duration`
then the last incomplete timeslot is dropped.
:param start: Start of the time range.
:param end: Start of the time range.
:param slot_duration: Duration of a single timeslot in the discretization.
:param slot_index: index of the first timeslot. Defaults to 0.
:yield: Block of optimizer timeslots as the discretization result.
:ytype: list of OptimizerTimeslot
:return: The first slot index after the yielded blocks, i.e.
`slot_index` + total # generated timeslots
:rtype: int
"""
# local import to prevent cyclic import
# pylint: disable=import-outside-toplevel
from AKModel.availability.models import Availability
current_slot_start = start
previous_slot_start: datetime | None = None
if constraints is None:
constraints = set()
current_block = []
room_availabilities = list({
availability
for room in Room.objects.filter(event=self)
for availability in room.availabilities.all()
})
while current_slot_start + slot_duration <= end:
slot = Availability(
event=self,
start=current_slot_start,
end=current_slot_start + slot_duration,
)
if any((availability.contains(slot) for availability in room_availabilities)):
# no gap in a block
if (
previous_slot_start is not None
and previous_slot_start + slot_duration < current_slot_start
):
yield current_block
current_block = []
current_block.append(
OptimizerTimeslot(avail=slot, idx=slot_index, constraints=constraints)
)
previous_slot_start = current_slot_start
slot_index += 1
current_slot_start += slot_duration
if current_block:
yield current_block
return slot_index
def uniform_time_slots(self, *, slots_in_an_hour: float) -> Iterable[TimeslotBlock]:
"""Uniformly discretize the entire event into blocks of timeslots.
Discretizes entire event uniformly. May not necessarily result in a single block
as slots with no room availability are dropped.
:param slots_in_an_hour: The percentage of an hour covered by a single slot.
Determines the discretization granularity.
:yield: Block of optimizer timeslots as the discretization result.
:ytype: list of OptimizerTimeslot
"""
all_category_constraints = AKCategory.create_category_constraints(
AKCategory.objects.filter(event=self).all()
)
yield from self._generate_slots_from_block(
start=self.start,
end=self.end,
slot_duration=timedelta(hours=1.0 / slots_in_an_hour),
constraints=all_category_constraints,
)
def default_time_slots(self, *, slots_in_an_hour: float) -> Iterable[TimeslotBlock]:
"""Discretize all default slots into blocks of timeslots.
In the discretization each default slot corresponds to one block.
:param slots_in_an_hour: The percentage of an hour covered by a single slot.
Determines the discretization granularity.
:yield: Block of optimizer timeslots as the discretization result.
:ytype: list of TimeslotBlock
"""
slot_duration = timedelta(hours=1.0 / slots_in_an_hour)
slot_index = 0
for block_slot in DefaultSlot.objects.filter(event=self).order_by("start", "end"):
category_constraints = AKCategory.create_category_constraints(
block_slot.primary_categories.all()
)
slot_index = yield from self._generate_slots_from_block(
start=block_slot.start,
end=block_slot.end,
slot_duration=slot_duration,
slot_index=slot_index,
constraints=category_constraints,
)
def discretize_timeslots(self, *, slots_in_an_hour: float | None = None) -> Iterable[TimeslotBlock]:
""""Choose discretization scheme.
Uses default_time_slots if the event has any DefaultSlot, otherwise uniform_time_slots.
:param slots_in_an_hour: The percentage of an hour covered by a single slot.
Determines the discretization granularity.
:yield: Block of optimizer timeslots as the discretization result.
:ytype: list of TimeslotBlock
"""
if slots_in_an_hour is None:
slots_in_an_hour = 1.0 / float(self.export_slot)
if DefaultSlot.objects.filter(event=self).exists():
# discretize default slots if they exists
yield from merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour))
else:
yield from self.uniform_time_slots(slots_in_an_hour=slots_in_an_hour)
@transaction.atomic
def schedule_from_json(
self, schedule: str | dict[str, Any], *, check_for_data_inconsistency: bool = True
) -> int:
"""Load AK schedule from a json string.
:param schedule: A string that can be decoded to json, describing
the AK schedule. The json data is assumed to be constructed
following the output specification of the KoMa conference optimizer, cf.
https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
"""
if isinstance(schedule, str):
schedule = json.loads(schedule)
export_dict = self.as_json_dict()
if "input" not in schedule or "scheduled_aks" not in schedule:
raise ValueError(_("Cannot parse malformed JSON input."))
if check_for_data_inconsistency and schedule["input"] != export_dict:
raise ValueError(_("Data has changed since the export. Reexport and run the solver again."))
slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"]
timeslot_dict = {
timeslot.idx: timeslot
for block in self.discretize_timeslots(slots_in_an_hour=slots_in_an_hour)
for timeslot in block
}
slots_updated = 0
for scheduled_slot in schedule["scheduled_aks"]:
scheduled_slot["timeslot_ids"] = list(map(int, scheduled_slot["timeslot_ids"]))
slot = AKSlot.objects.get(id=int(scheduled_slot["ak_id"]))
if not scheduled_slot["timeslot_ids"]:
raise ValueError(
_("AK {ak_name} is not assigned any timeslot by the solver").format(ak_name=slot.ak.name)
)
start_timeslot = timeslot_dict[min(scheduled_slot["timeslot_ids"])].avail
end_timeslot = timeslot_dict[max(scheduled_slot["timeslot_ids"])].avail
solver_duration = (end_timeslot.end - start_timeslot.start).total_seconds() / 3600.0
if solver_duration + 2e-4 < slot.duration:
raise ValueError(
_(
"Duration of AK {ak_name} assigned by solver ({solver_duration} hours) "
"is less than the duration required by the slot ({slot_duration} hours)"
).format(
ak_name=slot.ak.name,
solver_duration=solver_duration,
slot_duration=slot.duration,
)
)
if slot.fixed:
solver_room = Room.objects.get(id=int(scheduled_slot["room_id"]))
if slot.room != solver_room:
raise ValueError(
_(
"Fixed AK {ak_name} assigned by solver to room {solver_room} "
"is fixed to room {slot_room}"
).format(
ak_name=slot.ak.name,
solver_room=solver_room.name,
slot_room=slot.room.name,
)
)
if slot.start != start_timeslot.start:
raise ValueError(
_(
"Fixed AK {ak_name} assigned by solver to start at {solver_start} "
"is fixed to start at {slot_start}"
).format(
ak_name=slot.ak.name,
solver_start=start_timeslot.start,
slot_start=slot.start,
)
)
else:
slot.room = Room.objects.get(id=int(scheduled_slot["room_id"]))
slot.start = start_timeslot.start
slot.save()
slots_updated += 1
return slots_updated
def as_json_dict(self) -> dict[str, Any]:
"""Return the json representation of this Event.
:return: The json dict representation is constructed
following the input specification of the KoMa conference optimizer, cf.
https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
:rtype: dict[str, Any]
"""
# local import to prevent cyclic import
# pylint: disable=import-outside-toplevel
from AKModel.availability.models import Availability
def _test_event_not_covered(availabilities: list[Availability]) -> bool:
"""Test if event is not covered by availabilities."""
return not Availability.is_event_covered(self, availabilities)
def _test_akslot_fixed_in_timeslot(ak_slot: AKSlot, timeslot: Availability) -> bool:
"""Test if an AKSlot is fixed to overlap a timeslot slot."""
if not ak_slot.fixed or ak_slot.start is None:
return False
fixed_avail = Availability(event=self, start=ak_slot.start, end=ak_slot.end)
return fixed_avail.overlaps(timeslot, strict=True)
def _test_add_constraint(slot: Availability, availabilities: list[Availability]) -> bool:
"""Test if object is not available for whole event and may happen during slot."""
return (
_test_event_not_covered(availabilities) and slot.is_covered(availabilities)
)
def _generate_time_constraints(
avail_label: str,
avail_dict: dict,
timeslot_avail: Availability,
prefix: str = "availability",
) -> list[str]:
return [
f"{prefix}-{avail_label}-{pk}"
for pk, availabilities in avail_dict.items()
if _test_add_constraint(timeslot_avail, availabilities)
]
timeslots = {
"info": {"duration": float(self.export_slot)},
"blocks": [],
}
rooms = Room.objects.filter(event=self).order_by()
slots = AKSlot.objects.filter(event=self).order_by()
ak_availabilities = {
ak.pk: Availability.union(ak.availabilities.all())
for ak in AK.objects.filter(event=self).all()
}
room_availabilities = {
room.pk: Availability.union(room.availabilities.all())
for room in rooms
}
person_availabilities = {
person.pk: Availability.union(person.availabilities.all())
for person in AKOwner.objects.filter(event=self)
}
blocks = list(self.discretize_timeslots())
block_names = []
for block_idx, block in enumerate(blocks):
current_block = []
if not block:
continue
block_start = block[0].avail.start.astimezone(self.timezone)
block_end = block[-1].avail.end.astimezone(self.timezone)
start_day = block_start.strftime("%A, %d. %b")
if block_start.date() == block_end.date():
# same day
time_str = block_start.strftime("%H:%M") + "" + block_end.strftime("%H:%M")
else:
# different days
time_str = block_start.strftime("%a %H:%M") + "" + block_end.strftime("%a %H:%M")
block_names.append([start_day, time_str])
block_timeconstraints = [f"notblock{idx}" for idx in range(len(blocks)) if idx != block_idx]
for timeslot in block:
time_constraints = []
# if reso_deadline is set and timeslot ends before it,
# add fulfilled time constraint 'resolution'
if self.reso_deadline is None or timeslot.avail.end < self.reso_deadline:
time_constraints.append("resolution")
# add fulfilled time constraints for all AKs that cannot happen during full event
time_constraints.extend(
_generate_time_constraints("ak", ak_availabilities, timeslot.avail)
)
# add fulfilled time constraints for all persons that are not available for full event
time_constraints.extend(
_generate_time_constraints("person", person_availabilities, timeslot.avail)
)
# add fulfilled time constraints for all rooms that are not available for full event
time_constraints.extend(
_generate_time_constraints("room", room_availabilities, timeslot.avail)
)
# add fulfilled time constraints for all AKSlots fixed to happen during timeslot
time_constraints.extend([
f"fixed-akslot-{slot.id}"
for slot in AKSlot.objects.filter(event=self, fixed=True).exclude(start__isnull=True)
if _test_akslot_fixed_in_timeslot(slot, timeslot.avail)
])
time_constraints.extend(timeslot.constraints)
time_constraints.extend(block_timeconstraints)
current_block.append({
"id": timeslot.idx,
"info": {
"start": timeslot.avail.start.astimezone(self.timezone).strftime("%Y-%m-%d %H:%M"),
"end": timeslot.avail.end.astimezone(self.timezone).strftime("%Y-%m-%d %H:%M"),
},
"fulfilled_time_constraints": sorted(time_constraints),
})
timeslots["blocks"].append(current_block)
timeslots["info"]["blocknames"] = block_names
info_dict = {
"title": self.name,
"slug": self.slug
}
for attr in ["contact_email", "place"]:
if hasattr(self, attr) and getattr(self, attr):
info_dict[attr] = getattr(self, attr)
return {
"participants": [],
"rooms": [r.as_json_dict() for r in rooms],
"timeslots": timeslots,
"info": info_dict,
"aks": [ak.as_json_dict() for ak in slots],
}
class AKOwner(models.Model): class AKOwner(models.Model):
""" An AKOwner describes the person organizing/holding an AK. """ An AKOwner describes the person organizing/holding an AK.
...@@ -260,6 +746,20 @@ class AKCategory(models.Model): ...@@ -260,6 +746,20 @@ class AKCategory(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
@staticmethod
def create_category_constraints(categories: Iterable["AKCategory"]) -> set[str]:
"""Create a set of constraint strings from an AKCategory iterable.
:param categories: The iterable of categories to derive the constraint strings from.
:return: A set of category constraint strings, i.e. strings of the form
'availability-cat-<cat.name>'.
:rtype: set of strings.
"""
return {
f"availability-cat-{cat.name}"
for cat in categories
}
class AKTrack(models.Model): class AKTrack(models.Model):
""" An AKTrack describes a set of semantically related AKs. """ An AKTrack describes a set of semantically related AKs.
...@@ -400,7 +900,7 @@ class AK(models.Model): ...@@ -400,7 +900,7 @@ class AK(models.Model):
availabilities = ', \n'.join(f'{a.simplified}' for a in Availability.objects.select_related('event') availabilities = ', \n'.join(f'{a.simplified}' for a in Availability.objects.select_related('event')
.filter(ak=self)) .filter(ak=self))
detail_string = f"""{self.name}{" (R)" if self.reso else ""}: detail_string = f"""{self.name}{" (R)" if self.reso else ""}:
{self.owners_list} {self.owners_list}
{_('Interest')}: {self.interest}""" {_('Interest')}: {self.interest}"""
...@@ -435,7 +935,7 @@ class AK(models.Model): ...@@ -435,7 +935,7 @@ class AK(models.Model):
Get a list of stringified representations of all owners Get a list of stringified representations of all owners
:return: list of owners :return: list of owners
:rtype: List[str] :rtype: list[str]
""" """
return ", ".join(str(owner) for owner in self.owners.all()) return ", ".join(str(owner) for owner in self.owners.all())
...@@ -445,7 +945,7 @@ class AK(models.Model): ...@@ -445,7 +945,7 @@ class AK(models.Model):
Get a list of stringified representations of all durations of associated slots Get a list of stringified representations of all durations of associated slots
:return: list of durations :return: list of durations
:rtype: List[str] :rtype: list[str]
""" """
return ", ".join(str(slot.duration_simplified) for slot in self.akslot_set.select_related('event').all()) return ", ".join(str(slot.duration_simplified) for slot in self.akslot_set.select_related('event').all())
...@@ -550,6 +1050,44 @@ class Room(models.Model): ...@@ -550,6 +1050,44 @@ class Room(models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
def as_json_dict(self) -> dict[str, Any]:
"""Return a json representation of this room object.
:return: The json dict representation is constructed
following the input specification of the KoMa conference optimizer, cf.
https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
:rtype: dict[str, Any]
"""
# local import to prevent cyclic import
# pylint: disable=import-outside-toplevel
from AKModel.availability.models import Availability
# check if room is available for the whole event
# -> no time constraint needs to be introduced
if Availability.is_event_covered(self.event, self.availabilities.all()):
time_constraints = []
else:
time_constraints = [f"availability-room-{self.pk}"]
data = {
"id": self.pk,
"info": {
"name": self.name,
},
"capacity": self.capacity,
"fulfilled_room_constraints": [constraint.name
for constraint in self.properties.all()],
"time_constraints": time_constraints
}
data["fulfilled_room_constraints"].append(f"fixed-room-{self.pk}")
if not any(constr.startswith("proxy") for constr in data["fulfilled_room_constraints"]):
data["fulfilled_room_constraints"].append("no-proxy")
data["fulfilled_room_constraints"].sort()
return data
class AKSlot(models.Model): class AKSlot(models.Model):
""" An AK Mapping matches an AK to a room during a certain time. """ An AK Mapping matches an AK to a room during a certain time.
...@@ -645,6 +1183,84 @@ class AKSlot(models.Model): ...@@ -645,6 +1183,84 @@ class AKSlot(models.Model):
self.duration = min(self.duration, event_duration_hours) self.duration = min(self.duration, event_duration_hours)
super().save(force_insert, force_update, using, update_fields) super().save(force_insert, force_update, using, update_fields)
def as_json_dict(self) -> dict[str, Any]:
"""Return a json representation of the AK object of this slot.
:return: The json dict representation is constructed
following the input specification of the KoMa conference optimizer, cf.
https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
:rtype: dict[str, Any]
"""
# local import to prevent cyclic import
# pylint: disable=import-outside-toplevel
from AKModel.availability.models import Availability
# check if ak resp. owner is available for the whole event
# -> no time constraint needs to be introduced
if self.fixed and self.start is not None:
ak_time_constraints = [f"fixed-akslot-{self.id}"]
elif not Availability.is_event_covered(self.event, self.ak.availabilities.all()):
ak_time_constraints = [f"availability-ak-{self.ak.pk}"]
else:
ak_time_constraints = []
def _owner_time_constraints(owner: AKOwner):
owner_avails = owner.availabilities.all()
if not owner_avails or Availability.is_event_covered(self.event, owner_avails):
return []
return [f"availability-person-{owner.pk}"]
conflict_slots = AKSlot.objects.filter(ak__in=self.ak.conflicts.all())
dependency_slots = AKSlot.objects.filter(ak__in=self.ak.prerequisites.all())
other_ak_slots = AKSlot.objects.filter(ak=self.ak).exclude(pk=self.pk)
ceil_offet_eps = decimal.Decimal(1e-4)
data = {
"id": self.pk,
"duration": math.ceil(self.duration / self.event.export_slot - ceil_offet_eps),
"properties": {
"conflicts":
sorted(
[conflict.pk for conflict in conflict_slots.all()]
+ [second_slot.pk for second_slot in other_ak_slots.all()]
),
"dependencies": sorted([dep.pk for dep in dependency_slots.all()]),
},
"room_constraints": [constraint.name
for constraint in self.ak.requirements.all()],
"time_constraints": ["resolution"] if self.ak.reso else [],
"info": {
"name": self.ak.name,
"head": ", ".join([str(owner)
for owner in self.ak.owners.all()]),
"description": self.ak.description,
"reso": self.ak.reso,
"duration_in_hours": float(self.duration),
"django_ak_id": self.ak.pk,
"types": list(self.ak.types.values_list("name", flat=True).order_by()),
},
}
data["time_constraints"].extend(ak_time_constraints)
for owner in self.ak.owners.all():
data["time_constraints"].extend(_owner_time_constraints(owner))
if self.ak.category:
category_constraints = AKCategory.create_category_constraints([self.ak.category])
data["time_constraints"].extend(category_constraints)
if self.fixed and self.room is not None:
data["room_constraints"].append(f"fixed-room-{self.room.pk}")
if not any(constr.startswith("proxy") for constr in data["room_constraints"]):
data["room_constraints"].append("no-proxy")
data["room_constraints"].sort()
data["time_constraints"].sort()
return data
class AKOrgaMessage(models.Model): class AKOrgaMessage(models.Model):
""" """
......
{% extends "admin/base_site.html" %}
{% load tz %}
{% block content %}
<p>
Exported JSON:
<pre>
{{ json_data_oneline }}
</pre>
</p>
<p>
Exported JSON (indented for better readability):
<pre>
{{ json_data }}
</pre>
</p>
{% endblock %}
{% extends "admin/base_site.html" %}
{% load tags_AKModel %}
{% load i18n %}
{% load django_bootstrap5 %}
{% load fontawesome_6 %}
{% block title %}{{event}}: {{ title }}{% endblock %}
{% block content %}
{% block action_preview %}
<p>
{{ preview|linebreaksbr }}
</p>
{% endblock %}
<form enctype="multipart/form-data" method="post">{% csrf_token %}
{% bootstrap_form form %}
<div class="float-end">
<button type="submit" class="save btn btn-success" value="Submit">
{% fa6_icon "check" 'fas' %} {% trans "Confirm" %}
</button>
</div>
<a href="javascript:history.back()" class="btn btn-info">
{% fa6_icon "times" 'fas' %} {% trans "Cancel" %}
</a>
</form>
{% endblock %}
import json
import math
from collections import defaultdict
from collections.abc import Iterable
from datetime import datetime, timedelta
from itertools import chain
from bs4 import BeautifulSoup
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from jsonschema.exceptions import best_match
from AKModel.availability.models import Availability
from AKModel.models import AK, AKCategory, AKOwner, AKSlot, DefaultSlot, Event, Room
from AKModel.utils import construct_schema_validator
class JSONExportTest(TestCase):
"""Test if JSON export is correct.
It tests if the output conforms to the KoMa specification:
https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
"""
fixtures = ["model.json"]
@classmethod
def setUpTestData(cls):
"""Shared set up by initializing admin user."""
cls.admin_user = get_user_model().objects.create(
username="Test Admin User",
email="testadmin@example.com",
password="adminpw",
is_staff=True,
is_superuser=True,
is_active=True,
)
cls.json_export_validator = construct_schema_validator(
"solver-input.schema.json"
)
def setUp(self):
self.client.force_login(self.admin_user)
self.export_dict = {}
self.export_objects = {
"aks": {},
"rooms": {},
"participants": {},
}
self.ak_slots: Iterable[AKSlot] = []
self.rooms: Iterable[Room] = []
self.slots_in_an_hour: float = 1.0
self.event: Event | None = None
def set_up_event(self, event: Event) -> None:
"""Set up by retrieving json export and initializing data."""
export_url = reverse("admin:ak_json_export", kwargs={"event_slug": event.slug})
response = self.client.get(export_url)
self.assertEqual(response.status_code, 200, "Export not working at all")
soup = BeautifulSoup(response.content, features="lxml")
self.export_dict = json.loads(soup.find("pre").string)
self.export_objects["aks"] = {ak["id"]: ak for ak in self.export_dict["aks"]}
self.export_objects["rooms"] = {
room["id"]: room for room in self.export_dict["rooms"]
}
self.export_objects["participants"] = {
participant["id"]: participant
for participant in self.export_dict["participants"]
}
self.ak_slots = (
AKSlot.objects.filter(event__slug=event.slug)
.select_related("ak")
.prefetch_related("ak__conflicts")
.prefetch_related("ak__prerequisites")
.all()
)
self.rooms = Room.objects.filter(event__slug=event.slug).all()
self.slots_in_an_hour = 1 / self.export_dict["timeslots"]["info"]["duration"]
self.event = event
def test_all_aks_exported(self):
"""Test if exported AKs match AKSlots of Event."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
self.assertEqual(
{slot.pk for slot in self.ak_slots},
self.export_objects["aks"].keys(),
"Exported AKs does not match the AKSlots of the event",
)
def _check_uniqueness(self, lst, name: str, key: str | None = "id"):
if key is not None:
lst = [entry[key] for entry in lst]
self.assertEqual(len(lst), len(set(lst)), f"{name} IDs not unique!")
def _check_type(self, attr, cls, name: str, item: str) -> None:
self.assertTrue(isinstance(attr, cls), f"{item} {name} not a {cls}")
def _check_lst(
self, lst: list[str], name: str, item: str, contained_type=str
) -> None:
self.assertTrue(isinstance(lst, list), f"{item} {name} not a list")
self.assertTrue(
all(isinstance(c, contained_type) for c in lst),
f"{item} has non-{contained_type} {name}",
)
if contained_type in {str, int}:
self._check_uniqueness(lst, name, key=None)
def test_conformity_to_schema(self):
"""Test if JSON structure and types conform to schema."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
error = best_match(
self.json_export_validator.iter_errors(self.export_dict)
)
msg = "" if error is None else error.message
self.assertTrue(error is None, msg)
def test_id_uniqueness(self):
"""Test if objects are only exported once."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
self._check_uniqueness(self.export_dict["aks"], "AKs")
self._check_uniqueness(self.export_dict["rooms"], "Rooms")
self._check_uniqueness(self.export_dict["participants"], "Participants")
self._check_uniqueness(
chain.from_iterable(self.export_dict["timeslots"]["blocks"]),
"Timeslots",
)
def test_timeslot_ids_consecutive(self):
"""Test if Timeslots ids are chronologically consecutive."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
prev_id = None
for timeslot in chain.from_iterable(
self.export_dict["timeslots"]["blocks"]
):
if prev_id is not None:
self.assertLess(
prev_id,
timeslot["id"],
"timeslot ids must be increasing",
)
prev_id = timeslot["id"]
def test_general_conformity_to_spec(self):
"""Test if rest of JSON structure and types conform to standard."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
self.assertEqual(
self.export_dict["participants"],
[],
"Empty participant list expected",
)
info_keys = {"title": "name", "slug": "slug"}
for attr in ["contact_email", "place"]:
if hasattr(self.event, attr) and getattr(self.event, attr):
info_keys[attr] = attr
self.assertEqual(
self.export_dict["info"].keys(),
info_keys.keys(),
"info keys not as expected",
)
for attr, attr_field in info_keys.items():
self.assertEqual(
getattr(self.event, attr_field), self.export_dict["info"][attr]
)
def test_ak_durations(self):
"""Test if all AK durations are correct."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
for slot in self.ak_slots:
ak = self.export_objects["aks"][slot.pk]
self.assertLessEqual(
float(slot.duration) * self.slots_in_an_hour - 1e-4,
ak["duration"],
"Slot duration is too short",
)
self.assertEqual(
math.ceil(float(slot.duration) * self.slots_in_an_hour - 1e-4),
ak["duration"],
"Slot duration is wrong",
)
self.assertEqual(
float(slot.duration),
ak["info"]["duration_in_hours"],
"Slot duration_in_hours is wrong",
)
def test_ak_conflicts(self):
"""Test if all AK conflicts are correct."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
for slot in self.ak_slots:
ak = self.export_objects["aks"][slot.pk]
conflict_slots = set(
self.ak_slots.filter(
ak__in=slot.ak.conflicts.all()
).values_list("pk", flat=True)
)
other_ak_slots = (
self.ak_slots.filter(ak=slot.ak)
.exclude(pk=slot.pk)
.values_list("pk", flat=True)
)
conflict_slots.update(other_ak_slots)
self.assertEqual(
conflict_slots,
set(ak["properties"]["conflicts"]),
f"Conflicts for slot {slot.pk} not as expected",
)
def test_ak_depenedencies(self):
"""Test if all AK dependencies are correct."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
for slot in self.ak_slots:
ak = self.export_objects["aks"][slot.pk]
dependency_slots = self.ak_slots.filter(
ak__in=slot.ak.prerequisites.all()
).values_list("pk", flat=True)
self.assertEqual(
set(dependency_slots),
set(ak["properties"]["dependencies"]),
f"Dependencies for slot {slot.pk} not as expected",
)
def test_ak_reso(self):
"""Test if resolution intent of AKs is correctly exported."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
for slot in self.ak_slots:
ak = self.export_objects["aks"][slot.pk]
self.assertEqual(slot.ak.reso, ak["info"]["reso"])
self.assertEqual(
slot.ak.reso, "resolution" in ak["time_constraints"]
)
def test_ak_info(self):
"""Test if contents of AK info dict is correct."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
for slot in self.ak_slots:
ak = self.export_objects["aks"][slot.pk]
self.assertEqual(ak["info"]["name"], slot.ak.name)
self.assertEqual(
ak["info"]["head"], ", ".join(map(str, slot.ak.owners.all()))
)
self.assertEqual(ak["info"]["description"], slot.ak.description)
self.assertEqual(ak["info"]["django_ak_id"], slot.ak.pk)
self.assertEqual(
ak["info"]["types"],
list(slot.ak.types.values_list("name", flat=True).order_by()),
)
def test_ak_room_constraints(self):
"""Test if AK room constraints are exported as expected."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
for slot in self.ak_slots:
ak = self.export_objects["aks"][slot.pk]
requirements = list(
slot.ak.requirements.values_list("name", flat=True)
)
# proxy rooms
if not any(constr.startswith("proxy") for constr in requirements):
requirements.append("no-proxy")
# fixed slot
if slot.fixed and slot.room is not None:
requirements.append(f"fixed-room-{slot.room.pk}")
self.assertEqual(
set(ak["room_constraints"]),
set(requirements),
f"Room constraints for slot {slot.pk} not as expected",
)
def test_ak_time_constraints(self):
"""Test if AK time constraints are exported as expected."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
for slot in self.ak_slots:
time_constraints = set()
# add time constraints for AK category
if slot.ak.category:
category_constraints = AKCategory.create_category_constraints(
[slot.ak.category]
)
time_constraints |= category_constraints
if slot.fixed and slot.start is not None:
# fixed slot
time_constraints.add(f"fixed-akslot-{slot.pk}")
elif not Availability.is_event_covered(
slot.event, slot.ak.availabilities.all()
):
# restricted AK availability
time_constraints.add(f"availability-ak-{slot.ak.pk}")
for owner in slot.ak.owners.all():
# restricted owner availability
if not owner.availabilities.all():
# no availability for owner -> assume full event is covered
continue
if not Availability.is_event_covered(
slot.event, owner.availabilities.all()
):
time_constraints.add(f"availability-person-{owner.pk}")
ak = self.export_objects["aks"][slot.pk]
self.assertEqual(
set(ak["time_constraints"]),
time_constraints,
f"Time constraints for slot {slot.pk} not as expected",
)
def test_all_rooms_exported(self):
"""Test if exported Rooms match the rooms of Event."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
self.assertEqual(
{room.pk for room in self.rooms},
self.export_objects["rooms"].keys(),
"Exported Rooms do not match the Rooms of the event",
)
def test_room_capacity(self):
"""Test if room capacity is exported correctly."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
for room in self.rooms:
export_room = self.export_objects["rooms"][room.pk]
self.assertEqual(room.capacity, export_room["capacity"])
def test_room_info(self):
"""Test if contents of Room info dict is correct."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
for room in self.rooms:
export_room = self.export_objects["rooms"][room.pk]
self.assertEqual(room.name, export_room["info"]["name"])
def test_room_timeconstraints(self):
"""Test if Room time constraints are exported as expected."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
for room in self.rooms:
time_constraints = set()
# test if time availability of room is restricted
if not Availability.is_event_covered(
event, room.availabilities.all()
):
time_constraints.add(f"availability-room-{room.pk}")
export_room = self.export_objects["rooms"][room.pk]
self.assertEqual(
time_constraints, set(export_room["time_constraints"])
)
def test_room_fulfilledroomconstraints(self):
"""Test if room constraints fulfilled by Room are correct."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
for room in self.rooms:
# room properties
fulfilled_room_constraints = set(
room.properties.values_list("name", flat=True)
)
# proxy rooms
if not any(
constr.startswith("proxy")
for constr in fulfilled_room_constraints
):
fulfilled_room_constraints.add("no-proxy")
fulfilled_room_constraints.add(f"fixed-room-{room.pk}")
export_room = self.export_objects["rooms"][room.pk]
self.assertEqual(
fulfilled_room_constraints,
set(export_room["fulfilled_room_constraints"]),
)
def _get_timeslot_start_end(self, timeslot):
start = datetime.strptime(timeslot["info"]["start"], "%Y-%m-%d %H:%M").replace(
tzinfo=self.event.timezone
)
end = datetime.strptime(timeslot["info"]["end"], "%Y-%m-%d %H:%M").replace(
tzinfo=self.event.timezone
)
return start, end
def _get_cat_availability_in_export(self):
export_slot_cat_avails = defaultdict(list)
for timeslot in chain.from_iterable(self.export_dict["timeslots"]["blocks"]):
for constr in timeslot["fulfilled_time_constraints"]:
if constr.startswith("availability-cat-"):
cat_name = constr[len("availability-cat-") :]
start, end = self._get_timeslot_start_end(timeslot)
export_slot_cat_avails[cat_name].append(
Availability(event=self.event, start=start, end=end)
)
return {
cat_name: Availability.union(avail_lst)
for cat_name, avail_lst in export_slot_cat_avails.items()
}
def _get_cat_availability(self):
if DefaultSlot.objects.filter(event=self.event).exists():
# Event has default slots -> use them for category availability
default_slots_avails = defaultdict(list)
for def_slot in DefaultSlot.objects.filter(event=self.event).all():
avail = Availability(
event=self.event,
start=def_slot.start.astimezone(self.event.timezone),
end=def_slot.end.astimezone(self.event.timezone),
)
for cat in def_slot.primary_categories.all():
default_slots_avails[cat.name].append(avail)
return {
cat_name: Availability.union(avail_lst)
for cat_name, avail_lst in default_slots_avails.items()
}
# Event has no default slots -> all categories available through whole event
start = self.event.start.astimezone(self.event.timezone)
end = self.event.end.astimezone(self.event.timezone)
delta = (end - start).total_seconds()
# tweak event end
# 1. shorten event to match discrete slot grid
slot_seconds = 3600 / self.slots_in_an_hour
remainder_seconds = delta % slot_seconds
remainder_seconds += 1 # add a second to compensate rounding errs
end -= timedelta(seconds=remainder_seconds)
# set seconds and microseconds to 0 as they are not exported to the json
start -= timedelta(seconds=start.second, microseconds=start.microsecond)
end -= timedelta(seconds=end.second, microseconds=end.microsecond)
event_avail = Availability(event=self.event, start=start, end=end)
category_names = AKCategory.objects.filter(event=self.event).values_list(
"name", flat=True
)
return {cat_name: [event_avail] for cat_name in category_names}
def test_timeslots_consecutive(self):
"""Test if consecutive timeslots in JSON are in fact consecutive."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
prev_end = None
for timeslot in chain.from_iterable(
self.export_dict["timeslots"]["blocks"]
):
start, end = self._get_timeslot_start_end(timeslot)
self.assertLess(start, end)
delta = end - start
self.assertAlmostEqual(
delta.total_seconds() / (3600), 1 / self.slots_in_an_hour
)
if prev_end is not None:
self.assertLessEqual(prev_end, start)
prev_end = end
def test_block_cover_categories(self):
"""Test if blocks covers all default slot resp. whole event per category."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
category_names = AKCategory.objects.filter(event=event).values_list(
"name", flat=True
)
export_cat_avails = self._get_cat_availability_in_export()
cat_avails = self._get_cat_availability()
for cat_name in category_names:
for avail in cat_avails[cat_name]:
# check that all category availabilities are covered
self.assertTrue(
avail.is_covered(export_cat_avails[cat_name]),
f"AKCategory {cat_name}: avail ({avail.start}{avail.end}) "
f"not covered by {[f'({a.start} {a.end})' for a in export_cat_avails[cat_name]]}",
)
def _is_restricted_and_contained_slot(
self, slot: Availability, availabilities: list[Availability]
) -> bool:
"""Test if object is not available for whole event and may happen during slot."""
return slot.is_covered(availabilities) and not Availability.is_event_covered(
self.event, availabilities
)
def _is_ak_fixed_in_slot(
self,
ak_slot: AKSlot,
timeslot_avail: Availability,
) -> bool:
if not ak_slot.fixed or ak_slot.start is None:
return False
ak_slot_avail = Availability(
event=self.event,
start=ak_slot.start.astimezone(self.event.timezone),
end=ak_slot.end.astimezone(self.event.timezone),
)
return timeslot_avail.overlaps(ak_slot_avail, strict=True)
def test_timeslot_fulfilledconstraints(self):
"""Test if fulfilled time constraints by timeslot are as expected."""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
cat_avails = self._get_cat_availability()
num_blocks = len(self.export_dict["timeslots"]["blocks"])
for block_idx, block in enumerate(
self.export_dict["timeslots"]["blocks"]
):
for timeslot in block:
start, end = self._get_timeslot_start_end(timeslot)
timeslot_avail = Availability(
event=self.event, start=start, end=end
)
fulfilled_time_constraints = set()
# reso deadline
if self.event.reso_deadline is not None:
# timeslot ends before deadline
if end < self.event.reso_deadline.astimezone(
self.event.timezone
):
fulfilled_time_constraints.add("resolution")
# add category constraints
fulfilled_time_constraints |= (
AKCategory.create_category_constraints(
[
cat
for cat in AKCategory.objects.filter(
event=self.event
).all()
if timeslot_avail.is_covered(cat_avails[cat.name])
]
)
)
# add owner constraints
fulfilled_time_constraints |= {
f"availability-person-{owner.id}"
for owner in AKOwner.objects.filter(event=self.event).all()
if self._is_restricted_and_contained_slot(
timeslot_avail,
Availability.union(owner.availabilities.all()),
)
}
# add room constraints
fulfilled_time_constraints |= {
f"availability-room-{room.id}"
for room in self.rooms
if self._is_restricted_and_contained_slot(
timeslot_avail,
Availability.union(room.availabilities.all()),
)
}
# add ak constraints
fulfilled_time_constraints |= {
f"availability-ak-{ak.id}"
for ak in AK.objects.filter(event=event)
if self._is_restricted_and_contained_slot(
timeslot_avail,
Availability.union(ak.availabilities.all()),
)
}
fulfilled_time_constraints |= {
f"fixed-akslot-{slot.id}"
for slot in self.ak_slots
if self._is_ak_fixed_in_slot(slot, timeslot_avail)
}
fulfilled_time_constraints |= {
f"notblock{idx}"
for idx in range(num_blocks)
if idx != block_idx
}
self.assertEqual(
fulfilled_time_constraints,
set(timeslot["fulfilled_time_constraints"]),
)
def test_timeslots_info(self):
"""Test timeslots info dict"""
for event in Event.objects.all():
with self.subTest(event=event):
self.set_up_event(event=event)
self.assertAlmostEqual(
self.export_dict["timeslots"]["info"]["duration"],
float(self.event.export_slot),
)
block_names = []
for block in self.export_dict["timeslots"]["blocks"]:
if not block:
continue
block_start, _ = self._get_timeslot_start_end(block[0])
_, block_end = self._get_timeslot_start_end(block[-1])
start_day = block_start.strftime("%A, %d. %b")
if block_start.date() == block_end.date():
# same day
time_str = (
block_start.strftime("%H:%M")
+ ""
+ block_end.strftime("%H:%M")
)
else:
# different days
time_str = (
block_start.strftime("%a %H:%M")
+ ""
+ block_end.strftime("%a %H:%M")
)
block_names.append([start_day, time_str])
self.assertEqual(
block_names, self.export_dict["timeslots"]["info"]["blocknames"]
)
...@@ -7,8 +7,19 @@ from django.contrib.messages.storage.base import Message ...@@ -7,8 +7,19 @@ from django.contrib.messages.storage.base import Message
from django.test import TestCase from django.test import TestCase
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, Room, AKSlot, AKOrgaMessage, \ from AKModel.models import (
ConstraintViolation, DefaultSlot Event,
AKOwner,
AKCategory,
AKTrack,
AKRequirement,
AK,
Room,
AKSlot,
AKOrgaMessage,
ConstraintViolation,
DefaultSlot,
)
class BasicViewTests: class BasicViewTests:
...@@ -29,9 +40,10 @@ class BasicViewTests: ...@@ -29,9 +40,10 @@ class BasicViewTests:
since the test framework does not understand the concept of abstract test definitions and would handle this class since the test framework does not understand the concept of abstract test definitions and would handle this class
as real test case otherwise, distorting the test results. as real test case otherwise, distorting the test results.
""" """
# pylint: disable=no-member # pylint: disable=no-member
VIEWS = [] VIEWS = []
APP_NAME = '' APP_NAME = ""
VIEWS_STAFF_ONLY = [] VIEWS_STAFF_ONLY = []
EDIT_TESTCASES = [] EDIT_TESTCASES = []
...@@ -41,16 +53,26 @@ class BasicViewTests: ...@@ -41,16 +53,26 @@ class BasicViewTests:
""" """
user_model = get_user_model() user_model = get_user_model()
self.staff_user = user_model.objects.create( self.staff_user = user_model.objects.create(
username='Test Staff User', email='teststaff@example.com', password='staffpw', username="Test Staff User",
is_staff=True, is_active=True email="teststaff@example.com",
password="staffpw",
is_staff=True,
is_active=True,
) )
self.admin_user = user_model.objects.create( self.admin_user = user_model.objects.create(
username='Test Admin User', email='testadmin@example.com', password='adminpw', username="Test Admin User",
is_staff=True, is_superuser=True, is_active=True email="testadmin@example.com",
password="adminpw",
is_staff=True,
is_superuser=True,
is_active=True,
) )
self.deactivated_user = user_model.objects.create( self.deactivated_user = user_model.objects.create(
username='Test Deactivated User', email='testdeactivated@example.com', password='deactivatedpw', username="Test Deactivated User",
is_staff=True, is_active=False email="testdeactivated@example.com",
password="deactivatedpw",
is_staff=True,
is_active=False,
) )
def _name_and_url(self, view_name): def _name_and_url(self, view_name):
...@@ -62,7 +84,9 @@ class BasicViewTests: ...@@ -62,7 +84,9 @@ class BasicViewTests:
:return: full view name with prefix if applicable, url of the view :return: full view name with prefix if applicable, url of the view
:rtype: str, str :rtype: str, str
""" """
view_name_with_prefix = f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0] view_name_with_prefix = (
f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0]
)
url = reverse(view_name_with_prefix, kwargs=view_name[1]) url = reverse(view_name_with_prefix, kwargs=view_name[1])
return view_name_with_prefix, url return view_name_with_prefix, url
...@@ -74,7 +98,7 @@ class BasicViewTests: ...@@ -74,7 +98,7 @@ class BasicViewTests:
:param expected_message: message that should be shown :param expected_message: message that should be shown
:param msg_prefix: prefix for the error message when test fails :param msg_prefix: prefix for the error message when test fails
""" """
messages:List[Message] = list(get_messages(response.wsgi_request)) messages: List[Message] = list(get_messages(response.wsgi_request))
msg_count = "No message shown to user" msg_count = "No message shown to user"
msg_content = "Wrong message, expected '{expected_message}'" msg_content = "Wrong message, expected '{expected_message}'"
...@@ -95,10 +119,16 @@ class BasicViewTests: ...@@ -95,10 +119,16 @@ class BasicViewTests:
view_name_with_prefix, url = self._name_and_url(view_name) view_name_with_prefix, url = self._name_and_url(view_name)
try: try:
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"{view_name_with_prefix} ({url}) broken") self.assertEqual(
except Exception: # pylint: disable=broad-exception-caught response.status_code,
self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):" 200,
f"\n\n{traceback.format_exc()}") msg=f"{view_name_with_prefix} ({url}) broken",
)
except Exception: # pylint: disable=broad-exception-caught
self.fail(
f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
f"\n\n{traceback.format_exc()}"
)
def test_access_control_staff_only(self): def test_access_control_staff_only(self):
""" """
...@@ -107,11 +137,16 @@ class BasicViewTests: ...@@ -107,11 +137,16 @@ class BasicViewTests:
# Not logged in? Views should not be visible # Not logged in? Views should not be visible
self.client.logout() self.client.logout()
for view_name_info in self.VIEWS_STAFF_ONLY: for view_name_info in self.VIEWS_STAFF_ONLY:
expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2] expected_response_code = (
302 if len(view_name_info) == 2 else view_name_info[2]
)
view_name_with_prefix, url = self._name_and_url(view_name_info) view_name_with_prefix, url = self._name_and_url(view_name_info)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, expected_response_code, self.assertEqual(
msg=f"{view_name_with_prefix} ({url}) accessible by non-staff") response.status_code,
expected_response_code,
msg=f"{view_name_with_prefix} ({url}) accessible by non-staff",
)
# Logged in? Views should be visible # Logged in? Views should be visible
self.client.force_login(self.staff_user) self.client.force_login(self.staff_user)
...@@ -119,20 +154,30 @@ class BasicViewTests: ...@@ -119,20 +154,30 @@ class BasicViewTests:
view_name_with_prefix, url = self._name_and_url(view_name_info) view_name_with_prefix, url = self._name_and_url(view_name_info)
try: try:
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, self.assertEqual(
msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)") response.status_code,
200,
msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)",
)
except Exception: # pylint: disable=broad-exception-caught except Exception: # pylint: disable=broad-exception-caught
self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):" self.fail(
f"\n\n{traceback.format_exc()}") f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
f"\n\n{traceback.format_exc()}"
)
# Disabled user? Views should not be visible # Disabled user? Views should not be visible
self.client.force_login(self.deactivated_user) self.client.force_login(self.deactivated_user)
for view_name_info in self.VIEWS_STAFF_ONLY: for view_name_info in self.VIEWS_STAFF_ONLY:
expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2] expected_response_code = (
302 if len(view_name_info) == 2 else view_name_info[2]
)
view_name_with_prefix, url = self._name_and_url(view_name_info) view_name_with_prefix, url = self._name_and_url(view_name_info)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, expected_response_code, self.assertEqual(
msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user") response.status_code,
expected_response_code,
msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user",
)
def _to_sendable_value(self, val): def _to_sendable_value(self, val):
""" """
...@@ -182,16 +227,26 @@ class BasicViewTests: ...@@ -182,16 +227,26 @@ class BasicViewTests:
self.client.logout() self.client.logout()
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"{name}: Could not load edit form via GET ({url})") self.assertEqual(
response.status_code,
200,
msg=f"{name}: Could not load edit form via GET ({url})",
)
form = response.context[form_name] form = response.context[form_name]
data = {k:self._to_sendable_value(v) for k,v in form.initial.items()} data = {k: self._to_sendable_value(v) for k, v in form.initial.items()}
response = self.client.post(url, data=data) response = self.client.post(url, data=data)
if expected_code == 200: if expected_code == 200:
self.assertEqual(response.status_code, 200, msg=f"{name}: Did not return 200 ({url}") self.assertEqual(
response.status_code, 200, msg=f"{name}: Did not return 200 ({url}"
)
elif expected_code == 302: elif expected_code == 302:
self.assertRedirects(response, target_url, msg_prefix=f"{name}: Did not redirect ({url} -> {target_url}") self.assertRedirects(
response,
target_url,
msg_prefix=f"{name}: Did not redirect ({url} -> {target_url}",
)
if expected_message != "": if expected_message != "":
self._assert_message(response, expected_message, msg_prefix=f"{name}") self._assert_message(response, expected_message, msg_prefix=f"{name}")
...@@ -200,30 +255,44 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -200,30 +255,44 @@ class ModelViewTests(BasicViewTests, TestCase):
""" """
Basic view test cases for views from AKModel plus some custom tests Basic view test cases for views from AKModel plus some custom tests
""" """
fixtures = ['model.json']
fixtures = ["model.json"]
ADMIN_MODELS = [ ADMIN_MODELS = [
(Event, 'event'), (AKOwner, 'akowner'), (AKCategory, 'akcategory'), (AKTrack, 'aktrack'), (Event, "event"),
(AKRequirement, 'akrequirement'), (AK, 'ak'), (Room, 'room'), (AKSlot, 'akslot'), (AKOwner, "akowner"),
(AKOrgaMessage, 'akorgamessage'), (ConstraintViolation, 'constraintviolation'), (AKCategory, "akcategory"),
(DefaultSlot, 'defaultslot') (AKTrack, "aktrack"),
(AKRequirement, "akrequirement"),
(AK, "ak"),
(Room, "room"),
(AKSlot, "akslot"),
(AKOrgaMessage, "akorgamessage"),
(ConstraintViolation, "constraintviolation"),
(DefaultSlot, "defaultslot"),
] ]
VIEWS_STAFF_ONLY = [ VIEWS_STAFF_ONLY = [
('admin:index', {}), ("admin:index", {}),
('admin:event_status', {'event_slug': 'kif42'}), ("admin:event_status", {"event_slug": "kif42"}),
('admin:event_requirement_overview', {'event_slug': 'kif42'}), ("admin:event_requirement_overview", {"event_slug": "kif42"}),
('admin:ak_csv_export', {'event_slug': 'kif42'}), ("admin:ak_csv_export", {"event_slug": "kif42"}),
('admin:ak_wiki_export', {'slug': 'kif42'}), ("admin:ak_json_export", {"event_slug": "kif42"}),
('admin:ak_delete_orga_messages', {'event_slug': 'kif42'}), ("admin:ak_wiki_export", {"slug": "kif42"}),
('admin:ak_slide_export', {'event_slug': 'kif42'}), ("admin:ak_schedule_json_import", {"event_slug": "kif42"}),
('admin:default-slots-editor', {'event_slug': 'kif42'}), ("admin:ak_delete_orga_messages", {"event_slug": "kif42"}),
('admin:room-import', {'event_slug': 'kif42'}), ("admin:ak_slide_export", {"event_slug": "kif42"}),
('admin:new_event_wizard_start', {}), ("admin:default-slots-editor", {"event_slug": "kif42"}),
("admin:room-import", {"event_slug": "kif42"}),
("admin:new_event_wizard_start", {}),
] ]
EDIT_TESTCASES = [ EDIT_TESTCASES = [
{'view': 'admin:default-slots-editor', 'kwargs': {'event_slug': 'kif42'}, "admin": True}, {
"view": "admin:default-slots-editor",
"kwargs": {"event_slug": "kif42"},
"admin": True,
},
] ]
def test_admin(self): def test_admin(self):
...@@ -234,24 +303,32 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -234,24 +303,32 @@ class ModelViewTests(BasicViewTests, TestCase):
for model in self.ADMIN_MODELS: for model in self.ADMIN_MODELS:
# Special treatment for a subset of views (where we exchanged default functionality, e.g., create views) # Special treatment for a subset of views (where we exchanged default functionality, e.g., create views)
if model[1] == "event": if model[1] == "event":
_, url = self._name_and_url(('admin:new_event_wizard_start', {})) _, url = self._name_and_url(("admin:new_event_wizard_start", {}))
elif model[1] == "room": elif model[1] == "room":
_, url = self._name_and_url(('admin:room-new', {})) _, url = self._name_and_url(("admin:room-new", {}))
# Otherwise, just call the creation form view # Otherwise, just call the creation form view
else: else:
_, url = self._name_and_url((f'admin:AKModel_{model[1]}_add', {})) _, url = self._name_and_url((f"admin:AKModel_{model[1]}_add", {}))
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"Add form for model {model[1]} ({url}) broken") self.assertEqual(
response.status_code,
200,
msg=f"Add form for model {model[1]} ({url}) broken",
)
for model in self.ADMIN_MODELS: for model in self.ADMIN_MODELS:
# Test the update view using the first existing instance of each model # Test the update view using the first existing instance of each model
m = model[0].objects.first() m = model[0].objects.first()
if m is not None: if m is not None:
_, url = self._name_and_url( _, url = self._name_and_url(
(f'admin:AKModel_{model[1]}_change', {'object_id': m.pk}) (f"admin:AKModel_{model[1]}_change", {"object_id": m.pk})
) )
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"Edit form for model {model[1]} ({url}) broken") self.assertEqual(
response.status_code,
200,
msg=f"Edit form for model {model[1]} ({url}) broken",
)
def test_wiki_export(self): def test_wiki_export(self):
""" """
...@@ -260,17 +337,27 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -260,17 +337,27 @@ class ModelViewTests(BasicViewTests, TestCase):
""" """
self.client.force_login(self.admin_user) self.client.force_login(self.admin_user)
export_url = reverse_lazy("admin:ak_wiki_export", kwargs={'slug': 'kif42'}) export_url = reverse_lazy("admin:ak_wiki_export", kwargs={"slug": "kif42"})
response = self.client.get(export_url) response = self.client.get(export_url)
self.assertEqual(response.status_code, 200, "Export not working at all") self.assertEqual(response.status_code, 200, "Export not working at all")
export_count = 0 export_count = 0
for _, aks in response.context["categories_with_aks"]: for _, aks in response.context["categories_with_aks"]:
for ak in aks: for ak in aks:
self.assertEqual(ak.include_in_export, True, self.assertEqual(
f"AK with export flag set to False (pk={ak.pk}) included in export") ak.include_in_export,
self.assertNotEqual(ak.pk, 1, "AK known to be excluded from export (PK 1) included in export") True,
f"AK with export flag set to False (pk={ak.pk}) included in export",
)
self.assertNotEqual(
ak.pk,
1,
"AK known to be excluded from export (PK 1) included in export",
)
export_count += 1 export_count += 1
self.assertEqual(export_count, AK.objects.filter(event_id=2, include_in_export=True).count(), self.assertEqual(
"Wiki export contained the wrong number of AKs") export_count,
AK.objects.filter(event_id=2, include_in_export=True).count(),
"Wiki export contained the wrong number of AKs",
)
...@@ -5,8 +5,9 @@ from rest_framework.routers import DefaultRouter ...@@ -5,8 +5,9 @@ from rest_framework.routers import DefaultRouter
import AKModel.views.api import AKModel.views.api
from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, \ from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, \
AKsByUserView AKsByUserView, AKScheduleJSONImportView
from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKJSONExportView, AKWikiExportView, \
AKMessageDeleteView
from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \ from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \
NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, NewEventWizardSettingsView NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, NewEventWizardSettingsView
from AKModel.views.room import RoomBatchCreationView from AKModel.views.room import RoomBatchCreationView
...@@ -96,6 +97,10 @@ def get_admin_urls_event(admin_site): ...@@ -96,6 +97,10 @@ def get_admin_urls_event(admin_site):
name="aks_by_owner"), name="aks_by_owner"),
path('<slug:event_slug>/ak-csv-export/', admin_site.admin_view(AKCSVExportView.as_view()), path('<slug:event_slug>/ak-csv-export/', admin_site.admin_view(AKCSVExportView.as_view()),
name="ak_csv_export"), name="ak_csv_export"),
path('<slug:event_slug>/ak-json-export/', admin_site.admin_view(AKJSONExportView.as_view()),
name="ak_json_export"),
path('<slug:event_slug>/ak-schedule-json-import/', admin_site.admin_view(AKScheduleJSONImportView.as_view()),
name="ak_schedule_json_import"),
path('<slug:slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()), path('<slug:slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()),
name="ak_wiki_export"), name="ak_wiki_export"),
path('<slug:event_slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()), path('<slug:event_slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()),
......
import json
from pathlib import Path
from jsonschema import Draft202012Validator
from jsonschema.protocols import Validator
from referencing import Registry, Resource
from AKPlanning import settings
def construct_schema_validator(schema: str | dict) -> Validator:
"""Construct a validator for a JSON schema.
In particular, all schemas from the 'schemas' directory
are loaded into the registry.
"""
schema_base_path = Path(settings.BASE_DIR) / "schemas"
resources = []
for schema_path in schema_base_path.glob("**/*.schema.json"):
with schema_path.open("r") as ff:
res = Resource.from_contents(json.load(ff))
resources.append((res.id(), res))
registry = Registry().with_resources(resources)
if isinstance(schema, str):
with (schema_base_path / schema).open("r") as ff:
schema = json.load(ff)
return Draft202012Validator(schema=schema, registry=registry)
import json
from django.contrib import messages from django.contrib import messages
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 _
...@@ -37,6 +39,28 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): ...@@ -37,6 +39,28 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
return super().get_queryset().order_by("ak__track") return super().get_queryset().order_by("ak__track")
class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
"""
View: Export all AK slots of this event in JSON format ordered by tracks
"""
template_name = "admin/AKModel/ak_json_export.html"
model = AKSlot
context_object_name = "slots"
title = _("AK JSON Export")
def get_queryset(self):
return super().get_queryset().order_by("ak__track")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
data = self.event.as_json_dict()
context["json_data_oneline"] = json.dumps(data)
context["json_data"] = json.dumps(data, indent=2)
return context
class AKWikiExportView(AdminViewMixin, DetailView): class AKWikiExportView(AdminViewMixin, DetailView):
""" """
View: Export AKs of this event in wiki syntax View: Export AKs of this event in wiki syntax
......
...@@ -4,15 +4,17 @@ import os ...@@ -4,15 +4,17 @@ import os
import tempfile import tempfile
from itertools import zip_longest from itertools import zip_longest
from django.contrib import messages from django.contrib import messages
from django.db.models.functions import Now from django.db.models.functions import Now
from django.shortcuts import redirect
from django.utils.dateparse import parse_datetime from django.utils.dateparse import parse_datetime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView, DetailView from django.views.generic import TemplateView, DetailView
from django_tex.core import render_template_with_context, run_tex_in_directory from django_tex.core import render_template_with_context, run_tex_in_directory
from django_tex.response import PDFResponse from django_tex.response import PDFResponse
from AKModel.forms import SlideExportForm, DefaultSlotEditorForm from AKModel.forms import SlideExportForm, DefaultSlotEditorForm, JSONScheduleImportForm
from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin
from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner
...@@ -58,7 +60,7 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView): ...@@ -58,7 +60,7 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
Create a list of tuples cosisting of an AK and a list of upcoming AKs (list length depending on setting) Create a list of tuples cosisting of an AK and a list of upcoming AKs (list length depending on setting)
""" """
next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None) next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None)
return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=[])] return list(zip_longest(ak_list, next_aks_list, fillvalue=[]))
# Get all relevant AKs (wishes separately, and either all AKs or only those who should directly or indirectly # Get all relevant AKs (wishes separately, and either all AKs or only those who should directly or indirectly
# be presented when restriction setting was chosen) # be presented when restriction setting was chosen)
...@@ -245,3 +247,29 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView): ...@@ -245,3 +247,29 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView):
model = AKOwner model = AKOwner
context_object_name = 'owner' context_object_name = 'owner'
template_name = "admin/AKModel/aks_by_user.html" template_name = "admin/AKModel/aks_by_user.html"
class AKScheduleJSONImportView(EventSlugMixin, IntermediateAdminView):
"""
View: Import an AK schedule from a json file that can be pasted into this view.
"""
template_name = "admin/AKModel/import_json.html"
form_class = JSONScheduleImportForm
title = _("AK Schedule JSON Import")
def form_valid(self, form):
try:
number_of_slots_changed = self.event.schedule_from_json(form.cleaned_data["data"])
messages.add_message(
self.request,
messages.SUCCESS,
_("Successfully imported {n} slot(s)").format(n=number_of_slots_changed)
)
except ValueError as ex:
messages.add_message(
self.request,
messages.ERROR,
_("Importing an AK schedule failed! Reason: ") + str(ex),
)
return redirect("admin:event_status", self.event.slug)