Skip to content
Snippets Groups Projects

Compare revisions

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

Source

Select target project
No results found

Target

Select target project
  • konstantin/akplanning
  • matedealer/akplanning
  • kif/akplanning
  • mirco/akplanning
  • lordofthevoid/akplanning
  • voidptr/akplanning
  • xayomer/akplanning-fork
  • mollux/akplanning
  • neumantm/akplanning
  • mmarx/akplanning
  • nerf/akplanning
  • felix_bonn/akplanning
  • sebastian.uschmann/akplanning
13 results
Show changes
Showing
with 493 additions and 89 deletions
......@@ -7,6 +7,11 @@
{% block title %}{% trans "New event wizard" %}: {{ wizard_step_text }}{% endblock %}
{% block extrahead %}
{{ block.super }}
{{ form.media }}
{% endblock %}
{% block content %}
{% include "admin/AKModel/event_wizard/wizard_steps.html" %}
......
{% load tz %}
{% load fontawesome_6 %}
{% timezone event.timezone %}
<table class="table table-striped">
......@@ -7,7 +8,10 @@
<span class="text-secondary float-end">
{{ message.timestamp|date:"Y-m-d H:i:s" }}
</span>
<h5><a href="{{ message.ak.detail_url }}">{{ message.ak }}</a></h5>
<h5><a href="{{ message.ak.detail_url }}">
{% if message.resolved %}{% fa6_icon "check-circle" %} {% endif %}
{{ message.ak }}
</a></h5>
<p>{{ message.text }}</p>
</td></tr>
{% endfor %}
......
......@@ -3,35 +3,65 @@ from django.apps import apps
from django.conf import settings
from django.utils.html import format_html, mark_safe, conditional_escape
from django.templatetags.static import static
from django.template.defaultfilters import date
from fontawesome_6.app_settings import get_css
from AKModel.models import Event
register = template.Library()
# Get Footer Info from settings
@register.simple_tag
def footer_info():
"""
Get Footer Info from settings
:return: a dict of several strings like the impress URL to use in the footer
:rtype: Dict[str, str]
"""
return settings.FOOTER_INFO
@register.filter
def check_app_installed(name):
"""
Check whether the app with the given name is active in this instance
:param name: name of the app to check for
:return: true if app is installed
:rtype: bool
"""
return apps.is_installed(name)
@register.filter
def message_bootstrap_class(tag):
"""
Turn message severity classes into corresponding bootstrap css classes
:param tag: severity of the message
:return: matching bootstrap class
"""
if tag == "error":
return "alert-danger"
elif tag == "success":
if tag == "success":
return "alert-success"
elif tag == "warning":
if tag == "warning":
return "alert-warning"
return "alert-info"
@register.filter
def wiki_owners_export(owners, event):
"""
Preserve owner link information for wiki export by using internal links if possible
but external links when owner specified a non-wikilink. This is applied to the full list of owners
:param owners: list of owners
:param event: event this owner belongs to and that is currently exported (specifying this directly prevents unnecessary database lookups) #pylint: disable=line-too-long
:return: linkified owners list in wiki syntax
:rtype: str
"""
def to_link(owner):
if owner.link != '':
event_link_prefix, _ = event.base_url.rsplit("/", 1)
......@@ -44,17 +74,45 @@ def wiki_owners_export(owners, event):
return ", ".join(to_link(owner) for owner in owners.all())
@register.filter
def event_month_year(event:Event):
"""
Print rough event date (month and year)
:param event: event to print the date for
:return: string containing rough date information for event
"""
if event.start.month == event.end.month:
return f"{date(event.start, 'F')} {event.start.year}"
event_start_string = date(event.start, 'F')
if event.start.year != event.end.year:
event_start_string = f"{event_start_string} {event.start.year}"
return f"{event_start_string} - {date(event.end, 'F')} {event.end.year}"
# get list of relevant css fontawesome css files for this instance
css = get_css()
@register.simple_tag
def fontawesome_6_css():
"""
Create html code to load all required fontawesome css files
:return: HTML code to load css
:rtype: str
"""
return mark_safe(conditional_escape('\n').join(format_html(
'<link href="{}" rel="stylesheet" media="all">', stylesheet) for stylesheet in css))
@register.simple_tag
def fontawesome_6_js():
"""
Create html code to load all required fontawesome javascript files
:return: HTML code to load js
:rtype: str
"""
return mark_safe(format_html(
'<script type="text/javascript" src="{}"></script>', static('fontawesome_6/js/django-fontawesome.js')
))
\ No newline at end of file
))
import traceback
from typing import List
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages
from django.contrib.messages.storage.base import Message
from django.test import TestCase
......@@ -12,21 +12,43 @@ from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, A
class BasicViewTests:
"""
Parent class for "standard" tests of views
Provided with a list of views and arguments (if necessary), this will test that views
- render correctly without errors
- are only reachable with the correct rights (neither too freely nor too restricted)
To do this, the test creates sample users, fixtures are loaded automatically by the django test framework.
It also provides helper functions, e.g., to check for correct messages to the user or more simply generate
the URLs to test
In this class, methods from :class:`TestCase` will be called at multiple places event though TestCase is not a
parent of this class but has to be included as parent in concrete implementations of this class seperately.
It however still makes sense to treat this class as some kind of mixin and not implement it as a child of TestCase,
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.
"""
# pylint: disable=no-member
VIEWS = []
APP_NAME = ''
VIEWS_STAFF_ONLY = []
EDIT_TESTCASES = []
def setUp(self):
self.staff_user = User.objects.create(
def setUp(self): # pylint: disable=invalid-name
"""
Setup testing by creating sample users
"""
user_model = get_user_model()
self.staff_user = user_model.objects.create(
username='Test Staff User', email='teststaff@example.com', password='staffpw',
is_staff=True, is_active=True
)
self.admin_user = User.objects.create(
self.admin_user = user_model.objects.create(
username='Test Admin User', email='testadmin@example.com', password='adminpw',
is_staff=True, is_superuser=True, is_active=True
)
self.deactivated_user = User.objects.create(
self.deactivated_user = user_model.objects.create(
username='Test Deactivated User', email='testdeactivated@example.com', password='deactivatedpw',
is_staff=True, is_active=False
)
......@@ -45,6 +67,13 @@ class BasicViewTests:
return view_name_with_prefix, url
def _assert_message(self, response, expected_message, msg_prefix=""):
"""
Assert that the correct message is shown and cause test to fail if not
:param response: response to check
:param expected_message: message that should be shown
:param msg_prefix: prefix for the error message when test fails
"""
messages:List[Message] = list(get_messages(response.wsgi_request))
msg_count = "No message shown to user"
......@@ -59,60 +88,83 @@ class BasicViewTests:
self.assertEqual(messages[-1].message, expected_message, msg=msg_content)
def test_views_for_200(self):
"""
Test the list of public views (as specified in "VIEWS") for error-free rendering
"""
for view_name in self.VIEWS:
view_name_with_prefix, url = self._name_and_url(view_name)
try:
response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"{view_name_with_prefix} ({url}) broken")
except Exception as e:
self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):\n\n{traceback.format_exc()}")
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):
"""
Test whether internal views (as specified in "VIEWS_STAFF_ONLY" are visible to staff users and staff users only
"""
# Not logged in? Views should not be visible
self.client.logout()
for view_name in self.VIEWS_STAFF_ONLY:
view_name_with_prefix, url = self._name_and_url(view_name)
for view_name_info in self.VIEWS_STAFF_ONLY:
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)
response = self.client.get(url)
self.assertEqual(response.status_code, 302, msg=f"{view_name_with_prefix} ({url}) accessible by non-staff")
self.assertEqual(response.status_code, expected_response_code,
msg=f"{view_name_with_prefix} ({url}) accessible by non-staff")
# Logged in? Views should be visible
self.client.force_login(self.staff_user)
for view_name in self.VIEWS_STAFF_ONLY:
view_name_with_prefix, url = self._name_and_url(view_name)
for view_name_info in self.VIEWS_STAFF_ONLY:
view_name_with_prefix, url = self._name_and_url(view_name_info)
try:
response = self.client.get(url)
self.assertEqual(response.status_code, 200,
msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)")
except Exception as e:
self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):\n\n{traceback.format_exc()}")
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()}")
# Disabled user? Views should not be visible
self.client.force_login(self.deactivated_user)
for view_name in self.VIEWS_STAFF_ONLY:
view_name_with_prefix, url = self._name_and_url(view_name)
for view_name_info in self.VIEWS_STAFF_ONLY:
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)
response = self.client.get(url)
self.assertEqual(response.status_code, 302,
self.assertEqual(response.status_code, expected_response_code,
msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user")
def _to_sendable_value(self, v):
def _to_sendable_value(self, val):
"""
Create representation sendable via POST from form data
:param v: value to prepare
:type v: any
Needed to automatically check create, update and delete views
:param val: value to prepare
:type val: any
:return: prepared value (normally either raw value or primary key of complex object)
"""
if type(v) == list:
return [e.pk for e in v]
if type(v) == "RelatedManager":
return [e.pk for e in v.all()]
return v
if isinstance(val, list):
return [e.pk for e in val]
if type(val) == "RelatedManager": # pylint: disable=unidiomatic-typecheck
return [e.pk for e in val.all()]
return val
def test_submit_edit_form(self):
"""
Test edit forms in the most simple way (sending them again unchanged)
Test edit forms (as specified in "EDIT_TESTCASES") in the most simple way (sending them again unchanged)
"""
for testcase in self.EDIT_TESTCASES:
self._test_submit_edit_form(testcase)
def _test_submit_edit_form(self, testcase):
"""
Test a single edit form by rendering and sending it again unchanged
This will test for correct rendering, dispatching/redirecting, messages and access control handling
:param testcase: details of the form to test
"""
name, url = self._name_and_url((testcase["view"], testcase["kwargs"]))
form_name = testcase.get("form_name", "form")
expected_code = testcase.get("expected_code", 302)
......@@ -145,6 +197,9 @@ class BasicViewTests:
class ModelViewTests(BasicViewTests, TestCase):
"""
Basic view test cases for views from AKModel plus some custom tests
"""
fixtures = ['model.json']
ADMIN_MODELS = [
......@@ -172,35 +227,48 @@ class ModelViewTests(BasicViewTests, TestCase):
]
def test_admin(self):
"""
Test basic admin functionality (displaying and interacting with model instances)
"""
self.client.force_login(self.admin_user)
for model in self.ADMIN_MODELS:
# Special treatment for a subset of views (where we exchanged default functionality, e.g., create views)
if model[1] == "event":
view_name_with_prefix, url = self._name_and_url((f'admin:new_event_wizard_start', {}))
_, url = self._name_and_url(('admin:new_event_wizard_start', {}))
elif model[1] == "room":
view_name_with_prefix, url = self._name_and_url((f'admin:room-new', {}))
_, url = self._name_and_url(('admin:room-new', {}))
# Otherwise, just call the creation form view
else:
view_name_with_prefix, 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)
self.assertEqual(response.status_code, 200, msg=f"Add form for model {model[1]} ({url}) broken")
for model in self.ADMIN_MODELS:
# Test the update view using the first existing instance of each model
m = model[0].objects.first()
if m is not None:
view_name_with_prefix, url = self._name_and_url((f'admin:AKModel_{model[1]}_change', {'object_id': m.pk}))
_, url = self._name_and_url(
(f'admin:AKModel_{model[1]}_change', {'object_id': m.pk})
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"Edit form for model {model[1]} ({url}) broken")
def test_wiki_export(self):
"""
Test wiki export
This will test whether the view renders at all and whether the export list contains the correct AKs
"""
self.client.force_login(self.admin_user)
export_url = reverse_lazy(f"admin:ak_wiki_export", kwargs={'slug': 'kif42'})
export_url = reverse_lazy("admin:ak_wiki_export", kwargs={'slug': 'kif42'})
response = self.client.get(export_url)
self.assertEqual(response.status_code, 200, "Export not working at all")
export_count = 0
for category, aks in response.context["categories_with_aks"]:
for _, aks in response.context["categories_with_aks"]:
for ak in aks:
self.assertEqual(ak.include_in_export, True, f"AK with export flag set to False (pk={ak.pk}) included in export")
self.assertEqual(ak.include_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
......
......@@ -4,12 +4,15 @@ from django.urls import include, path
from rest_framework.routers import DefaultRouter
import AKModel.views.api
from AKModel.views.manage import ExportSlidesView
from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, \
AKsByUserView
from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView
from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \
NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, NewEventWizardSettingsView
from AKModel.views.room import RoomBatchCreationView
from AKModel.views.status import EventStatusView
# Register basic API views/endpoints
api_router = DefaultRouter()
api_router.register('akowner', AKModel.views.api.AKOwnerViewSet, basename='AKOwner')
api_router.register('akcategory', AKModel.views.api.AKCategoryViewSet, basename='AKCategory')
......@@ -18,7 +21,9 @@ api_router.register('ak', AKModel.views.api.AKViewSet, basename='AK')
api_router.register('room', AKModel.views.api.RoomViewSet, basename='Room')
api_router.register('akslot', AKModel.views.api.AKSlotViewSet, basename='AKSlot')
# TODO Can we move this functionality to the individual apps instead?
extra_paths = []
# If AKScheduling is active, register additional API endpoints
if apps.is_installed("AKScheduling"):
from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView, EventsViewSet, \
ConstraintViolationsViewSet, DefaultSlotsView
......@@ -33,9 +38,10 @@ if apps.is_installed("AKScheduling"):
name='scheduling-room-availabilities')),
extra_paths.append(path('api/scheduling-default-slots/', DefaultSlotsView.as_view(),
name='scheduling-default-slots'))
#If AKSubmission is active, register an additional API endpoint for increasing the interest counter
if apps.is_installed("AKSubmission"):
from AKSubmission.api import increment_interest_counter
extra_paths.append(path('api/ak/<pk>/indicate-interest/', increment_interest_counter, name='submission-ak-indicate-interest'))
event_specific_paths = [
......@@ -45,6 +51,7 @@ event_specific_paths.extend(extra_paths)
app_name = 'model'
# Included all these extra view paths at a path starting with the event slug
urlpatterns = [
path(
'<slug:event_slug>/',
......@@ -55,6 +62,9 @@ urlpatterns = [
def get_admin_urls_event_wizard(admin_site):
"""
Defines all additional URLs for the event creation wizard
"""
return [
path('add/wizard/start/', admin_site.admin_view(NewEventWizardStartView.as_view()),
name="new_event_wizard_start"),
......@@ -75,10 +85,15 @@ def get_admin_urls_event_wizard(admin_site):
def get_admin_urls_event(admin_site):
"""
Defines all additional event-related view URLs that will be included in the event admin interface
"""
return [
path('<slug:event_slug>/status/', admin_site.admin_view(EventStatusView.as_view()), name="event_status"),
path('<slug:event_slug>/requirements/', admin_site.admin_view(AKRequirementOverview.as_view()),
name="event_requirement_overview"),
path('<slug:event_slug>/aks/owner/<pk>/', admin_site.admin_view(AKsByUserView.as_view()),
name="aks_by_owner"),
path('<slug:event_slug>/ak-csv-export/', admin_site.admin_view(AKCSVExportView.as_view()),
name="ak_csv_export"),
path('<slug:slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()),
......@@ -86,4 +101,10 @@ def get_admin_urls_event(admin_site):
path('<slug:event_slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()),
name="ak_delete_orga_messages"),
path('<slug:event_slug>/ak-slide-export/', admin_site.admin_view(ExportSlidesView.as_view()), name="ak_slide_export"),
path('plan/publish/', admin_site.admin_view(PlanPublishView.as_view()), name="plan-publish"),
path('plan/unpublish/', admin_site.admin_view(PlanUnpublishView.as_view()), name="plan-unpublish"),
path('<slug:event_slug>/defaultSlots/', admin_site.admin_view(DefaultSlotEditorView.as_view()),
name="default-slots-editor"),
path('<slug:event_slug>/importRooms/', admin_site.admin_view(RoomBatchCreationView.as_view()),
name="room-import"),
]
......@@ -9,6 +9,9 @@ from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK
class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
"""
View: Display requirements for the given event
"""
model = AKRequirement
context_object_name = "requirements"
title = _("Requirements for Event")
......@@ -22,6 +25,9 @@ class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
"""
View: Export all AK slots of this event in CSV format ordered by tracks
"""
template_name = "admin/AKModel/ak_csv_export.html"
model = AKSlot
context_object_name = "slots"
......@@ -30,12 +36,12 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
def get_queryset(self):
return super().get_queryset().order_by("ak__track")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
class AKWikiExportView(AdminViewMixin, DetailView):
"""
View: Export AKs of this event in wiki syntax
This will show one text field per category, with a separate category/field for wishes
"""
template_name = "admin/AKModel/wiki_export.html"
model = Event
context_object_name = "event"
......@@ -46,7 +52,7 @@ class AKWikiExportView(AdminViewMixin, DetailView):
categories_with_aks, ak_wishes = context["event"].get_categories_with_aks(
wishes_seperately=True,
filter=lambda ak: ak.include_in_export
filter_func=lambda ak: ak.include_in_export
)
context["categories_with_aks"] = [(category.name, ak_list) for category, ak_list in categories_with_aks]
......@@ -56,10 +62,18 @@ class AKWikiExportView(AdminViewMixin, DetailView):
class AKMessageDeleteView(EventSlugMixin, IntermediateAdminView):
"""
View: Confirmation page to delete confidential AK-related messages to orga
Confirmation functionality provided by :class:`AKModel.metaviews.admin.IntermediateAdminView`
"""
template_name = "admin/AKModel/message_delete.html"
title = _("Delete AK Orga Messages")
def get_orga_messages_for_event(self, event):
"""
Get all orga messages for the given event
"""
return AKOrgaMessage.objects.filter(ak__event=event)
def get_success_url(self):
......@@ -77,6 +91,11 @@ class AKMessageDeleteView(EventSlugMixin, IntermediateAdminView):
class AKResetInterestView(IntermediateAdminActionView):
"""
View: Confirmation page to reset all manually specified interest values
Confirmation functionality provided by :class:`AKModel.metaviews.admin.IntermediateAdminView`
"""
title = _("Reset interest in AKs")
model = AK
confirmation_message = _("Interest of the following AKs will be set to not filled (-1):")
......@@ -87,6 +106,11 @@ class AKResetInterestView(IntermediateAdminActionView):
class AKResetInterestCounterView(IntermediateAdminActionView):
"""
View: Confirmation page to reset all interest counters (online interest indication)
Confirmation functionality provided by :class:`AKModel.metaviews.admin.IntermediateAdminView`
"""
title = _("Reset AKs' interest counters")
model = AK
confirmation_message = _("Interest counter of the following AKs will be set to 0:")
......
......@@ -7,6 +7,10 @@ from AKModel.serializers import AKOwnerSerializer, AKCategorySerializer, AKTrack
class AKOwnerViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
API View: Owners (restricted to those of the given event)
Read-only
"""
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKOwnerSerializer
......@@ -15,6 +19,10 @@ class AKOwnerViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModel
class AKCategoryViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
API View: Categories (restricted to those of the given event)
Read-only
"""
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKCategorySerializer
......@@ -24,6 +32,10 @@ class AKCategoryViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListMo
class AKTrackViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
API View: Tracks (restricted to those of the given event)
Read, Write, Delete
"""
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKTrackSerializer
......@@ -33,6 +45,10 @@ class AKTrackViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateMod
class AKViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.ListModelMixin,
viewsets.GenericViewSet):
"""
API View: AKs (restricted to those of the given event)
Read, Write
"""
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKSerializer
......@@ -41,6 +57,10 @@ class AKViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMix
class RoomViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
API View: Rooms (restricted to those of the given event)
Read-only
"""
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = RoomSerializer
......@@ -50,6 +70,10 @@ class RoomViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMix
class AKSlotViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin,
mixins.ListModelMixin, viewsets.GenericViewSet):
"""
API View: AK slots (restricted to those of the given event)
Read, Write
"""
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKSlotSerializer
......
......@@ -12,6 +12,12 @@ from AKModel.models import Event
class NewEventWizardStartView(AdminViewMixin, WizardViewMixin, CreateView):
"""
Wizard view: Entry/Start
Specify basic settings, especially the timezone for correct time treatment in the next view
(:class:`NewEventWizardSettingsView`) where this view will redirect to without saving the new event already
"""
model = Event
form_class = NewEventWizardStartForm
template_name = "admin/AKModel/event_wizard/start.html"
......@@ -19,6 +25,16 @@ class NewEventWizardStartView(AdminViewMixin, WizardViewMixin, CreateView):
class NewEventWizardSettingsView(AdminViewMixin, WizardViewMixin, CreateView):
"""
Wizard view: Event settings
Specify most of the event settings. The user will see that certain fields are required since they were lead here
from another form in :class:`NewEventWizardStartView` that did not contain these fields even though they are
mandatory for the event model
Next step will then be :class:`NewEventWizardPrepareImportView` to prepare copy configuration elements
from an existing event
"""
model = Event
form_class = NewEventWizardSettingsForm
template_name = "admin/AKModel/event_wizard/settings.html"
......@@ -34,6 +50,14 @@ class NewEventWizardSettingsView(AdminViewMixin, WizardViewMixin, CreateView):
class NewEventWizardPrepareImportView(WizardViewMixin, EventSlugMixin, FormView):
"""
Wizard view: Choose event to copy configuration elements from
The user can here select an existing event to copy elements like requirements, categories and dashboard buttons from
The exact subset of elements to copy from can then be selected in the next view (:class:`NewEventWizardImportView`)
Instead, this step can be skipped by directly continuing with :class:`NewEventWizardActivateView`
"""
form_class = NewEventWizardPrepareImportForm
template_name = "admin/AKModel/event_wizard/created_prepare_import.html"
wizard_step = 3
......@@ -45,29 +69,40 @@ class NewEventWizardPrepareImportView(WizardViewMixin, EventSlugMixin, FormView)
class NewEventWizardImportView(EventSlugMixin, WizardViewMixin, FormView):
"""
Wizard view: Select configuration elements to copy
Displays lists of requirements, categories and dashboard buttons that the user can select entries to be copied from
Afterwards, the event can be activated in :class:`NewEventWizardActivateView`
"""
form_class = NewEventWizardImportForm
template_name = "admin/AKModel/event_wizard/import.html"
wizard_step = 4
def get_initial(self):
initial = super().get_initial()
# Remember which event was selected and send it again when submitting the form for validation
initial["import_event"] = Event.objects.get(slug=self.kwargs["import_slug"])
return initial
def form_valid(self, form):
# pylint: disable=consider-using-f-string
import_types = ["import_categories", "import_requirements"]
if apps.is_installed("AKDashboard"):
import_types.append("import_buttons")
# Loop over all kinds of configuration elements and then over all selected elements of each type
# and try to clone them by requesting a new primary key, adapting the event and then storing the
# object in the database
for import_type in import_types:
for import_obj in form.cleaned_data.get(import_type):
# clone existing entry
try:
import_obj.event = self.event
import_obj.pk = None
import_obj.save()
messages.add_message(self.request, messages.SUCCESS, _("Copied '%(obj)s'" % {'obj': import_obj}))
except BaseException as e:
except BaseException as e: # pylint: disable=broad-exception-caught
messages.add_message(self.request, messages.ERROR,
_("Could not copy '%(obj)s' (%(error)s)" % {'obj': import_obj,
"error": str(e)}))
......@@ -75,6 +110,17 @@ class NewEventWizardImportView(EventSlugMixin, WizardViewMixin, FormView):
class NewEventWizardActivateView(WizardViewMixin, UpdateView):
"""
Wizard view: Allow activating the event
The user is asked to make the created event active. This is done in this step and not already during the creation
in the second step of the wizard to prevent users seeing an unconfigured submission.
The event will nevertheless already be visible in the dashboard before, when a public event was created in
:class:`NewEventWizardSettingsView`.
In the following last step (:class:`NewEventWizardFinishView`), a confirmation of the full process and some
details of the created event are shown
"""
model = Event
template_name = "admin/AKModel/event_wizard/activate.html"
form_class = NewEventWizardActivateForm
......@@ -85,6 +131,11 @@ class NewEventWizardActivateView(WizardViewMixin, UpdateView):
class NewEventWizardFinishView(WizardViewMixin, DetailView):
"""
Wizard view: Confirmation and summary
Show a confirmation and a summary of the created event
"""
model = Event
template_name = "admin/AKModel/event_wizard/finish.html"
wizard_step = 6
......@@ -8,26 +8,38 @@ from django.contrib import messages
from django.db.models.functions import Now
from django.utils.dateparse import parse_datetime
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView
from django.views.generic import TemplateView, DetailView
from django_tex.core import render_template_with_context, run_tex_in_directory
from django_tex.response import PDFResponse
from AKModel.forms import SlideExportForm, DefaultSlotEditorForm
from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView
from AKModel.models import ConstraintViolation, Event, DefaultSlot
from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin
from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner
class UserView(TemplateView):
"""
View: Start page for logged in user
Will over a link to backend or inform the user that their account still needs to be confirmed
"""
template_name = "AKModel/user.html"
class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
"""
View: Export slides to present AKs
Over a form to choose some settings for the export and then generate the PDF
"""
title = _('Export AK Slides')
form_class = SlideExportForm
def form_valid(self, form):
# pylint: disable=invalid-name
template_name = 'admin/AKModel/export/slides.tex'
# Settings
NEXT_AK_LIST_LENGTH = form.cleaned_data['num_next']
RESULT_PRESENTATION_MODE = form.cleaned_data["presentation_mode"]
SPACE_FOR_NOTES_IN_WISHES = form.cleaned_data["wish_notes"]
......@@ -42,12 +54,18 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
}
def build_ak_list_with_next_aks(ak_list):
"""
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)
return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=list())]
return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=[])]
categories_with_aks, ak_wishes = self.event.get_categories_with_aks(wishes_seperately=True, filter=lambda
# 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)
categories_with_aks, ak_wishes = self.event.get_categories_with_aks(wishes_seperately=True, filter_func=lambda
ak: not RESULT_PRESENTATION_MODE or (ak.present or (ak.present is None and ak.category.present_by_default)))
# Create context for LaTeX rendering
context = {
'title': self.event.name,
'categories_with_aks': [(category, build_ak_list_with_next_aks(ak_list)) for category, ak_list in
......@@ -67,11 +85,17 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
os.remove(f'{tempdir}/texput.tex')
pdf = run_tex_in_directory(source, tempdir, template_name=self.template_name)
# Show PDF file to the user (with a filename containing a timestamp to prevent confusions about the right
# version to use when generating multiple versions of the slides, e.g., because owners did last-minute changes
# to their AKs
timestamp = datetime.datetime.now(tz=self.event.timezone).strftime("%Y-%m-%d_%H_%M")
return PDFResponse(pdf, filename=f'{self.event.slug}_ak_slides_{timestamp}.pdf')
class CVMarkResolvedView(IntermediateAdminActionView):
"""
Admin action view: Mark one or multitple constraint violation(s) as resolved
"""
title = _('Mark Constraint Violations as manually resolved')
model = ConstraintViolation
confirmation_message = _("The following Constraint Violations will be marked as manually resolved")
......@@ -82,6 +106,9 @@ class CVMarkResolvedView(IntermediateAdminActionView):
class CVSetLevelViolationView(IntermediateAdminActionView):
"""
Admin action view: Set one or multitple constraint violation(s) as to level "violation"
"""
title = _('Set Constraint Violations to level "violation"')
model = ConstraintViolation
confirmation_message = _("The following Constraint Violations will be set to level 'violation'")
......@@ -92,6 +119,9 @@ class CVSetLevelViolationView(IntermediateAdminActionView):
class CVSetLevelWarningView(IntermediateAdminActionView):
"""
Admin action view: Set one or multitple constraint violation(s) as to level "warning"
"""
title = _('Set Constraint Violations to level "warning"')
model = ConstraintViolation
confirmation_message = _("The following Constraint Violations will be set to level 'warning'")
......@@ -102,6 +132,9 @@ class CVSetLevelWarningView(IntermediateAdminActionView):
class PlanPublishView(IntermediateAdminActionView):
"""
Admin action view: Publish the plan of one or multitple event(s)
"""
title = _('Publish plan')
model = Event
confirmation_message = _('Publish the plan(s) of:')
......@@ -112,6 +145,9 @@ class PlanPublishView(IntermediateAdminActionView):
class PlanUnpublishView(IntermediateAdminActionView):
"""
Admin action view: Unpublish the plan of one or multitple event(s)
"""
title = _('Unpublish plan')
model = Event
confirmation_message = _('Unpublish the plan(s) of:')
......@@ -122,6 +158,9 @@ class PlanUnpublishView(IntermediateAdminActionView):
class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
"""
Admin view: Allow to edit the default slots of an event
"""
template_name = "admin/AKModel/default_slot_editor.html"
form_class = DefaultSlotEditorForm
title = _("Edit Default Slots")
......@@ -149,13 +188,14 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
previous_slot_ids = set(s.id for s in self.event.defaultslot_set.all())
# Loop over inputs and update or add slots
for slot in default_slots_raw:
start = parse_datetime(slot["start"]).astimezone(tz)
end = parse_datetime(slot["end"]).astimezone(tz)
start = parse_datetime(slot["start"]).replace(tzinfo=tz)
end = parse_datetime(slot["end"]).replace(tzinfo=tz)
if slot["id"] != '':
id = int(slot["id"])
if id not in previous_slot_ids:
slot_id = int(slot["id"])
if slot_id not in previous_slot_ids:
# Make sure only slots (currently) belonging to this event are edited
# (user did not manipulate IDs and slots have not been deleted in another session in the meantime)
messages.add_message(
......@@ -166,8 +206,8 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
)
else:
# Update existing entries
previous_slot_ids.remove(id)
original_slot = DefaultSlot.objects.get(id=id)
previous_slot_ids.remove(slot_id)
original_slot = DefaultSlot.objects.get(id=slot_id)
if original_slot.start != start or original_slot.end != end:
original_slot.start = start
original_slot.end = end
......@@ -187,6 +227,7 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
for d_id in previous_slot_ids:
DefaultSlot.objects.get(id=d_id).delete()
# Inform user about changes performed
if created_count + updated_count + deleted_count > 0:
messages.add_message(
self.request,
......@@ -195,3 +236,12 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
.format(u=str(updated_count), c=str(created_count), d=str(deleted_count))
)
return super().form_valid(form)
class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView):
"""
View: Show all AKs of a given user
"""
model = AKOwner
context_object_name = 'owner'
template_name = "admin/AKModel/aks_by_user.html"
......@@ -15,6 +15,9 @@ from AKModel.models import Room
class RoomCreationView(AdminViewMixin, CreateView):
"""
Admin view: Create a room
"""
form_class = RoomForm
template_name = 'admin/AKModel/room_create.html'
......@@ -22,18 +25,28 @@ class RoomCreationView(AdminViewMixin, CreateView):
print(self.request.POST['save_action'])
if self.request.POST['save_action'] == 'save_add_another':
return reverse_lazy('admin:room-new')
elif self.request.POST['save_action'] == 'save_continue':
if self.request.POST['save_action'] == 'save_continue':
return reverse_lazy('admin:AKModel_room_change', kwargs={'object_id': self.room.pk})
else:
return reverse_lazy('admin:AKModel_room_changelist')
return reverse_lazy('admin:AKModel_room_changelist')
def form_valid(self, form):
self.room = form.save()
self.room = form.save() # pylint: disable=attribute-defined-outside-init
# translatable string with placeholders, no f-string possible
# pylint: disable=consider-using-f-string
messages.success(self.request, _("Created Room '%(room)s'" % {'room': self.room}))
return HttpResponseRedirect(self.get_success_url())
class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView):
"""
Admin action: Allow to create rooms in batch by inputing a CSV-formatted list of room details into a textbox
This offers the input form, supports creation of virtual rooms if AKOnline is active, too,
and users can specify that default availabilities (from event start to end) should be created for the rooms
automatically
"""
form_class = RoomBatchCreationForm
title = _("Import Rooms from CSV")
......@@ -47,23 +60,33 @@ class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView):
rooms_raw_dict: csv.DictReader = form.cleaned_data["rooms"]
# Prepare creation of virtual rooms if there is information (an URL) in the data and the AKOnline app is active
if apps.is_installed("AKOnline") and "url" in rooms_raw_dict.fieldnames:
virtual_rooms_support = True
# pylint: disable=import-outside-toplevel
from AKOnline.models import VirtualRoom
# Loop over all inputs
for raw_room in rooms_raw_dict:
# Gather the relevant information (most fields can be empty)
name = raw_room["name"]
location = raw_room["location"] if "location" in rooms_raw_dict.fieldnames else ""
capacity = raw_room["capacity"] if "capacity" in rooms_raw_dict.fieldnames else -1
try:
# Try to create a room (catches cases where the room name contains keywords or symbols that the
# database cannot handle (.e.g., special UTF-8 characters)
r = Room.objects.create(name=name,
location=location,
capacity=capacity,
event=self.event)
# and if necessary an associated virtual room, too
if virtual_rooms_support and raw_room["url"] != "":
VirtualRoom.objects.create(room=r,
url=raw_room["url"])
# If user requested default availabilities, create them
if create_default_availabilities:
a = Availability.with_event_length(event=self.event, room=r)
a.save()
......@@ -72,6 +95,7 @@ class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView):
messages.add_message(self.request, messages.WARNING,
_("Could not import room {name}: {e}").format(name=name, e=str(e)))
# Inform the user about the rooms created
if created_count > 0:
messages.add_message(self.request, messages.SUCCESS,
_("Imported {count} room(s)").format(count=created_count))
......
from django.apps import apps
from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from AKModel.metaviews import status_manager
from AKModel.metaviews.admin import EventSlugMixin, AdminViewMixin
from AKModel.metaviews.admin import EventSlugMixin
from AKModel.metaviews.status import TemplateStatusWidget, StatusView
@status_manager.register(name="event_overview")
class EventOverviewWidget(TemplateStatusWidget):
"""
Status page widget: Event overview
"""
required_context_type = "event"
title = _("Overview")
template_name = "admin/AKModel/status/event_overview.html"
......@@ -20,6 +22,12 @@ class EventOverviewWidget(TemplateStatusWidget):
@status_manager.register(name="event_categories")
class EventCategoriesWidget(TemplateStatusWidget):
"""
Status page widget: Category information
Show all categories of the event together with the number of AKs belonging to this category.
Offers an action to add a new category.
"""
required_context_type = "event"
title = _("Categories")
template_name = "admin/AKModel/status/event_categories.html"
......@@ -31,7 +39,8 @@ class EventCategoriesWidget(TemplateStatusWidget):
]
def render_title(self, context: {}) -> str:
self.category_count = context['event'].akcategory_set.count()
# Store category count as instance variable for re-use in body
self.category_count = context['event'].akcategory_set.count() # pylint: disable=attribute-defined-outside-init
return f"{super().render_title(context)} ({self.category_count})"
def render_status(self, context: {}) -> str:
......@@ -40,6 +49,12 @@ class EventCategoriesWidget(TemplateStatusWidget):
@status_manager.register(name="event_rooms")
class EventRoomsWidget(TemplateStatusWidget):
"""
Status page widget: Category information
Show all rooms of the event.
Offers actions to add a single new room as well as for batch creation.
"""
required_context_type = "event"
title = _("Rooms")
template_name = "admin/AKModel/status/event_rooms.html"
......@@ -51,7 +66,8 @@ class EventRoomsWidget(TemplateStatusWidget):
]
def render_title(self, context: {}) -> str:
self.room_count = context['event'].room_set.count()
# Store room count as instance variable for re-use in body
self.room_count = context['event'].room_set.count() # pylint: disable=attribute-defined-outside-init
return f"{super().render_title(context)} ({self.room_count})"
def render_status(self, context: {}) -> str:
......@@ -59,10 +75,16 @@ class EventRoomsWidget(TemplateStatusWidget):
def render_actions(self, context: {}) -> list[dict]:
actions = super().render_actions(context)
# Action has to be added here since it depends on the event for URL building
import_room_url = reverse_lazy("admin:room-import", kwargs={"event_slug": context["event"].slug})
for action in actions:
if action["url"] == import_room_url:
return actions
actions.append(
{
"text": _("Import Rooms from CSV"),
"url": reverse_lazy("admin:room-import", kwargs={"event_slug": context["event"].slug}),
"url": import_room_url,
}
)
return actions
......@@ -70,6 +92,12 @@ class EventRoomsWidget(TemplateStatusWidget):
@status_manager.register(name="event_aks")
class EventAKsWidget(TemplateStatusWidget):
"""
Status page widget: AK information
Show information about the AKs of this event.
Offers a long list of AK-related actions and also scheduling actions of AKScheduling is active
"""
required_context_type = "event"
title = _("AKs")
template_name = "admin/AKModel/status/event_aks.html"
......@@ -88,22 +116,19 @@ class EventAKsWidget(TemplateStatusWidget):
]
if apps.is_installed("AKScheduling"):
actions.extend([
{
"text": format_html('{} <span class="badge bg-secondary">{}</span>',
_("Constraint Violations"),
context["event"].constraintviolation_set.count()),
"url": reverse_lazy("admin:constraint-violations", kwargs={"slug": context["event"].slug}),
},
{
"text": _("AKs requiring special attention"),
"url": reverse_lazy("admin:special-attention", kwargs={"slug": context["event"].slug}),
},
{
])
if context["event"].ak_set.count() > 0:
actions.append({
"text": _("Enter Interest"),
"url": reverse_lazy("admin:enter-interest",
kwargs={"event_slug": context["event"].slug, "pk": context["event"].ak_set.all().first().pk}),
},
])
kwargs={"event_slug": context["event"].slug,
"pk": context["event"].ak_set.all().first().pk}
),
})
actions.extend([
{
"text": _("Edit Default Slots"),
......@@ -132,11 +157,19 @@ class EventAKsWidget(TemplateStatusWidget):
@status_manager.register(name="event_requirements")
class EventRequirementsWidget(TemplateStatusWidget):
"""
Status page widget: Requirement information information
Show information about the requirements of this event.
Offers actions to add new requirements or to get a list of AKs having a given requirement.
"""
required_context_type = "event"
title = _("Requirements")
template_name = "admin/AKModel/status/event_requirements.html"
def render_title(self, context: {}) -> str:
# Store requirements count as instance variable for re-use in body
# pylint: disable=attribute-defined-outside-init
self.requirements_count = context['event'].akrequirement_set.count()
return f"{super().render_title(context)} ({self.requirements_count})"
......@@ -154,6 +187,9 @@ class EventRequirementsWidget(TemplateStatusWidget):
class EventStatusView(EventSlugMixin, StatusView):
"""
View: Show a status dashboard for the given event
"""
title = _("Event Status")
provided_context_type = "event"
......
......@@ -5,6 +5,9 @@ from AKOnline.models import VirtualRoom
@admin.register(VirtualRoom)
class VirtualRoomAdmin(admin.ModelAdmin):
"""
Admin interface for virtual room model
"""
model = VirtualRoom
list_display = ['room', 'event', 'url']
list_filter = ['room__event']
......
......@@ -2,4 +2,7 @@ from django.apps import AppConfig
class AkonlineConfig(AppConfig):
"""
App configuration (default -- only to set the app name)
"""
name = 'AKOnline'
......@@ -6,23 +6,38 @@ from AKOnline.models import VirtualRoom
class VirtualRoomForm(ModelForm):
"""
Form to create a virtual room
Should be used as part of a multi form (see :class:`RoomWithVirtualForm` below)
"""
class Meta:
model = VirtualRoom
exclude = ['room']
# Show all fields except for room
exclude = ['room'] #pylint: disable=modelform-uses-exclude
def __init__(self, *args, **kwargs):
super(VirtualRoomForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
# Make the URL field optional to allow submitting the multi form without creating a virtual room
self.fields['url'].required = False
class RoomWithVirtualForm(MultiModelForm):
"""
Combined form to create rooms and optionally virtual rooms
Multi-Form that combines a :class:`RoomForm` (from AKModel) and a :class:`VirtualRoomForm` (see above).
The form will always create a room on valid input
and may additionally create a virtual room if the url field of the virtual room form part is set.
"""
form_classes = {
'room': RoomForm,
'virtual': VirtualRoomForm
}
def save(self, commit=True):
objects = super(RoomWithVirtualForm, self).save(commit=False)
objects = super().save(commit=False)
if commit:
room = objects['room']
......
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-26 19:51+0200\n"
"POT-Creation-Date: 2023-08-16 16:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -34,7 +34,7 @@ msgstr "Raum"
msgid "Virtual Room"
msgstr "Virtueller Raum"
#: AKOnline/models.py:17 AKOnline/views.py:27
#: AKOnline/models.py:17 AKOnline/views.py:38
msgid "Virtual Rooms"
msgstr "Virtuelle Räume"
......@@ -42,12 +42,12 @@ msgstr "Virtuelle Räume"
msgid "Leave empty if that room is not virtual/hybrid."
msgstr "Leer lassen wenn der Raum nicht virtuell/hybrid ist"
#: AKOnline/views.py:18
#: AKOnline/views.py:25
#, python-format
msgid "Created Room '%(room)s'"
msgstr "Raum '%(room)s' angelegt"
#: AKOnline/views.py:20
#: AKOnline/views.py:28
#, python-format
msgid "Created related Virtual Room '%(vroom)s'"
msgstr "Verbundenen virtuellen Raum '%(vroom)s' angelegt"
from django.db import models
from django.utils.translation import gettext_lazy as _
from AKModel.models import Event, Room
from AKModel.models import Room
class VirtualRoom(models.Model):
......@@ -18,6 +18,12 @@ class VirtualRoom(models.Model):
@property
def event(self):
"""
Property: Event this virtual room belongs to.
:return: Event this virtual room belongs to
:rtype: Event
"""
return self.room.event
def __str__(self):
......
# Create your tests here.
......@@ -9,20 +9,31 @@ from AKOnline.forms import RoomWithVirtualForm
class RoomCreationWithVirtualView(RoomCreationView):
"""
View to create both rooms and optionally virtual rooms by filling one form
"""
form_class = RoomWithVirtualForm
template_name = 'admin/AKOnline/room_create_with_virtual.html'
room = None
def form_valid(self, form):
# This will create the room and additionally a virtual room if the url field is not blank
# objects['room'] will always a room instance afterwards, objects['virtual'] may be empty
objects = form.save()
self.room = objects['room']
messages.success(self.request, _("Created Room '%(room)s'" % {'room': objects['room']}))
# Create a (translated) success message containing information about the created room
messages.success(self.request, _("Created Room '%(room)s'" % {'room': objects['room']})) #pylint: disable=consider-using-f-string, line-too-long
if objects['virtual'] is not None:
messages.success(self.request, _("Created related Virtual Room '%(vroom)s'" % {'vroom': objects['virtual']}))
# Create a (translated) success message containing information about the created virtual room
messages.success(self.request, _("Created related Virtual Room '%(vroom)s'" % {'vroom': objects['virtual']})) #pylint: disable=consider-using-f-string, line-too-long
return HttpResponseRedirect(self.get_success_url())
@status_manager.register(name="event_virtual_rooms")
class EventVirtualRoomsWidget(TemplateStatusWidget):
"""
Status page widget to contain information about all virtual rooms belonging to the given event
"""
required_context_type = "event"
title = _("Virtual Rooms")
template_name = "admin/AKOnline/status/event_virtual_rooms.html"
# Register your models here.
......@@ -2,4 +2,7 @@ from django.apps import AppConfig
class AkplanConfig(AppConfig):
"""
App configuration (default, only specifies name of the app)
"""
name = 'AKPlan'