Skip to content
Snippets Groups Projects

Compare revisions

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

Source

Select target project
No results found
Select Git revision
  • feature-type-filters
  • komasolver
  • main
  • renovate/django-5.x
  • renovate/django_csp-4.x
  • renovate/jsonschema-4.x
  • renovate/uwsgi-2.x
7 results

Target

Select target project
  • konstantin/akplanning
  • matedealer/akplanning
  • kif/akplanning
  • mirco/akplanning
  • lordofthevoid/akplanning
  • voidptr/akplanning
  • xayomer/akplanning-fork
  • mollux/akplanning
  • neumantm/akplanning
  • mmarx/akplanning
  • nerf/akplanning
  • felix_bonn/akplanning
  • sebastian.uschmann/akplanning
13 results
Select Git revision
  • 520-akowner
  • 520-fix-event-wizard-datepicker
  • 520-fix-scheduling
  • 520-improve-scheduling
  • 520-improve-scheduling-2
  • 520-improve-submission
  • 520-improve-trackmanager
  • 520-improve-wall
  • 520-message-resolved
  • 520-status
  • 520-upgrades
  • add_express_interest_to_ak_overview
  • admin-production-color
  • bugfixes
  • csp
  • featire-ical-export
  • feature-ak-requirement-lists
  • feature-akslide-export-better-filename
  • feature-akslides
  • feature-better-admin
  • feature-better-cv-list
  • feature-colors
  • feature-constraint-checking
  • feature-constraint-checking-wip
  • feature-dashboard-history-button
  • feature-event-status
  • feature-event-wizard
  • feature-export-flag
  • feature-improve-admin
  • feature-improve-filters
  • feature-improved-user-creation-workflow
  • feature-interest-view
  • feature-mails
  • feature-modular-status
  • feature-plan-autoreload
  • feature-present-default
  • feature-register-link
  • feature-remaining-constraint-validation
  • feature-room-import
  • feature-scheduler-improve
  • feature-scheduling-2.0
  • feature-special-attention
  • feature-time-input
  • feature-tracker
  • feature-wiki-wishes
  • feature-wish-slots
  • feature-wizard-buttons
  • features-availabilities
  • fix-ak-times-above-folg
  • fix-api
  • fix-constraint-violation-string
  • fix-cv-checking
  • fix-default-slot-length
  • fix-default-slot-localization
  • fix-doc-minor
  • fix-duration-display
  • fix-event-tz-pytz-update
  • fix-history-interest
  • fix-interest-view
  • fix-js
  • fix-pipeline
  • fix-plan-timezone-now
  • fix-room-add
  • fix-scheduling-drag
  • fix-slot-defaultlength
  • fix-timezone
  • fix-translation-scheduling
  • fix-virtual-room-admin
  • fix-wizard-csp
  • font-locally
  • improve-admin
  • improve-online
  • improve-slides
  • improve-submission-coupling
  • interest_restriction
  • main
  • master
  • meta-debug-toolbar
  • meta-export
  • meta-makemessages
  • meta-performance
  • meta-tests
  • meta-tests-gitlab-test
  • meta-upgrades
  • mollux-master-patch-02906
  • port-availabilites-fullcalendar
  • qs
  • remove-tags
  • renovate/configure
  • renovate/django-4.x
  • renovate/django-5.x
  • renovate/django-bootstrap-datepicker-plus-5.x
  • renovate/django-bootstrap5-23.x
  • renovate/django-bootstrap5-24.x
  • renovate/django-compressor-4.x
  • renovate/django-debug-toolbar-4.x
  • renovate/django-registration-redux-2.x
  • renovate/django-simple-history-3.x
  • renovate/django-split-settings-1.x
  • renovate/django-timezone-field-5.x
