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 1437 additions and 64 deletions
{% load i18n %}
{% if event.akcategory_set.count == 0 %}
<p class="text-danger">{% trans "No categories yet" %}</p>
{% else %}
<ul>
{% for category in event.akcategory_set.all %}
<li>
<a href="{% url 'admin:AKModel_akcategory_change' category.pk %}">{{ category }}</a>
({{ category.ak_set.count }})
</li>
{% endfor %}
</ul>
{% endif %}
{% load i18n %}
{% load tz %}
{% timezone event.timezone %}
<h2><a href="{% url 'admin:AKModel_event_change' event.pk %}">{{event}}</a></h2>
<h5>{{ event.start }} - {{ event.end }}</h5>
<div class="form-check form-switch mt-2 mb-2">
<input type="checkbox" class="form-check-input" id="planPublishedSwitch"
{% if not event.plan_hidden %}checked{% endif %}
onclick="location.href='{% if event.plan_hidden %}{% url 'admin:plan-publish' %}{% else %}{% url 'admin:plan-unpublish' %}{% endif %}?pks={{event.pk}}';">
<label class="form-check-label" for="planPublishedSwitch">{% trans "Plan published?" %}</label>
</div>
{% endtimezone %}
{% load i18n %}
{% if event.akrequirement_set.count == 0 %}
<p class="text-danger">{% trans "No requirements yet" %}</p>
{% else %}
<ul>
{% for requirement in event.akrequirement_set.all %}
<li>
<a href="{% url 'admin:AKModel_akrequirement_change' requirement.pk %}">{{ requirement }}</a>
({{ requirement.ak_set.count }})
</li>
{% endfor %}
</ul>
{% endif %}
{% load i18n %}
{% if event.room_set.count == 0 %}
<p class="text-danger">{% trans "No rooms yet" %}</p>
{% else %}
<p>
{% for room in event.room_set.all %}
{% if forloop.counter0 > 0 %}
&middot;
{% endif %}
<a href="{% url 'admin:AKModel_room_change' room.pk %}">{{ room }}</a>
{% endfor %}
</p>
{% endif %}
{% extends "admin/base_site.html" %}
{% load tags_AKModel %}
{% load i18n %}
{% load tz %}
{% load fontawesome_6 %}
{% block title %}{% trans "Status" %}: {{ event }}{% endblock %}
{% block content %}
{% timezone event.timezone %}
<div class="row">
{% for widget in widgets %}
<div class="card border-{{ widget.status }} mb-3 me-2 col-xl-3 col-md-4 col-sm-6 p-0">
<div class="card-header">
{% if widget.actions %}
<div class="float-end">
<a style="cursor: pointer;" data-bs-toggle="dropdown" aria-expanded="false">
&nbsp;{% fa6_icon "ellipsis-vertical" %}&nbsp;
</a>
<ul class="dropdown-menu dropdown-menu-end">
{% for action in widget.actions %}
<li class="dropdown-item">
<a href="{{ action.url }}">{{ action.text }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{{ widget.title }}
</div>
<div class="card-body">
{{ widget.body }}
</div>
</div>
{% endfor %}
</div>
{% endtimezone %}
{% endblock %}
......@@ -12,7 +12,7 @@
<a href="{% url 'admin:AKModel_event_change' event.pk %}">{{ event }}</a>
({{ event.start|timezone:event.timezone|date:"d.m.y" }} -
{{ event.end|timezone:event.timezone|date:"d.m.y" }}) &middot;
<a href="{% url 'admin:event_status' slug=event.slug %}">{% trans "Status" %}</a> &middot;
<a href="{% url 'admin:event_status' event_slug=event.slug %}">{% trans "Status" %}</a> &middot;
<a href="{% url 'admin:schedule' event_slug=event.slug %}">{% trans "Scheduling" %}</a>
</li>
{% endfor %}
......
from django import template
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)
......@@ -39,3 +72,47 @@ def wiki_owners_export(owners, event):
return str(owner)
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')
))
from django.contrib.auth.models import User
import traceback
from typing import List
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
from django.urls import reverse_lazy
from django.urls import reverse_lazy, reverse
from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, Room, AKSlot, AKOrgaMessage, \
ConstraintViolation, DefaultSlot
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
)
......@@ -35,38 +63,143 @@ class BasicViewTests:
:rtype: str, str
"""
view_name_with_prefix = f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0]
url = reverse_lazy(view_name_with_prefix, kwargs=view_name[1])
url = reverse(view_name_with_prefix, kwargs=view_name[1])
return view_name_with_prefix, url
def testViewsFor200(self):
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"
msg_content = "Wrong message, expected '{expected_message}'"
if msg_prefix != "":
msg_count = f"{msg_prefix}: {msg_count}"
msg_content = f"{msg_prefix}: {msg_content}"
# Check that the last message correctly reports the issue
# (there might be more messages from previous calls that were not already rendered)
self.assertGreater(len(messages), 0, msg=msg_count)
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)
response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"{view_name_with_prefix} ({url}) broken")
try:
response = self.client.get(url)
self.assertEqual(response.status_code, 200, 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 testAccessControlStaffOnly(self):
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)
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)")
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: # 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, val):
"""
Create representation sendable via POST from form data
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 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 (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)
if "target_view" in testcase.keys():
kwargs = testcase.get("target_kwargs", testcase["kwargs"])
_, target_url = self._name_and_url((testcase["target_view"], kwargs))
else:
target_url = url
expected_message = testcase.get("expected_message", "")
admin_user = testcase.get("admin", False)
if admin_user:
self.client.force_login(self.admin_user)
else:
self.client.logout()
response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"{name}: Could not load edit form via GET ({url})")
form = response.context[form_name]
data = {k:self._to_sendable_value(v) for k,v in form.initial.items()}
response = self.client.post(url, data=data)
if expected_code == 200:
self.assertEqual(response.status_code, 200, msg=f"{name}: Did not return 200 ({url}")
elif expected_code == 302:
self.assertRedirects(response, target_url, msg_prefix=f"{name}: Did not redirect ({url} -> {target_url}")
if expected_message != "":
self._assert_message(response, expected_message, msg_prefix=f"{name}")
class ModelViewTests(BasicViewTests, TestCase):
"""
Basic view test cases for views from AKModel plus some custom tests
"""
fixtures = ['model.json']
ADMIN_MODELS = [
......@@ -76,20 +209,68 @@ class ModelViewTests(BasicViewTests, TestCase):
(DefaultSlot, 'defaultslot')
]
def testAdmin(self):
self.client.force_login(self.admin_user)
VIEWS_STAFF_ONLY = [
('admin:index', {}),
('admin:event_status', {'event_slug': 'kif42'}),
('admin:event_requirement_overview', {'event_slug': 'kif42'}),
('admin:ak_csv_export', {'event_slug': 'kif42'}),
('admin:ak_wiki_export', {'slug': 'kif42'}),
('admin:ak_delete_orga_messages', {'event_slug': 'kif42'}),
('admin:ak_slide_export', {'event_slug': 'kif42'}),
('admin:default-slots-editor', {'event_slug': 'kif42'}),
('admin:room-import', {'event_slug': 'kif42'}),
('admin:new_event_wizard_start', {}),
]
EDIT_TESTCASES = [
{'view': 'admin:default-slots-editor', 'kwargs': {'event_slug': 'kif42'}, "admin": True},
]
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":
continue
view_name_with_prefix, url = self._name_and_url((f'admin:AKModel_{model[1]}_add', {}))
_, url = self._name_and_url(('admin:new_event_wizard_start', {}))
elif model[1] == "room":
_, url = self._name_and_url(('admin:room-new', {}))
# Otherwise, just call the creation form view
else:
_, 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}))
print(view_name_with_prefix)
_, 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("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 _, 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.assertNotEqual(ak.pk, 1, "AK known to be excluded from export (PK 1) included in export")
export_count += 1
self.assertEqual(export_count, AK.objects.filter(event_id=2, include_in_export=True).count(),
"Wiki export contained the wrong number of AKs")
......@@ -3,20 +3,27 @@ from django.apps import apps
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from AKModel import views
from AKModel.views import NewEventWizardStartView, NewEventWizardSettingsView, NewEventWizardPrepareImportView, \
NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, EventStatusView, \
AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, ExportSlidesView
import AKModel.views.api
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', views.AKOwnerViewSet, basename='AKOwner')
api_router.register('akcategory', views.AKCategoryViewSet, basename='AKCategory')
api_router.register('aktrack', views.AKTrackViewSet, basename='AKTrack')
api_router.register('ak', views.AKViewSet, basename='AK')
api_router.register('room', views.RoomViewSet, basename='Room')
api_router.register('akslot', views.AKSlotViewSet, basename='AKSlot')
api_router.register('akowner', AKModel.views.api.AKOwnerViewSet, basename='AKOwner')
api_router.register('akcategory', AKModel.views.api.AKCategoryViewSet, basename='AKCategory')
api_router.register('aktrack', AKModel.views.api.AKTrackViewSet, basename='AKTrack')
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
......@@ -31,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 = [
......@@ -43,16 +51,20 @@ 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>/',
include(event_specific_paths)
),
path('user/', views.UserView.as_view(), name="user"),
path('user/', AKModel.views.manage.UserView.as_view(), name="user"),
]
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"),
......@@ -73,15 +85,26 @@ 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:slug>/status/', admin_site.admin_view(EventStatusView.as_view()), name="event_status"),
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()),
name="ak_wiki_export"),
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/', ExportSlidesView.as_view(), name="ak_slide_export"),
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"),
]
from django.contrib import messages
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView, DetailView
from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \
IntermediateAdminActionView
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")
template_name = "admin/AKModel/requirements_overview.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["event"] = self.event
context["site_url"] = reverse_lazy("dashboard:dashboard_event", kwargs={'slug': context["event"].slug})
return context
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"
title = _("AK CSV Export")
def get_queryset(self):
return super().get_queryset().order_by("ak__track")
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"
title = _("AK Wiki Export")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
categories_with_aks, ak_wishes = context["event"].get_categories_with_aks(
wishes_seperately=True,
filter_func=lambda ak: ak.include_in_export
)
context["categories_with_aks"] = [(category.name, ak_list) for category, ak_list in categories_with_aks]
context["categories_with_aks"].append((_("Wishes"), ak_wishes))
return context
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):
return reverse_lazy('admin:event_status', kwargs={'slug': self.event.slug})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["ak_messages"] = self.get_orga_messages_for_event(self.event)
return context
def form_valid(self, form):
self.get_orga_messages_for_event(self.event).delete()
messages.add_message(self.request, messages.SUCCESS, _("AK Orga Messages successfully deleted"))
return super().form_valid(form)
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):")
success_message = _("Reset of interest in AKs successful.")
def action(self, form):
self.entities.update(interest=-1)
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:")
success_message = _("AKs' interest counters set back to 0.")
def action(self, form):
self.entities.update(interest_counter=0)
from rest_framework import mixins, viewsets, permissions
from AKModel.metaviews.admin import EventSlugMixin
from AKModel.models import AKOwner, AKCategory, AKTrack, AK, Room, AKSlot
from AKModel.serializers import AKOwnerSerializer, AKCategorySerializer, AKTrackSerializer, AKSerializer, \
RoomSerializer, AKSlotSerializer
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
def get_queryset(self):
return AKOwner.objects.filter(event=self.event)
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
def get_queryset(self):
return AKCategory.objects.filter(event=self.event)
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
def get_queryset(self):
return AKTrack.objects.filter(event=self.event)
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
def get_queryset(self):
return AK.objects.filter(event=self.event)
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
def get_queryset(self):
return Room.objects.filter(event=self.event)
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
def get_queryset(self):
return AKSlot.objects.filter(event=self.event)
from django.apps import apps
from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, FormView, UpdateView, DetailView
from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \
NewEventWizardImportForm, NewEventWizardActivateForm
from AKModel.metaviews.admin import AdminViewMixin, WizardViewMixin, EventSlugMixin
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"
wizard_step = 1
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"
wizard_step = 2
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["timezone"] = context["form"].cleaned_data["timezone"]
return context
def get_success_url(self):
return reverse_lazy("admin:new_event_wizard_prepare_import", kwargs={"event_slug": self.object.slug})
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
def form_valid(self, form):
# Selected a valid event to import from? Use this to go to next step of wizard
return redirect("admin:new_event_wizard_import", event_slug=self.event.slug,
import_slug=form.cleaned_data["import_event"].slug)
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):
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: # 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)}))
return redirect("admin:new_event_wizard_activate", slug=self.event.slug)
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
wizard_step = 5
def get_success_url(self):
return reverse_lazy("admin:new_event_wizard_finish", kwargs={"slug": self.object.slug})
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
import csv
import datetime
import json
import os
import tempfile
from abc import ABC, abstractmethod
from itertools import zip_longest
import django.db
from django.contrib import admin, messages
from django.contrib import messages
from django.db.models.functions import Now
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy, reverse
from django.utils.dateparse import parse_datetime
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView, DetailView, ListView, DeleteView, CreateView, FormView, UpdateView
from django.views.generic import TemplateView, DetailView
from django_tex.core import render_template_with_context, run_tex_in_directory
from django_tex.response import PDFResponse
from rest_framework import viewsets, permissions, mixins
from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \
NewEventWizardImportForm, NewEventWizardActivateForm, AdminIntermediateForm, SlideExportForm, \
AdminIntermediateActionForm, DefaultSlotEditorForm, RoomBatchCreationForm
from AKModel.models import Event, AK, AKSlot, Room, AKTrack, AKCategory, AKOwner, AKOrgaMessage, AKRequirement, \
ConstraintViolation, DefaultSlot
from AKModel.serializers import AKSerializer, AKSlotSerializer, RoomSerializer, AKTrackSerializer, AKCategorySerializer, \
AKOwnerSerializer
from AKModel.forms import SlideExportForm, DefaultSlotEditorForm
from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin
from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner
class EventSlugMixin:
"""
Mixin to handle views with event slugs
class UserView(TemplateView):
"""
event = None
def _load_event(self):
# Find event based on event slug
if self.event is None:
self.event = get_object_or_404(Event, slug=self.kwargs.get("event_slug", None))
def get(self, request, *args, **kwargs):
self._load_event()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self._load_event()
return super().post(request, *args, **kwargs)
def list(self, request, *args, **kwargs):
self._load_event()
return super().list(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
self._load_event()
return super().create(request, *args, **kwargs)
def dispatch(self, request, *args, **kwargs):
if self.event is None:
self._load_event()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
# Add event to context (to make it accessible in templates)
context["event"] = self.event
return context
View: Start page for logged in user
class FilterByEventSlugMixin(EventSlugMixin):
Will over a link to backend or inform the user that their account still needs to be confirmed
"""
Mixin to filter different querysets based on a event slug from the request url
"""
def get_queryset(self):
# Filter current queryset based on url event slug or return 404 if event slug is invalid
return super().get_queryset().filter(event=self.event)
class AdminViewMixin:
site_url = ''
title = ''
def get_context_data(self, **kwargs):
extra = admin.site.each_context(self.request)
extra.update(super().get_context_data(**kwargs))
if self.site_url != '':
extra["site_url"] = self.site_url
if self.title != '':
extra["title"] = self.title
return extra
class AKOwnerViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKOwnerSerializer
def get_queryset(self):
return AKOwner.objects.filter(event=self.event)
class AKCategoryViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKCategorySerializer
def get_queryset(self):
return AKCategory.objects.filter(event=self.event)
class AKTrackViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKTrackSerializer
def get_queryset(self):
return AKTrack.objects.filter(event=self.event)
class AKViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.ListModelMixin,
viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKSerializer
def get_queryset(self):
return AK.objects.filter(event=self.event)
class RoomViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = RoomSerializer
def get_queryset(self):
return Room.objects.filter(event=self.event)
class AKSlotViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin,
mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = AKSlotSerializer
def get_queryset(self):
return AKSlot.objects.filter(event=self.event)
class UserView(TemplateView):
template_name = "AKModel/user.html"
class EventStatusView(AdminViewMixin, DetailView):
template_name = "admin/AKModel/status.html"
model = Event
context_object_name = "event"
title = _("Event Status")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["unscheduled_slots_count"] = context["event"].akslot_set.filter(start=None).count
context["site_url"] = reverse_lazy("dashboard:dashboard_event", kwargs={'slug': context["event"].slug})
context["ak_messages"] = AKOrgaMessage.objects.filter(ak__event=context["event"])
return context
class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
model = AKRequirement
context_object_name = "requirements"
title = _("Requirements for Event")
template_name = "admin/AKModel/requirements_overview.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["event"] = self.event
context["site_url"] = reverse_lazy("dashboard:dashboard_event", kwargs={'slug': context["event"].slug})
return context
class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
template_name = "admin/AKModel/ak_csv_export.html"
model = AKSlot
context_object_name = "slots"
title = _("AK CSV Export")
def get_queryset(self):
return super().get_queryset().order_by("ak__track")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
class AKWikiExportView(AdminViewMixin, DetailView):
template_name = "admin/AKModel/wiki_export.html"
model = Event
context_object_name = "event"
title = _("AK Wiki Export")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
categories_with_aks, ak_wishes = context["event"].get_categories_with_aks(wishes_seperately=True)
context["categories_with_aks"] = [(category.name, ak_list) for category, ak_list in categories_with_aks]
context["categories_with_aks"].append((_("Wishes"), ak_wishes))
return context
class IntermediateAdminView(AdminViewMixin, FormView):
template_name = "admin/AKModel/action_intermediate.html"
form_class = AdminIntermediateForm
def get_preview(self):
return ""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = self.title
context["preview"] = self.get_preview()
return context
class AKMessageDeleteView(EventSlugMixin, IntermediateAdminView):
template_name = "admin/AKModel/message_delete.html"
title = _("Delete AK Orga Messages")
def get_orga_messages_for_event(self, event):
return AKOrgaMessage.objects.filter(ak__event=event)
def get_success_url(self):
return reverse_lazy('admin:event_status', kwargs={'slug': self.event.slug})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["ak_messages"] = self.get_orga_messages_for_event(self.event)
return context
def form_valid(self, form):
self.get_orga_messages_for_event(self.event).delete()
messages.add_message(self.request, messages.SUCCESS, _("AK Orga Messages successfully deleted"))
return super().form_valid(form)
class WizardViewMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["wizard_step"] = self.wizard_step
context["wizard_steps"] = [
_("Start"),
_("Settings"),
_("Event created, Prepare Import"),
_("Import categories & requirements"),
_("Activate?"),
_("Finish")
]
context["wizard_step_text"] = context["wizard_steps"][self.wizard_step - 1]
context["wizard_steps_total"] = len(context["wizard_steps"])
return context
class NewEventWizardStartView(AdminViewMixin, WizardViewMixin, CreateView):
model = Event
form_class = NewEventWizardStartForm
template_name = "admin/AKModel/event_wizard/start.html"
wizard_step = 1
class NewEventWizardSettingsView(AdminViewMixin, WizardViewMixin, CreateView):
model = Event
form_class = NewEventWizardSettingsForm
template_name = "admin/AKModel/event_wizard/settings.html"
wizard_step = 2
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["timezone"] = context["form"].cleaned_data["timezone"]
return context
def get_success_url(self):
return reverse_lazy("admin:new_event_wizard_prepare_import", kwargs={"event_slug": self.object.slug})
class NewEventWizardPrepareImportView(WizardViewMixin, EventSlugMixin, FormView):
form_class = NewEventWizardPrepareImportForm
template_name = "admin/AKModel/event_wizard/created_prepare_import.html"
wizard_step = 3
def form_valid(self, form):
# Selected a valid event to import from? Use this to go to next step of wizard
return redirect("admin:new_event_wizard_import", event_slug=self.event.slug,
import_slug=form.cleaned_data["import_event"].slug)
class NewEventWizardImportView(EventSlugMixin, WizardViewMixin, FormView):
form_class = NewEventWizardImportForm
template_name = "admin/AKModel/event_wizard/import.html"
wizard_step = 4
def get_initial(self):
initial = super().get_initial()
initial["import_event"] = Event.objects.get(slug=self.kwargs["import_slug"])
return initial
def form_valid(self, form):
for import_type in ["import_categories", "import_requirements"]:
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:
messages.add_message(self.request, messages.ERROR,
_("Could not copy '%(obj)s' (%(error)s)" % {'obj': import_obj,
"error": str(e)}))
return redirect("admin:new_event_wizard_activate", slug=self.event.slug)
class NewEventWizardActivateView(WizardViewMixin, UpdateView):
model = Event
template_name = "admin/AKModel/event_wizard/activate.html"
form_class = NewEventWizardActivateForm
wizard_step = 5
def get_success_url(self):
return reverse_lazy("admin:new_event_wizard_finish", kwargs={"slug": self.object.slug})
class NewEventWizardFinishView(WizardViewMixin, DetailView):
model = Event
template_name = "admin/AKModel/event_wizard/finish.html"
wizard_step = 6
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"]
......@@ -353,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
......@@ -378,63 +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 IntermediateAdminActionView(IntermediateAdminView, ABC):
form_class = AdminIntermediateActionForm
entities = None
def get_queryset(self, pks=None):
if pks is None:
pks = self.request.GET['pks']
return self.model.objects.filter(pk__in=pks.split(","))
def get_initial(self):
initial = super().get_initial()
initial['pks'] = self.request.GET['pks']
return initial
def get_preview(self):
self.entities = self.get_queryset()
joined_entities = '\n'.join(str(e) for e in self.entities)
return f"{self.confirmation_message}:\n\n {joined_entities}"
def get_success_url(self):
return reverse(f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_changelist")
@abstractmethod
def action(self, form):
pass
def form_valid(self, form):
self.entities = self.get_queryset(pks=form.cleaned_data['pks'])
self.action(form)
messages.add_message(self.request, messages.SUCCESS, self.success_message)
return super().form_valid(form)
class LoopActionMixin(ABC):
def action(self, form):
self.pre_action()
for entity in self.entities:
self.perform_action(entity)
entity.save()
self.post_action()
@abstractmethod
def perform_action(self, entity):
pass
def pre_action(self):
pass
def post_action(self):
pass
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")
......@@ -445,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'")
......@@ -455,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'")
......@@ -464,27 +131,10 @@ class CVSetLevelWarningView(IntermediateAdminActionView):
self.entities.update(level=ConstraintViolation.ViolationLevel.WARNING)
class AKResetInterestView(IntermediateAdminActionView):
title = _("Reset interest in AKs")
model = AK
confirmation_message = _("Interest of the following AKs will be set to not filled (-1):")
success_message = _("Reset of interest in AKs successful.")
def action(self, form):
self.entities.update(interest=-1)
class AKResetInterestCounterView(IntermediateAdminActionView):
title = _("Reset AKs' interest counters")
model = AK
confirmation_message = _("Interest counter of the following AKs will be set to 0:")
success_message = _("AKs' interest counters set back to 0.")
def action(self, form):
self.entities.update(interest_counter=0)
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:')
......@@ -495,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:')
......@@ -505,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")
......@@ -532,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 = tz.localize(parse_datetime(slot["start"]))
end = tz.localize(parse_datetime(slot["end"]))
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(
......@@ -549,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
......@@ -570,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,
......@@ -580,49 +238,10 @@ class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
return super().form_valid(form)
class RoomBatchCreationView(EventSlugMixin, IntermediateAdminView):
form_class = RoomBatchCreationForm
title = _("Import Rooms from CSV")
def get_success_url(self):
return reverse_lazy('admin:event_status', kwargs={'slug': self.event.slug})
def form_valid(self, form):
from django.apps import apps
virtual_rooms_support = False
created_count = 0
rooms_raw_dict: csv.DictReader = form.cleaned_data["rooms"]
if apps.is_installed("AKOnline") and "url" in rooms_raw_dict.fieldnames:
virtual_rooms_support = True
from AKOnline.models import VirtualRoom
for raw_room in rooms_raw_dict:
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:
if virtual_rooms_support and raw_room["url"] != "":
VirtualRoom.objects.create(name=name,
location=location,
capacity=capacity,
url=raw_room["url"],
event=self.event)
else:
Room.objects.create(name=name,
location=location,
capacity=capacity,
event=self.event)
created_count += 1
except django.db.Error as e:
messages.add_message(self.request, messages.WARNING,
_("Could not import room {name}: {e}").format(name=name, e=str(e)))
if created_count > 0:
messages.add_message(self.request, messages.SUCCESS,
_("Imported {count} room(s)").format(count=created_count))
else:
messages.add_message(self.request, messages.WARNING, _("No rooms imported"))
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"
import csv
import django.db
from django.apps import apps
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView
from AKModel.availability.models import Availability
from AKModel.forms import RoomForm, RoomBatchCreationForm
from AKModel.metaviews.admin import AdminViewMixin, EventSlugMixin, IntermediateAdminView
from AKModel.models import Room
class RoomCreationView(AdminViewMixin, CreateView):
"""
Admin view: Create a room
"""
form_class = RoomForm
template_name = 'admin/AKModel/room_create.html'
def get_success_url(self):
print(self.request.POST['save_action'])
if self.request.POST['save_action'] == 'save_add_another':
return reverse_lazy('admin:room-new')
if self.request.POST['save_action'] == 'save_continue':
return reverse_lazy('admin:AKModel_room_change', kwargs={'object_id': self.room.pk})
return reverse_lazy('admin:AKModel_room_changelist')
def form_valid(self, form):
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")
def get_success_url(self):
return reverse_lazy('admin:event_status', kwargs={'event_slug': self.event.slug})
def form_valid(self, form):
virtual_rooms_support = False
create_default_availabilities = form.cleaned_data["create_default_availabilities"]
created_count = 0
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()
created_count += 1
except django.db.Error as e:
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))
else:
messages.add_message(self.request, messages.WARNING, _("No rooms imported"))
return super().form_valid(form)
from django.apps import apps
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from AKModel.metaviews import status_manager
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"
def render_status(self, context: {}) -> str:
return "success" if not context["event"].plan_hidden else "primary"
@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"
actions = [
{
"text": _("Add category"),
"url": reverse_lazy("admin:AKModel_akcategory_add"),
}
]
def render_title(self, context: {}) -> str:
# 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:
return "danger" if self.category_count == 0 else "primary"
@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"
actions = [
{
"text": _("Add Room"),
"url": reverse_lazy("admin:AKModel_room_add"),
}
]
def render_title(self, context: {}) -> str:
# 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:
return "danger" if self.room_count == 0 else "primary"
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": import_room_url,
}
)
return actions
@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"
def get_context_data(self, context) -> dict:
context["ak_count"] = context["event"].ak_set.count()
context["unscheduled_slots_count"] = context["event"].akslot_set.filter(start=None).count
return context
def render_actions(self, context: {}) -> list[dict]:
actions = [
{
"text": _("Scheduling"),
"url": reverse_lazy("admin:schedule", kwargs={"event_slug": context["event"].slug}),
},
]
if apps.is_installed("AKScheduling"):
actions.extend([
{
"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}
),
})
actions.extend([
{
"text": _("Edit Default Slots"),
"url": reverse_lazy("admin:default-slots-editor", kwargs={"event_slug": context["event"].slug}),
},
{
"text": _("Manage ak tracks"),
"url": reverse_lazy("admin:tracks_manage", kwargs={"event_slug": context["event"].slug}),
},
{
"text": _("Export AKs as CSV"),
"url": reverse_lazy("admin:ak_csv_export", kwargs={"event_slug": context["event"].slug}),
},
{
"text": _("Export AKs for Wiki"),
"url": reverse_lazy("admin:ak_wiki_export", kwargs={"slug": context["event"].slug}),
},
{
"text": _("Export AK Slides"),
"url": reverse_lazy("admin:ak_slide_export", kwargs={"event_slug": context["event"].slug}),
},
]
)
return actions
@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})"
def render_actions(self, context: {}) -> list[dict]:
return [
{
"text": _("Show AKs for requirements"),
"url": reverse_lazy("admin:event_requirement_overview", kwargs={"event_slug": context["event"].slug}),
},
{
"text": _("Add Requirement"),
"url": reverse_lazy("admin:AKModel_akrequirement_add"),
},
]
class EventStatusView(EventSlugMixin, StatusView):
"""
View: Show a status dashboard for the given event
"""
title = _("Event Status")
provided_context_type = "event"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["site_url"] = reverse_lazy("dashboard:dashboard_event", kwargs={'slug': context["event"].slug})
return context
from django.contrib import admin
from AKModel.admin import RoomAdmin, RoomForm
from AKOnline.models import VirtualRoom
class VirtualRoomForm(RoomForm):
class Meta(RoomForm.Meta):
model = VirtualRoom
fields = ['name',
'location',
'url',
'capacity',
'properties',
'event',
]
@admin.register(VirtualRoom)
class VirtualRoomAdmin(RoomAdmin):
class VirtualRoomAdmin(admin.ModelAdmin):
"""
Admin interface for virtual room model
"""
model = VirtualRoom
list_display = ['room', 'event', 'url']
list_filter = ['room__event']
def get_form(self, request, obj=None, change=False, **kwargs):
if obj is not None:
return VirtualRoomForm
return super().get_form(request, obj, change, **kwargs)
def get_readonly_fields(self, request, obj=None):
# Don't allow changing the room on existing virtual rooms
# Instead, a link to the room editing form will be displayed automatically
if obj:
return self.readonly_fields + ('room', )
return self.readonly_fields
......@@ -2,4 +2,7 @@ from django.apps import AppConfig
class AkonlineConfig(AppConfig):
"""
App configuration (default -- only to set the app name)
"""
name = 'AKOnline'
from betterforms.multiform import MultiModelForm
from django.forms import ModelForm
from AKModel.forms import RoomForm
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
# Show all fields except for room
exclude = ['room'] #pylint: disable=modelform-uses-exclude
def __init__(self, *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().save(commit=False)
if commit:
room = objects['room']
room.save()
virtual = objects['virtual']
if virtual.url != "":
virtual.room = room
virtual.save()
else:
objects['virtual'] = None
return objects
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: AKOnline/models.py:11
msgid "URL"
msgstr "URL"
#: AKOnline/models.py:11
msgid "URL to the room or server"
msgstr "URL zum Raum oder Server"
#: AKOnline/models.py:12
msgid "Room"
msgstr "Raum"
#: AKOnline/models.py:16
#: AKOnline/templates/admin/AKOnline/room_create_with_virtual.html:10
msgid "Virtual Room"
msgstr "Virtueller Raum"
#: AKOnline/models.py:17 AKOnline/views.py:38
msgid "Virtual Rooms"
msgstr "Virtuelle Räume"
#: AKOnline/templates/admin/AKOnline/room_create_with_virtual.html:11
msgid "Leave empty if that room is not virtual/hybrid."
msgstr "Leer lassen wenn der Raum nicht virtuell/hybrid ist"
#: AKOnline/views.py:25
#, python-format
msgid "Created Room '%(room)s'"
msgstr "Raum '%(room)s' angelegt"
#: AKOnline/views.py:28
#, python-format
msgid "Created related Virtual Room '%(vroom)s'"
msgstr "Verbundenen virtuellen Raum '%(vroom)s' angelegt"