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
Select Git revision
  • komasolver
  • main
  • renovate/django-5.x
  • renovate/django_csp-4.x
  • renovate/jsonschema-4.x
  • renovate/uwsgi-2.x
6 results

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
Select Git revision
  • ak-import
  • feature/clear-schedule-button
  • feature/json-export-via-rest-framework
  • feature/json-export-via-rest-framework-rebased
  • feature/json-schedule-import-tests
  • feature/preference-polling
  • feature/preference-polling-form
  • feature/preference-polling-form-rebased
  • feature/preference-polling-rebased
  • fix/add-room-import-only-once
  • main
  • merge-to-upstream
  • renovate/django-5.x
  • renovate/django-debug-toolbar-4.x
  • renovate/django-simple-history-3.x
  • renovate/mysqlclient-2.x
16 results
Show changes
Showing
with 402 additions and 132 deletions
...@@ -5,10 +5,21 @@ from django.contrib.auth import get_user_model ...@@ -5,10 +5,21 @@ from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages from django.contrib.messages import get_messages
from django.contrib.messages.storage.base import Message 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, reverse_lazy
from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, Room, AKSlot, AKOrgaMessage, \ from AKModel.models import (
ConstraintViolation, DefaultSlot AK,
AKCategory,
AKOrgaMessage,
AKOwner,
AKRequirement,
AKSlot,
AKTrack,
ConstraintViolation,
DefaultSlot,
Event,
Room,
)
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()),
......
from pathlib import Path
import referencing.retrieval
from jsonschema import Draft202012Validator
from jsonschema.protocols import Validator
from referencing import Registry
from AKPlanning import settings
def _construct_schema_path(uri: str | Path) -> Path:
"""Construct a schema URI.
This function also checks for unallowed directory traversals
out of the 'schema' subfolder.
"""
schema_base_path = Path(settings.BASE_DIR).resolve()
uri_path = (schema_base_path / uri).resolve()
if not uri_path.is_relative_to(schema_base_path / "schemas"):
raise ValueError("Unallowed dictionary traversal")
return uri_path
@referencing.retrieval.to_cached_resource()
def retrieve_schema_from_disk(uri: str) -> str:
"""Retrieve schemas from disk by URI."""
uri_path = _construct_schema_path(uri)
with uri_path.open("r") as ff:
return ff.read()
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.
"""
registry = Registry(retrieve=retrieve_schema_from_disk)
if isinstance(schema, str):
schema_uri = str(Path("schemas") / schema)
schema = registry.get_or_retrieve(schema_uri).value.contents
return Draft202012Validator(schema=schema, registry=registry)
import json
from django.contrib import messages from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView, DetailView from django.views.generic import ListView, DetailView
...@@ -37,6 +40,44 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): ...@@ -37,6 +40,44 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
return super().get_queryset().order_by("ak__track") return super().get_queryset().order_by("ak__track")
class AKJSONExportView(AdminViewMixin, DetailView):
"""
View: Export all AK slots of this event in JSON format ordered by tracks
"""
template_name = "admin/AKModel/ak_json_export.html"
model = Event
context_object_name = "event"
title = _("AK JSON Export")
slug_url_kwarg = "event_slug"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
try:
data = context["event"].as_json_dict()
context["json_data_oneline"] = json.dumps(data, ensure_ascii=False)
context["json_data"] = json.dumps(data, indent=2, ensure_ascii=False)
context["is_valid"] = True
except ValueError as ex:
messages.add_message(
self.request,
messages.ERROR,
_("Exporting AKs for the solver failed! Reason: ") + str(ex),
)
return context
def get(self, request, *args, **kwargs):
# as this code is adapted from BaseDetailView::get
# pylint: disable=attribute-defined-outside-init
self.object = self.get_object()
context = self.get_context_data(object=self.object)
# if serialization failed in `get_context_data` we redirect to
# the status page and show a message instead
if not context.get("is_valid", False):
return redirect("admin:event_status", context["event"].slug)
return self.render_to_response(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)
...@@ -138,10 +138,18 @@ class EventAKsWidget(TemplateStatusWidget): ...@@ -138,10 +138,18 @@ class EventAKsWidget(TemplateStatusWidget):
"text": _("Manage ak tracks"), "text": _("Manage ak tracks"),
"url": reverse_lazy("admin:tracks_manage", kwargs={"event_slug": context["event"].slug}), "url": reverse_lazy("admin:tracks_manage", kwargs={"event_slug": context["event"].slug}),
}, },
{
"text": _("Import AK schedule from JSON"),
"url": reverse_lazy("admin:ak_schedule_json_import", kwargs={"event_slug": context["event"].slug}),
},
{ {
"text": _("Export AKs as CSV"), "text": _("Export AKs as CSV"),
"url": reverse_lazy("admin:ak_csv_export", kwargs={"event_slug": context["event"].slug}), "url": reverse_lazy("admin:ak_csv_export", kwargs={"event_slug": context["event"].slug}),
}, },
{
"text": _("Export AKs as JSON"),
"url": reverse_lazy("admin:ak_json_export", kwargs={"event_slug": context["event"].slug}),
},
{ {
"text": _("Export AKs for Wiki"), "text": _("Export AKs for Wiki"),
"url": reverse_lazy("admin:ak_wiki_export", kwargs={"slug": context["event"].slug}), "url": reverse_lazy("admin:ak_wiki_export", kwargs={"slug": context["event"].slug}),
......
from django.test import TestCase from django.test import TestCase
from AKModel.tests import BasicViewTests from AKModel.tests.test_views import BasicViewTests
class PlanViewTests(BasicViewTests, TestCase): class PlanViewTests(BasicViewTests, TestCase):
......
from datetime import timedelta from datetime import datetime, timedelta
from django.conf import settings from django.conf import settings
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.datetime_safe import datetime from django.views.generic import DetailView, ListView
from django.views.generic import ListView, DetailView
from AKModel.models import AKSlot, Room, AKTrack
from AKModel.metaviews.admin import FilterByEventSlugMixin from AKModel.metaviews.admin import FilterByEventSlugMixin
from AKModel.models import AKSlot, AKTrack, Room
class PlanIndexView(FilterByEventSlugMixin, ListView): class PlanIndexView(FilterByEventSlugMixin, ListView):
...@@ -152,7 +151,7 @@ class PlanTrackView(FilterByEventSlugMixin, DetailView): ...@@ -152,7 +151,7 @@ class PlanTrackView(FilterByEventSlugMixin, DetailView):
context = super().get_context_data(object_list=object_list, **kwargs) context = super().get_context_data(object_list=object_list, **kwargs)
# Restrict AKSlot list to given track # Restrict AKSlot list to given track
# while joining AK, room and category information to reduce the amount of necessary SQL queries # while joining AK, room and category information to reduce the amount of necessary SQL queries
context["slots"] = AKSlot.objects.\ context["slots"] = AKSlot.objects. \
filter(event=self.event, ak__track=context['track']).\ filter(event=self.event, ak__track=context['track']). \
select_related('ak', 'room', 'ak__category') select_related('ak', 'room', 'ak__category')
return context return context
...@@ -55,7 +55,9 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView): ...@@ -55,7 +55,9 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
model = AKSlot model = AKSlot
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_related('ak').filter(event=self.event, room__isnull=False) return super().get_queryset().select_related('ak').filter(
event=self.event, room__isnull=False, start__isnull=False
)
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
return JsonResponse( return JsonResponse(
......
...@@ -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: 2024-04-25 00:24+0200\n" "POT-Creation-Date: 2025-01-22 19:00+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"
...@@ -27,7 +27,7 @@ msgstr "Ende" ...@@ -27,7 +27,7 @@ msgstr "Ende"
#: AKScheduling/forms.py:26 #: AKScheduling/forms.py:26
msgid "Duration" msgid "Duration"
msgstr "" msgstr "Dauer"
#: AKScheduling/forms.py:27 #: AKScheduling/forms.py:27
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:171 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:171
...@@ -107,6 +107,7 @@ msgid "Event Status" ...@@ -107,6 +107,7 @@ msgid "Event Status"
msgstr "Event-Status" msgstr "Event-Status"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:113 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:113
#: AKScheduling/views.py:48
msgid "Scheduling" msgid "Scheduling"
msgstr "Scheduling" msgstr "Scheduling"
...@@ -239,6 +240,7 @@ msgstr[1] "" ...@@ -239,6 +240,7 @@ msgstr[1] ""
" " " "
#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:7 #: AKScheduling/templates/admin/AKScheduling/unscheduled.html:7
#: AKScheduling/views.py:25
msgid "Unscheduled AK Slots" msgid "Unscheduled AK Slots"
msgstr "Noch nicht geschedulte AK-Slots" msgstr "Noch nicht geschedulte AK-Slots"
...@@ -246,10 +248,22 @@ msgstr "Noch nicht geschedulte AK-Slots" ...@@ -246,10 +248,22 @@ msgstr "Noch nicht geschedulte AK-Slots"
msgid "Count" msgid "Count"
msgstr "Anzahl" msgstr "Anzahl"
#: AKScheduling/views.py:89
msgid "Constraint violations for"
msgstr "Constraintverletzungen für"
#: AKScheduling/views.py:104
msgid "AKs requiring special attention for"
msgstr "AKs die besondere Aufmerksamkeit erfordern für"
#: AKScheduling/views.py:150 #: AKScheduling/views.py:150
msgid "Interest updated" msgid "Interest updated"
msgstr "Interesse aktualisiert" msgstr "Interesse aktualisiert"
#: AKScheduling/views.py:157
msgid "Enter interest"
msgstr "Interesse eingeben"
#: AKScheduling/views.py:201 #: AKScheduling/views.py:201
msgid "Wishes" msgid "Wishes"
msgstr "Wünsche" msgstr "Wünsche"
......
...@@ -288,6 +288,8 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs) ...@@ -288,6 +288,8 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs)
for slot in slots_of_this_ak: for slot in slots_of_this_ak:
room = slot.room room = slot.room
if room is None:
continue
room_requirements = room.properties.all() room_requirements = room.properties.all()
for requirement in instance.requirements.all(): for requirement in instance.requirements.all():
...@@ -363,8 +365,8 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -363,8 +365,8 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
new_violations = [] new_violations = []
# For all slots in this room... # For all slots in this room...
if instance.room: if instance.room and instance.start:
for other_slot in instance.room.akslot_set.all(): for other_slot in instance.room.akslot_set.filter(start__isnull=False):
if other_slot != instance: if other_slot != instance:
# ... find overlapping slots... # ... find overlapping slots...
if instance.overlaps(other_slot): if instance.overlaps(other_slot):
......
...@@ -355,9 +355,11 @@ ...@@ -355,9 +355,11 @@
<h5 class="mt-2">{{ track_slots.grouper }}</h5> <h5 class="mt-2">{{ track_slots.grouper }}</h5>
{% endif %} {% endif %}
{% for slot in track_slots.list %} {% for slot in track_slots.list %}
<div class="unscheduled-slot badge" style='background-color: {{ slot.ak.category.color }}' <div class="unscheduled-slot badge" style='background-color: {% with slot.ak.category.color as color %} {% if color %}{{ color }}{% else %}#000000;{% endif %}{% endwith %}'
data-event='{ "title": "{{ slot.ak.short_name }}", "duration": {"hours": "{{ slot.duration|unlocalize }}"}, "constraint": "roomAvailable", "description": "{{ slot.ak.details | escapejs }}", "slotID": "{{ slot.pk }}", "backgroundColor": "{{ slot.ak.category.color }}", "url": "{% url "admin:AKModel_akslot_change" slot.pk %}"}' data-details="{{ slot.ak.details }}">{{ slot.ak.short_name }} {% with slot.ak.details as details %}
data-event='{ "title": "{{ slot.ak.short_name }}", "duration": {"hours": "{{ slot.duration|unlocalize }}"}, "constraint": "roomAvailable", "description": "{{ details | escapejs }}", "slotID": "{{ slot.pk }}", "backgroundColor": "{{ slot.ak.category.color }}", "url": "{% url "admin:AKModel_akslot_change" slot.pk %}"}' data-details="{{ details }}">{{ slot.ak.short_name }}
({{ slot.duration }} h)<br>{{ slot.ak.owners_list }} ({{ slot.duration }} h)<br>{{ slot.ak.owners_list }}
{% endwith %}
</div> </div>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
......
...@@ -4,7 +4,7 @@ from datetime import timedelta ...@@ -4,7 +4,7 @@ from datetime import timedelta
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from AKModel.tests import BasicViewTests from AKModel.tests.test_views import BasicViewTests
from AKModel.models import AKSlot, Event, Room from AKModel.models import AKSlot, Event, Room
class ModelViewTests(BasicViewTests, TestCase): class ModelViewTests(BasicViewTests, TestCase):
......
...@@ -41,7 +41,9 @@ class SchedulingAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView): ...@@ -41,7 +41,9 @@ class SchedulingAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
context_object_name = "slots_unscheduled" context_object_name = "slots_unscheduled"
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter(start__isnull=True).select_related('event', 'ak').order_by('ak__track', 'ak') return super().get_queryset().filter(start__isnull=True).select_related('event', 'ak', 'ak__track',
'ak__category').prefetch_related('ak__types', 'ak__owners', 'ak__conflicts', 'ak__prerequisites',
'ak__requirements', 'ak__conflict').order_by('ak__track', 'ak')
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs) context = super().get_context_data(object_list=object_list, **kwargs)
...@@ -152,6 +154,13 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi ...@@ -152,6 +154,13 @@ class InterestEnteringAdminView(SuccessMessageMixin, AdminViewMixin, EventSlugMi
def get_success_url(self): def get_success_url(self):
return self.request.path return self.request.path
def form_valid(self, form):
# Don't create a history entry for this change
form.instance.skip_history_when_saving = True
r = super().form_valid(form)
del form.instance.skip_history_when_saving
return r
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = f"{_('Enter interest')}" context["title"] = f"{_('Enter interest')}"
......
from datetime import datetime
from rest_framework import status from rest_framework import status
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from django.utils.datetime_safe import datetime
from AKModel.models import AK from AKModel.models import AK
......
...@@ -152,6 +152,9 @@ class AKSubmissionForm(AKForm): ...@@ -152,6 +152,9 @@ class AKSubmissionForm(AKForm):
class Meta(AKForm.Meta): class Meta(AKForm.Meta):
# Exclude fields again that were previously included in the parent class # Exclude fields again that were previously included in the parent class
exclude = ['link', 'protocol_link'] #pylint: disable=modelform-uses-exclude exclude = ['link', 'protocol_link'] #pylint: disable=modelform-uses-exclude
widgets = AKForm.Meta.widgets | {
'types': forms.CheckboxSelectMultiple(attrs={'checked' : 'checked'}),
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
...@@ -188,6 +191,9 @@ class AKWishForm(AKForm): ...@@ -188,6 +191,9 @@ class AKWishForm(AKForm):
class Meta(AKForm.Meta): class Meta(AKForm.Meta):
# Exclude fields again that were previously included in the parent class # Exclude fields again that were previously included in the parent class
exclude = ['owners', 'link', 'protocol_link'] #pylint: disable=modelform-uses-exclude exclude = ['owners', 'link', 'protocol_link'] #pylint: disable=modelform-uses-exclude
widgets = AKForm.Meta.widgets | {
'types': forms.CheckboxSelectMultiple(attrs={'checked': 'checked'}),
}
class AKOwnerForm(forms.ModelForm): class AKOwnerForm(forms.ModelForm):
......
...@@ -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-02-25 22:33+0100\n" "POT-Creation-Date: 2025-03-25 15:58+0100\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"
...@@ -22,11 +22,11 @@ msgstr "" ...@@ -22,11 +22,11 @@ msgstr ""
msgid "\"%(duration)s\" is not a valid duration" msgid "\"%(duration)s\" is not a valid duration"
msgstr "\"%(duration)s\" ist keine gültige Dauer" msgstr "\"%(duration)s\" ist keine gültige Dauer"
#: AKSubmission/forms.py:161 #: AKSubmission/forms.py:164
msgid "Duration(s)" msgid "Duration(s)"
msgstr "Dauer(n)" msgstr "Dauer(n)"
#: AKSubmission/forms.py:163 #: AKSubmission/forms.py:166
msgid "" msgid ""
"Enter at least one planned duration (in hours). If your AK should have " "Enter at least one planned duration (in hours). If your AK should have "
"multiple slots, use multiple lines" "multiple slots, use multiple lines"
...@@ -47,7 +47,7 @@ msgstr "" ...@@ -47,7 +47,7 @@ msgstr ""
#: AKSubmission/templates/AKSubmission/submission_overview.html:7 #: AKSubmission/templates/AKSubmission/submission_overview.html:7
#: AKSubmission/templates/AKSubmission/submission_overview.html:11 #: AKSubmission/templates/AKSubmission/submission_overview.html:11
#: AKSubmission/templates/AKSubmission/submission_overview.html:36 #: AKSubmission/templates/AKSubmission/submission_overview.html:36
#: AKSubmission/templates/AKSubmission/submit_new.html:31 #: AKSubmission/templates/AKSubmission/submit_new.html:38
#: AKSubmission/templates/AKSubmission/submit_new_wish.html:13 #: AKSubmission/templates/AKSubmission/submit_new_wish.html:13
msgid "AK Submission" msgid "AK Submission"
msgstr "AK-Eintragung" msgstr "AK-Eintragung"
...@@ -274,7 +274,7 @@ msgstr "Die Ergebnisse dieses AKs vorstellen" ...@@ -274,7 +274,7 @@ msgstr "Die Ergebnisse dieses AKs vorstellen"
msgid "Intends to submit a resolution" msgid "Intends to submit a resolution"
msgstr "Beabsichtigt eine Resolution einzureichen" msgstr "Beabsichtigt eine Resolution einzureichen"
#: AKSubmission/templates/AKSubmission/ak_list.html:6 AKSubmission/views.py:84 #: AKSubmission/templates/AKSubmission/ak_list.html:6 AKSubmission/views.py:82
msgid "All AKs" msgid "All AKs"
msgstr "Alle AKs" msgstr "Alle AKs"
...@@ -305,7 +305,7 @@ msgstr "Senden" ...@@ -305,7 +305,7 @@ msgstr "Senden"
#: AKSubmission/templates/AKSubmission/akmessage_add.html:31 #: AKSubmission/templates/AKSubmission/akmessage_add.html:31
#: AKSubmission/templates/AKSubmission/akowner_create_update.html:26 #: AKSubmission/templates/AKSubmission/akowner_create_update.html:26
#: AKSubmission/templates/AKSubmission/akslot_add_update.html:29 #: AKSubmission/templates/AKSubmission/akslot_add_update.html:29
#: AKSubmission/templates/AKSubmission/submit_new.html:52 #: AKSubmission/templates/AKSubmission/submit_new.html:59
msgid "Reset Form" msgid "Reset Form"
msgstr "Formular leeren" msgstr "Formular leeren"
...@@ -313,7 +313,7 @@ msgstr "Formular leeren" ...@@ -313,7 +313,7 @@ msgstr "Formular leeren"
#: AKSubmission/templates/AKSubmission/akowner_create_update.html:30 #: AKSubmission/templates/AKSubmission/akowner_create_update.html:30
#: AKSubmission/templates/AKSubmission/akslot_add_update.html:33 #: AKSubmission/templates/AKSubmission/akslot_add_update.html:33
#: AKSubmission/templates/AKSubmission/akslot_delete.html:45 #: AKSubmission/templates/AKSubmission/akslot_delete.html:45
#: AKSubmission/templates/AKSubmission/submit_new.html:56 #: AKSubmission/templates/AKSubmission/submit_new.html:63
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
...@@ -381,8 +381,8 @@ msgstr "Ich leite bisher keine AKs" ...@@ -381,8 +381,8 @@ msgstr "Ich leite bisher keine AKs"
#: AKSubmission/templates/AKSubmission/submission_overview.html:67 #: AKSubmission/templates/AKSubmission/submission_overview.html:67
#: AKSubmission/templates/AKSubmission/submit_new.html:9 #: AKSubmission/templates/AKSubmission/submit_new.html:9
#: AKSubmission/templates/AKSubmission/submit_new.html:34
#: AKSubmission/templates/AKSubmission/submit_new.html:41 #: AKSubmission/templates/AKSubmission/submit_new.html:41
#: AKSubmission/templates/AKSubmission/submit_new.html:48
msgid "New AK" msgid "New AK"
msgstr "Neuer AK" msgstr "Neuer AK"
...@@ -396,78 +396,88 @@ msgstr "" ...@@ -396,78 +396,88 @@ msgstr ""
"Dieses Event is nicht aktiv. Es können keine AKs hinzugefügt oder bearbeitet " "Dieses Event is nicht aktiv. Es können keine AKs hinzugefügt oder bearbeitet "
"werden" "werden"
#: AKSubmission/templates/AKSubmission/submit_new.html:48 #: AKSubmission/templates/AKSubmission/submit_new.html:29
msgid ""
"only relevant for KIF-AKs - determines whether the AK appears in the slides "
"for the closing plenary session"
msgstr "nur relevant für KIF-AKs - entscheidet, ob der AK in den Folien fürs Abschlussplenum auftaucht"
#: AKSubmission/templates/AKSubmission/submit_new.html:55
msgid "Submit" msgid "Submit"
msgstr "Eintragen" msgstr "Eintragen"
#: AKSubmission/views.py:127 #: AKSubmission/views.py:125
msgid "Wishes" msgid "Wishes"
msgstr "Wünsche" msgstr "Wünsche"
#: AKSubmission/views.py:127 #: AKSubmission/views.py:125
msgid "AKs one would like to have" msgid "AKs one would like to have"
msgstr "" msgstr ""
"AKs die sich gewünscht wurden, aber bei denen noch nicht klar ist, wer sie " "AKs die sich gewünscht wurden, aber bei denen noch nicht klar ist, wer sie "
"macht. Falls du dir das vorstellen kannst, trag dich einfach ein" "macht. Falls du dir das vorstellen kannst, trag dich einfach ein"
#: AKSubmission/views.py:169 #: AKSubmission/views.py:167
msgid "Currently planned AKs" msgid "Currently planned AKs"
msgstr "Aktuell geplante AKs" msgstr "Aktuell geplante AKs"
#: AKSubmission/views.py:302 #: AKSubmission/views.py:231
msgid "AKs with Track"
msgstr "AKs mit Track"
#: AKSubmission/views.py:300
msgid "Event inactive. Cannot create or update." msgid "Event inactive. Cannot create or update."
msgstr "Event inaktiv. Hinzufügen/Bearbeiten nicht möglich." msgstr "Event inaktiv. Hinzufügen/Bearbeiten nicht möglich."
#: AKSubmission/views.py:327 #: AKSubmission/views.py:330
msgid "AK successfully created" msgid "AK successfully created"
msgstr "AK erfolgreich angelegt" msgstr "AK erfolgreich angelegt"
#: AKSubmission/views.py:400 #: AKSubmission/views.py:404
msgid "AK successfully updated" msgid "AK successfully updated"
msgstr "AK erfolgreich aktualisiert" msgstr "AK erfolgreich aktualisiert"
#: AKSubmission/views.py:451 #: AKSubmission/views.py:455
#, python-brace-format #, python-brace-format
msgid "Added '{owner}' as new owner of '{ak.name}'" msgid "Added '{owner}' as new owner of '{ak.name}'"
msgstr "'{owner}' als neue Leitung von '{ak.name}' hinzugefügt" msgstr "'{owner}' als neue Leitung von '{ak.name}' hinzugefügt"
#: AKSubmission/views.py:555 #: AKSubmission/views.py:558
msgid "No user selected" msgid "No user selected"
msgstr "Keine Person ausgewählt" msgstr "Keine Person ausgewählt"
#: AKSubmission/views.py:571 #: AKSubmission/views.py:574
msgid "Person Info successfully updated" msgid "Person Info successfully updated"
msgstr "Personen-Info erfolgreich aktualisiert" msgstr "Personen-Info erfolgreich aktualisiert"
#: AKSubmission/views.py:607 #: AKSubmission/views.py:610
msgid "AK Slot successfully added" msgid "AK Slot successfully added"
msgstr "AK-Slot erfolgreich angelegt" msgstr "AK-Slot erfolgreich angelegt"
#: AKSubmission/views.py:626 #: AKSubmission/views.py:629
msgid "You cannot edit a slot that has already been scheduled" msgid "You cannot edit a slot that has already been scheduled"
msgstr "Bereits geplante AK-Slots können nicht mehr bearbeitet werden" msgstr "Bereits geplante AK-Slots können nicht mehr bearbeitet werden"
#: AKSubmission/views.py:636 #: AKSubmission/views.py:639
msgid "AK Slot successfully updated" msgid "AK Slot successfully updated"
msgstr "AK-Slot erfolgreich aktualisiert" msgstr "AK-Slot erfolgreich aktualisiert"
#: AKSubmission/views.py:654 #: AKSubmission/views.py:657
msgid "You cannot delete a slot that has already been scheduled" msgid "You cannot delete a slot that has already been scheduled"
msgstr "Bereits geplante AK-Slots können nicht mehr gelöscht werden" msgstr "Bereits geplante AK-Slots können nicht mehr gelöscht werden"
#: AKSubmission/views.py:664 #: AKSubmission/views.py:667
msgid "AK Slot successfully deleted" msgid "AK Slot successfully deleted"
msgstr "AK-Slot erfolgreich angelegt" msgstr "AK-Slot erfolgreich angelegt"
#: AKSubmission/views.py:676 #: AKSubmission/views.py:679
msgid "Messages" msgid "Messages"
msgstr "Nachrichten" msgstr "Nachrichten"
#: AKSubmission/views.py:686 #: AKSubmission/views.py:689
msgid "Delete all messages" msgid "Delete all messages"
msgstr "Alle Nachrichten löschen" msgstr "Alle Nachrichten löschen"
#: AKSubmission/views.py:713 #: AKSubmission/views.py:716
msgid "Message to organizers successfully saved" msgid "Message to organizers successfully saved"
msgstr "Nachricht an die Organisator*innen erfolgreich gespeichert" msgstr "Nachricht an die Organisator*innen erfolgreich gespeichert"
......
...@@ -23,6 +23,13 @@ ...@@ -23,6 +23,13 @@
); );
}); });
</script> </script>
<style>
#id_present_helptext::after {
content: " ({% trans "only relevant for KIF-AKs - determines whether the AK appears in the slides for the closing plenary session" %})";
color: #6c757d;
}
</style>
{% endblock %} {% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
......
from datetime import timedelta from datetime import datetime, timedelta
from django.test import TestCase from django.test import TestCase
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.datetime_safe import datetime
from AKModel.models import AK, AKSlot, Event from AKModel.models import AK, AKSlot, Event
from AKModel.tests import BasicViewTests from AKModel.tests.test_views import BasicViewTests
from AKSubmission.forms import AKSubmissionForm from AKSubmission.forms import AKSubmissionForm
...@@ -47,8 +46,8 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -47,8 +46,8 @@ class ModelViewTests(BasicViewTests, TestCase):
'expected_message': "AK successfully updated"}, 'expected_message': "AK successfully updated"},
{'view': 'akslot_edit', 'target_view': 'ak_detail', 'kwargs': {'event_slug': 'kif42', 'pk': 5}, {'view': 'akslot_edit', 'target_view': 'ak_detail', 'kwargs': {'event_slug': 'kif42', 'pk': 5},
'target_kwargs': {'event_slug': 'kif42', 'pk': 1}, 'expected_message': "AK Slot successfully updated"}, 'target_kwargs': {'event_slug': 'kif42', 'pk': 1}, 'expected_message': "AK Slot successfully updated"},
{'view': 'akowner_edit', 'target_view': 'submission_overview', 'kwargs': {'event_slug': 'kif42', 'slug': 'a'}, {'view': 'akowner_edit', 'target_view': 'submission_overview', 'kwargs': {'event_slug': 'kif42', 'slug': 'a'},
'target_kwargs': {'event_slug': 'kif42'}, 'expected_message': "Person Info successfully updated"}, 'target_kwargs': {'event_slug': 'kif42'}, 'expected_message': "Person Info successfully updated"},
] ]
def test_akslot_edit_delete_prevention(self): def test_akslot_edit_delete_prevention(self):
...@@ -147,7 +146,8 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -147,7 +146,8 @@ class ModelViewTests(BasicViewTests, TestCase):
add_redirect_url = reverse_lazy(f"{self.APP_NAME}:submit_ak", kwargs={'event_slug': 'kif42', 'owner_slug': 'a'}) add_redirect_url = reverse_lazy(f"{self.APP_NAME}:submit_ak", kwargs={'event_slug': 'kif42', 'owner_slug': 'a'})
response = self.client.post(select_url, {'owner_id': 1}) response = self.client.post(select_url, {'owner_id': 1})
self.assertRedirects(response, add_redirect_url, status_code=302, target_status_code=200, self.assertRedirects(response, add_redirect_url, status_code=302, target_status_code=200,
msg_prefix=f"Dispatch redirect to ak submission page failed (should go to {add_redirect_url})") msg_prefix=f"Dispatch redirect to ak submission page failed "
f"(should go to {add_redirect_url})")
def test_orga_message_submission(self): def test_orga_message_submission(self):
""" """
...@@ -201,7 +201,8 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -201,7 +201,8 @@ class ModelViewTests(BasicViewTests, TestCase):
# Test indication outside of indication window -> HTTP 403, counter not increased # Test indication outside of indication window -> HTTP 403, counter not increased
response = self.client.post(interest_api_url) response = self.client.post(interest_api_url)
self.assertEqual(response.status_code, 403, self.assertEqual(response.status_code, 403,
"API end point still reachable even though interest indication window ended ({interest_api_url})") "API end point still reachable even though interest indication window ended "
"({interest_api_url})")
self.assertEqual(AK.objects.get(pk=1).interest_counter, ak_interest_counter + 1, self.assertEqual(AK.objects.get(pk=1).interest_counter, ak_interest_counter + 1,
"Counter was increased even though interest indication window ended") "Counter was increased even though interest indication window ended")
...@@ -243,13 +244,14 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -243,13 +244,14 @@ class ModelViewTests(BasicViewTests, TestCase):
Test visibility of requirements field in submission form Test visibility of requirements field in submission form
""" """
event = Event.get_by_slug('kif42') event = Event.get_by_slug('kif42')
form = AKSubmissionForm(data={'name': 'Test AK', 'event': event}, instance=None, initial={"event":event}) form = AKSubmissionForm(data={'name': 'Test AK', 'event': event}, instance=None, initial={"event": event})
self.assertIn('requirements', form.fields, self.assertIn('requirements', form.fields,
msg="Requirements field not present in form even though event has requirements") msg="Requirements field not present in form even though event has requirements")
event2 = Event.objects.create(name='Event without requirements', event2 = Event.objects.create(name='Event without requirements',
slug='no_req', slug='no_req',
start=datetime.now(), end=datetime.now(), start=datetime.now().astimezone(event.timezone),
end=datetime.now().astimezone(event.timezone),
active=True) active=True)
form2 = AKSubmissionForm(data={'name': 'Test AK', 'event': event2}, instance=None, initial={"event": event2}) form2 = AKSubmissionForm(data={'name': 'Test AK', 'event': event2}, instance=None, initial={"event": event2})
self.assertNotIn('requirements', form2.fields, self.assertNotIn('requirements', form2.fields,
...@@ -260,13 +262,14 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -260,13 +262,14 @@ class ModelViewTests(BasicViewTests, TestCase):
Test visibility of types field in submission form Test visibility of types field in submission form
""" """
event = Event.get_by_slug('kif42') event = Event.get_by_slug('kif42')
form = AKSubmissionForm(data={'name': 'Test AK', 'event': event}, instance=None, initial={"event":event}) form = AKSubmissionForm(data={'name': 'Test AK', 'event': event}, instance=None, initial={"event": event})
self.assertIn('types', form.fields, self.assertIn('types', form.fields,
msg="Requirements field not present in form even though event has requirements") msg="Requirements field not present in form even though event has requirements")
event2 = Event.objects.create(name='Event without types', event2 = Event.objects.create(name='Event without types',
slug='no_types', slug='no_types',
start=datetime.now(), end=datetime.now(), start=datetime.now().astimezone(event.timezone),
end=datetime.now().astimezone(event.timezone),
active=True) active=True)
form2 = AKSubmissionForm(data={'name': 'Test AK', 'event': event2}, instance=None, initial={"event": event2}) form2 = AKSubmissionForm(data={'name': 'Test AK', 'event': event2}, instance=None, initial={"event": event2})
self.assertNotIn('types', form2.fields, self.assertNotIn('types', form2.fields,
......
from datetime import timedelta
from math import floor
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from math import floor
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
...@@ -8,19 +8,17 @@ from django.contrib import messages ...@@ -8,19 +8,17 @@ from django.contrib import messages
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.datetime_safe import datetime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView
from AKModel.availability.models import Availability from AKModel.availability.models import Availability
from AKModel.metaviews import status_manager from AKModel.metaviews import status_manager
from AKModel.metaviews.status import TemplateStatusWidget
from AKModel.models import AK, AKCategory, AKOwner, AKSlot, AKTrack, AKOrgaMessage
from AKModel.metaviews.admin import EventSlugMixin, FilterByEventSlugMixin from AKModel.metaviews.admin import EventSlugMixin, FilterByEventSlugMixin
from AKModel.metaviews.status import TemplateStatusWidget
from AKModel.models import AK, AKCategory, AKOrgaMessage, AKOwner, AKSlot, AKTrack
from AKSubmission.api import ak_interest_indication_active from AKSubmission.api import ak_interest_indication_active
from AKSubmission.forms import AKWishForm, AKOwnerForm, AKSubmissionForm, AKDurationForm, AKOrgaMessageForm, \ from AKSubmission.forms import AKDurationForm, AKForm, AKOrgaMessageForm, AKOwnerForm, AKSubmissionForm, AKWishForm
AKForm
class SubmissionErrorNotConfiguredView(EventSlugMixin, TemplateView): class SubmissionErrorNotConfiguredView(EventSlugMixin, TemplateView):
...@@ -47,7 +45,7 @@ class AKOverviewView(FilterByEventSlugMixin, ListView): ...@@ -47,7 +45,7 @@ class AKOverviewView(FilterByEventSlugMixin, ListView):
template_name = "AKSubmission/ak_overview.html" template_name = "AKSubmission/ak_overview.html"
wishes_as_category = False wishes_as_category = False
def filter_aks(self, context, category): # pylint: disable=unused-argument def filter_aks(self, context, category): # pylint: disable=unused-argument
""" """
Filter which AKs to display based on the given context and category Filter which AKs to display based on the given context and category
...@@ -73,7 +71,7 @@ class AKOverviewView(FilterByEventSlugMixin, ListView): ...@@ -73,7 +71,7 @@ class AKOverviewView(FilterByEventSlugMixin, ListView):
""" """
return context["categories_with_aks"][0][0].name return context["categories_with_aks"][0][0].name
def get_table_title(self, context): # pylint: disable=unused-argument def get_table_title(self, context): # pylint: disable=unused-argument
""" """
Specify the title above the AK list/table in this view Specify the title above the AK list/table in this view
...@@ -91,7 +89,7 @@ class AKOverviewView(FilterByEventSlugMixin, ListView): ...@@ -91,7 +89,7 @@ class AKOverviewView(FilterByEventSlugMixin, ListView):
redirect to error page if necessary (see :class:`SubmissionErrorNotConfiguredView`) redirect to error page if necessary (see :class:`SubmissionErrorNotConfiguredView`)
""" """
self._load_event() self._load_event()
self.object_list = self.get_queryset() # pylint: disable=attribute-defined-outside-init self.object_list = self.get_queryset() # pylint: disable=attribute-defined-outside-init
# No categories yet? Redirect to configuration error page # No categories yet? Redirect to configuration error page
if self.object_list.count() == 0: if self.object_list.count() == 0:
...@@ -124,7 +122,7 @@ class AKOverviewView(FilterByEventSlugMixin, ListView): ...@@ -124,7 +122,7 @@ class AKOverviewView(FilterByEventSlugMixin, ListView):
if self.wishes_as_category: if self.wishes_as_category:
categories_with_aks.append( categories_with_aks.append(
(AKCategory(name=_("Wishes"), pk=0, description=_("AKs one would like to have")), ak_wishes)) (AKCategory(name=_("Wishes"), pk=0, description=_("AKs one would like to have")), ak_wishes))
context["categories_with_aks"] = categories_with_aks context["categories_with_aks"] = categories_with_aks
context["active_category"] = self.get_active_category_name(context) context["active_category"] = self.get_active_category_name(context)
...@@ -183,10 +181,13 @@ class AKListByCategoryView(AKOverviewView): ...@@ -183,10 +181,13 @@ class AKListByCategoryView(AKOverviewView):
This view inherits from :class:`AKOverviewView`, but produces only one list instead of a tabbed one. This view inherits from :class:`AKOverviewView`, but produces only one list instead of a tabbed one.
""" """
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
# Override dispatching # Override dispatching
# Needed to handle the checking whether the category exists # Needed to handle the checking whether the category exists
self.category = get_object_or_404(AKCategory, pk=kwargs['category_pk']) # pylint: disable=attribute-defined-outside-init,line-too-long # noinspection PyAttributeOutsideInit
# pylint: disable=attribute-defined-outside-init
self.category = get_object_or_404(AKCategory, pk=kwargs['category_pk'])
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_active_category_name(self, context): def get_active_category_name(self, context):
...@@ -209,11 +210,12 @@ class AKListByTrackView(AKOverviewView): ...@@ -209,11 +210,12 @@ class AKListByTrackView(AKOverviewView):
This view inherits from :class:`AKOverviewView` and there will be one list per category This view inherits from :class:`AKOverviewView` and there will be one list per category
-- but only AKs of a certain given track will be included in them. -- but only AKs of a certain given track will be included in them.
""" """
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
# Override dispatching # Override dispatching
# Needed to handle the checking whether the track exists # Needed to handle the checking whether the track exists
self.track = get_object_or_404(AKTrack, pk=kwargs['track_pk']) # pylint: disable=attribute-defined-outside-init self.track = get_object_or_404(AKTrack, pk=kwargs['track_pk']) # pylint: disable=attribute-defined-outside-init
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def filter_aks(self, context, category): def filter_aks(self, context, category):
...@@ -292,6 +294,7 @@ class EventInactiveRedirectMixin: ...@@ -292,6 +294,7 @@ class EventInactiveRedirectMixin:
Will add a message explaining why the action was not performed to the user Will add a message explaining why the action was not performed to the user
and then redirect to start page of the submission component and then redirect to start page of the submission component
""" """
def get_error_message(self): def get_error_message(self):
""" """
Error message to display after redirect (can be adjusted by this method) Error message to display after redirect (can be adjusted by this method)
...@@ -351,6 +354,7 @@ class AKSubmissionView(AKAndAKWishSubmissionView): ...@@ -351,6 +354,7 @@ class AKSubmissionView(AKAndAKWishSubmissionView):
Extends :class:`AKAndAKWishSubmissionView` Extends :class:`AKAndAKWishSubmissionView`
""" """
def get_initial(self): def get_initial(self):
# Load initial values for the form # Load initial values for the form
# Used to directly add the first owner and the event this AK will belong to # Used to directly add the first owner and the event this AK will belong to
...@@ -500,7 +504,6 @@ class AKOwnerDispatchView(ABC, EventSlugMixin, View): ...@@ -500,7 +504,6 @@ class AKOwnerDispatchView(ABC, EventSlugMixin, View):
:rtype: HttpResponseRedirect :rtype: HttpResponseRedirect
""" """
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
# This view is solely meant to handle POST requests # This view is solely meant to handle POST requests
# Perform dispatching based on the submitted owner_id # Perform dispatching based on the submitted owner_id
......