100 results
Show changes
Showing
with 635 additions and 818 deletions
......@@ -8,6 +8,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" %}
......@@ -17,8 +22,6 @@
<h5 class="mb-3">{% trans "Successfully imported.<br><br>Do you want to activate your event now?" %}</h5>
{{ form.media }}
<form method="post">{% csrf_token %}
{% bootstrap_form form %}
......
......@@ -8,6 +8,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" %}
......@@ -29,8 +34,6 @@
<h5 class="mb-3">{% trans "Your event was created and can now be further configured." %}</h5>
{{ form.media }}
<form method="post">{% csrf_token %}
{% bootstrap_form form %}
......
......@@ -8,11 +8,14 @@
{% 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" %}
{{ form.media }}
<form method="post">{% csrf_token %}
{% bootstrap_form form %}
......
......@@ -8,11 +8,14 @@
{% 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" %}
{{ form.media }}
{% timezone timezone %}
<form method="post">{% csrf_token %}
......
......@@ -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" %}
......
<table class="table table-striped">
{% for message in ak_messages %}
<tr><td>
<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>
<p>{{ message.text }}</p>
</td></tr>
{% endfor %}
</table>
{% load tz %}
{% load fontawesome_6 %}
{% timezone event.timezone %}
<table class="table table-striped">
{% for message in ak_messages %}
<tr><td>
<span class="text-secondary float-end">
{{ message.timestamp|date:"Y-m-d H:i:s" }}
</span>
<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 %}
</table>
{% endtimezone %}
{% extends "admin/base_site.html" %}
{% load tags_AKModel %}
{% load i18n %}
{% load tz %}
{% block title %}{% trans "Status" %}: {{event}}{% endblock %}
{% block content %}
{% 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>
<div class="row">
<div class="col-md-8">
<h3 class="block-header">{% trans "Categories" %}</h3>
{% if event.akcategory_set.count == 0 %}
<p class="text-danger">{% trans "No categories yet" %}</p>
{% else %}
<p>
{{ event.akcategory_set.count }}:
{% for category in event.akcategory_set.all %}
{% if forloop.counter0 > 0 %}
&middot;
{% endif %}
<a href="{% url 'admin:AKModel_akcategory_change' category.pk %}">{{ category }}</a>
({{ category.ak_set.count }})
{% endfor %}
</p>
{% endif %}
<a class="btn btn-success" href="{% url 'admin:AKModel_akcategory_add' %}">{% trans "Add category" %}</a>
<h3 class="block-header">{% trans "Rooms" %}</h3>
{% if event.room_set.count == 0 %}
<p class="text-danger">{% trans "No rooms yet" %}</p>
{% else %}
<p>
{{ event.room_set.count }}:
{% 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 %}
<a class="btn btn-success" href="{% url 'admin:AKModel_room_add' %}">{% trans "Add Room" %}</a>
<a class="btn btn-success" href="{% url 'admin:room-import' event_slug=event.slug %}">{% trans "Import Rooms from CSV" %}</a>
<h3 class="block-header">{% trans "AKs" %}</h3>
{% if event.ak_set.count == 0 %}
<p class="text-danger">{% trans "No AKs yet" %}</p>
{% else %}
<table>
<tbody>
<tr>
<td>{% trans "AKs" %}</td><td>{{ event.ak_set.count }}</td>
</tr>
<tr>
<td>{% trans "Slots" %}</td><td>{{ event.akslot_set.count }}</td>
</tr>
<tr>
<td>{% trans "Unscheduled Slots" %}</td><td>
{% if "AKScheduling"|check_app_installed %}
<a href="{% url 'admin:slots_unscheduled' event_slug=event.slug %}">
{{ unscheduled_slots_count }}
</a>
{% else %}
{{ unscheduled_slots_count }}
{% endif %}
</td>
</tr>
</tbody>
</table>
<a class="btn btn-success"
href="{% url 'admin:schedule' event_slug=event.slug %}">{% trans "Scheduling" %}</a>
{% if "AKScheduling | is_installed" %}
<a class="btn btn-success"
href="{% url 'admin:constraint-violations' slug=event.slug %}">{% trans "Constraint Violations" %} <span class="badge bg-secondary">{{ event.constraintviolation_set.count }}</span></a>
<a class="btn btn-success"
href="{% url 'admin:special-attention' slug=event.slug %}">{% trans "AKs requiring special attention" %}</a>
<a class="btn btn-success"
href="{% url 'admin:enter-interest' event_slug=event.slug pk=event.ak_set.all.first.pk %}">{% trans "Enter Interest" %}</a>
{% endif %}
<a class="btn btn-success"
href="{% url 'admin:default-slots-editor' event_slug=event.slug %}">{% trans "Edit Default Slots" %}</a>
<a class="btn btn-success"
href="{% url 'admin:tracks_manage' event_slug=event.slug %}">{% trans "Manage ak tracks" %}</a>
<a class="btn btn-success"
href="{% url 'admin:ak_csv_export' event_slug=event.slug %}">{% trans "Export AKs as CSV" %}</a>
<a class="btn btn-success"
href="{% url 'admin:ak_wiki_export' slug=event.slug %}">{% trans "Export AKs for Wiki" %}</a>
<a class="btn btn-success"
href="{% url 'admin:ak_slide_export' event_slug=event.slug %}">{% trans "Export AK Slides" %}</a>
{% endif %}
<h3 class="block-header">{% trans "Requirements" %}</h3>
{% if event.akrequirement_set.count == 0 %}
<p class="text-danger">{% trans "No requirements yet" %}</p>
{% else %}
<p>
{{ event.akrequirement_set.count }}:
{% for requirement in event.akrequirement_set.all %}
{% if forloop.counter0 > 0 %}
&middot;
{% endif %}
<a href="{% url 'admin:AKModel_akrequirement_change' requirement.pk %}">{{ requirement }}</a>
({{ requirement.ak_set.count }})
{% endfor %}
</p>
{% endif %}
<a class="btn btn-success" href="{% url 'admin:event_requirement_overview' event.slug %}">{% trans "Show AKs for requirements" %}</a>
<a class="btn btn-success" href="{% url 'admin:AKModel_akrequirement_add' %}">{% trans "Add Requirement" %}</a>
</div>
<div class="col-md-4">
<h3 class="block-header">{% trans "Messages" %}</h3>
{% include "admin/AKModel/render_ak_messages.html" %}
<a class="btn btn-danger" href="{% url 'admin:ak_delete_orga_messages' event_slug=event.slug %}">{% trans "Delete all messages" %}</a>
</div>
</div>
{% endtimezone %}
{% endblock %}
{% load i18n %}
{% load tags_AKModel %}
{% if event.ak_set.count == 0 %}
<p class="text-danger">{% trans "No AKs yet" %}</p>
{% else %}
<table>
<tbody>
<tr>
<td>{% trans "AKs" %}</td><td>{{ ak_count }}</td>
</tr>
<tr>
<td>{% trans "Slots" %}</td><td>{{ event.akslot_set.count }}</td>
</tr>
<tr>
<td>{% trans "Unscheduled Slots" %}</td><td>
{% if "AKScheduling"|check_app_installed %}
<a href="{% url 'admin:slots_unscheduled' event_slug=event.slug %}">
{{ unscheduled_slots_count }}
</a>
{% else %}
{{ unscheduled_slots_count }}
{% endif %}
</td>
</tr>
</tbody>
</table>
{% endif %}
{% 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 %}
......
......@@ -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
from django.urls import reverse_lazy, reverse
from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, Room, AKSlot, AKOrgaMessage, \
ConstraintViolation, DefaultSlot
from django.urls import reverse, reverse_lazy
from AKModel.models import (
AK,
AKCategory,
AKOrgaMessage,
AKOwner,
AKRequirement,
AKSlot,
AKTrack,
ConstraintViolation,
DefaultSlot,
Event,
Room,
)
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 = ''
APP_NAME = ""
VIEWS_STAFF_ONLY = []
EDIT_TESTCASES = []
def setUp(self):
self.staff_user = User.objects.create(
username='Test Staff User', email='teststaff@example.com', password='staffpw',
is_staff=True, is_active=True
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(
username='Test Admin User', email='testadmin@example.com', password='adminpw',
is_staff=True, is_superuser=True, is_active=True
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(
username='Test Deactivated User', email='testdeactivated@example.com', password='deactivatedpw',
is_staff=True, is_active=False
self.deactivated_user = user_model.objects.create(
username="Test Deactivated User",
email="testdeactivated@example.com",
password="deactivatedpw",
is_staff=True,
is_active=False,
)
def _name_and_url(self, view_name):
......@@ -40,12 +84,21 @@ class BasicViewTests:
:return: full view name with prefix if applicable, url of the view
:rtype: str, str
"""
view_name_with_prefix = f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0]
view_name_with_prefix = (
f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0]
)
url = reverse(view_name_with_prefix, kwargs=view_name[1])
return view_name_with_prefix, url
def _assert_message(self, response, expected_message, msg_prefix=""):
messages:List[Message] = list(get_messages(response.wsgi_request))
"""
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}'"
......@@ -59,60 +112,104 @@ 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()}")
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 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()}")
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,
msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user")
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)
......@@ -130,79 +227,135 @@ class BasicViewTests:
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})")
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()}
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}")
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}")
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):
fixtures = ['model.json']
"""
Basic view test cases for views from AKModel plus some custom tests
"""
fixtures = ["model.json"]
ADMIN_MODELS = [
(Event, 'event'), (AKOwner, 'akowner'), (AKCategory, 'akcategory'), (AKTrack, 'aktrack'),
(AKRequirement, 'akrequirement'), (AK, 'ak'), (Room, 'room'), (AKSlot, 'akslot'),
(AKOrgaMessage, 'akorgamessage'), (ConstraintViolation, 'constraintviolation'),
(DefaultSlot, 'defaultslot')
(Event, "event"),
(AKOwner, "akowner"),
(AKCategory, "akcategory"),
(AKTrack, "aktrack"),
(AKRequirement, "akrequirement"),
(AK, "ak"),
(Room, "room"),
(AKSlot, "akslot"),
(AKOrgaMessage, "akorgamessage"),
(ConstraintViolation, "constraintviolation"),
(DefaultSlot, "defaultslot"),
]
VIEWS_STAFF_ONLY = [
('admin:index', {}),
('admin:event_status', {'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', {}),
("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},
{
"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":
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")
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")
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.assertNotEqual(ak.pk, 1, "AK known to be excluded from export (PK 1) 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
self.assertEqual(export_count, AK.objects.filter(event_id=2, include_in_export=True).count(),
"Wiki export contained the wrong number of AKs")
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,28 @@ 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,11 +39,17 @@ 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'))
# If AKSolverInterface is active, register additional API endpoints
if apps.is_installed("AKSolverInterface"):
from AKSolverInterface.api import ExportEventForSolverViewSet
api_router.register("solver-export", ExportEventForSolverViewSet, basename="solver-export")
event_specific_paths = [
path('api/', include(api_router.urls), name='api'),
]
......@@ -43,16 +57,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,10 +91,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: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()),
......@@ -84,4 +107,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"),
]
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.apps import apps
from django.contrib import admin, messages
from django.db.models.functions import Now
from django.http import HttpResponseRedirect
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_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, RoomForm
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
class EventSlugMixin:
"""
Mixin to handle views with event slugs
"""
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
class FilterByEventSlugMixin(EventSlugMixin):
"""
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,
filter=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 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):
import_types = ["import_categories", "import_requirements"]
if apps.is_installed("AKDashboard"):
import_types.append("import_buttons")
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:
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):
title = _('Export AK Slides')
form_class = SlideExportForm
def form_valid(self, form):
template_name = 'admin/AKModel/export/slides.tex'
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"]
translations = {
'symbols': _("Symbols"),
'who': _("Who?"),
'duration': _("Duration(s)"),
'reso': _("Reso intention?"),
'category': _("Category (for Wishes)"),
'wishes': _("Wishes"),
}
def build_ak_list_with_next_aks(ak_list):
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())]
categories_with_aks, ak_wishes = self.event.get_categories_with_aks(wishes_seperately=True, filter=lambda
ak: not RESULT_PRESENTATION_MODE or (ak.present or (ak.present is None and ak.category.present_by_default)))
context = {
'title': self.event.name,
'categories_with_aks': [(category, build_ak_list_with_next_aks(ak_list)) for category, ak_list in
categories_with_aks],
'subtitle': _("AKs"),
"wishes": build_ak_list_with_next_aks(ak_wishes),
"translations": translations,
"result_presentation_mode": RESULT_PRESENTATION_MODE,
"space_for_notes_in_wishes": SPACE_FOR_NOTES_IN_WISHES,
}
source = render_template_with_context(template_name, context)
# Perform real compilation (run latex twice for correct page numbers)
with tempfile.TemporaryDirectory() as tempdir:
run_tex_in_directory(source, tempdir, template_name=self.template_name)
os.remove(f'{tempdir}/texput.tex')
pdf = run_tex_in_directory(source, tempdir, template_name=self.template_name)
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):
title = _('Mark Constraint Violations as manually resolved')
model = ConstraintViolation
confirmation_message = _("The following Constraint Violations will be marked as manually resolved")
success_message = _("Constraint Violations marked as resolved")
def action(self, form):
self.entities.update(manually_resolved=True)
class CVSetLevelViolationView(IntermediateAdminActionView):
title = _('Set Constraint Violations to level "violation"')
model = ConstraintViolation
confirmation_message = _("The following Constraint Violations will be set to level 'violation'")
success_message = _("Constraint Violations set to level 'violation'")
def action(self, form):
self.entities.update(level=ConstraintViolation.ViolationLevel.VIOLATION)
class CVSetLevelWarningView(IntermediateAdminActionView):
title = _('Set Constraint Violations to level "warning"')
model = ConstraintViolation
confirmation_message = _("The following Constraint Violations will be set to level 'warning'")
success_message = _("Constraint Violations set to level 'warning'")
def action(self, form):
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):
title = _('Publish plan')
model = Event
confirmation_message = _('Publish the plan(s) of:')
success_message = _('Plan published')
def action(self, form):
self.entities.update(plan_published_at=Now(), plan_hidden=False)
class PlanUnpublishView(IntermediateAdminActionView):
title = _('Unpublish plan')
model = Event
confirmation_message = _('Unpublish the plan(s) of:')
success_message = _('Plan unpublished')
def action(self, form):
self.entities.update(plan_published_at=None, plan_hidden=True)
class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
template_name = "admin/AKModel/default_slot_editor.html"
form_class = DefaultSlotEditorForm
title = _("Edit Default Slots")
def get_success_url(self):
return self.request.path
def get_initial(self):
initial = super().get_initial()
default_slots = [
{"id": s.id, "start": s.start_iso, "end": s.end_iso, "allDay": False}
for s in self.event.defaultslot_set.all()
]
initial['availabilities'] = json.dumps({
'availabilities': default_slots
})
return initial
def form_valid(self, form):
default_slots_raw = json.loads(form.cleaned_data['availabilities'])["availabilities"]
tz = self.event.timezone
created_count = 0
updated_count = 0
previous_slot_ids = set(s.id for s in self.event.defaultslot_set.all())
for slot in default_slots_raw:
start = parse_datetime(slot["start"]).astimezone(tz)
end = parse_datetime(slot["end"]).astimezone(tz)
if slot["id"] != '':
id = int(slot["id"])
if 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(
self.request,
messages.WARNING,
_("Could not update slot {id} since it does not belong to {event}")
.format(id=slot['id'], event=self.event.name)
)
else:
# Update existing entries
previous_slot_ids.remove(id)
original_slot = DefaultSlot.objects.get(id=id)
if original_slot.start != start or original_slot.end != end:
original_slot.start = start
original_slot.end = end
original_slot.save()
updated_count += 1
else:
# Create new entries
DefaultSlot.objects.create(
start=start,
end=end,
event=self.event
)
created_count += 1
# Delete all slots not re-submitted by the user (and hence deleted in editor)
deleted_count = len(previous_slot_ids)
for d_id in previous_slot_ids:
DefaultSlot.objects.get(id=d_id).delete()
if created_count + updated_count + deleted_count > 0:
messages.add_message(
self.request,
messages.SUCCESS,
_("Updated {u} slot(s). created {c} new slot(s) and deleted {d} slot(s)")
.format(u=str(updated_count), c=str(created_count), d=str(deleted_count))
)
return super().form_valid(form)
class RoomCreationView(AdminViewMixin, CreateView):
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')
elif 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')
def form_valid(self, form):
self.room = form.save()
messages.success(self.request, _("Created Room '%(room)s'" % {'room': self.room}))
return HttpResponseRedirect(self.get_success_url())
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):
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:
r = Room.objects.create(name=name,
location=location,
capacity=capacity,
event=self.event)
if virtual_rooms_support and raw_room["url"] != "":
VirtualRoom.objects.create(room=r,
url=raw_room["url"])
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)