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
  • koma/feature/preference-polling-form
  • komasolver
  • main
  • renovate/django-5.x
  • renovate/django_csp-4.x
  • renovate/jsonschema-4.x
  • renovate/uwsgi-2.x
8 results

Target

Select target project
  • konstantin/akplanning
  • matedealer/akplanning
  • kif/akplanning
  • mirco/akplanning
  • lordofthevoid/akplanning
  • voidptr/akplanning
  • xayomer/akplanning-fork
  • mollux/akplanning
  • neumantm/akplanning
  • mmarx/akplanning
  • nerf/akplanning
  • felix_bonn/akplanning
  • sebastian.uschmann/akplanning
13 results
Select Git revision
  • ak-import
  • feature/clear-schedule-button
  • feature/export-filtering
  • feature/json-export-via-rest-framework
  • feature/json-schedule-import-tests
  • feature/preference-polling-form
  • fix/add-room-import-only-once
  • main
  • renovate/django-5.x
  • renovate/django-debug-toolbar-4.x
  • renovate/django-simple-history-3.x
  • renovate/mysqlclient-2.x
12 results
Show changes
Showing
with 1135 additions and 102 deletions
# Generated by Django 5.1.6 on 2025-03-29 22:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("AKModel", "0063_field_validators"),
]
operations = [
migrations.AddField(
model_name="event",
name="export_slot",
field=models.DecimalField(
decimal_places=2,
default=1,
help_text="Slot duration in hours that is used in the timeslot discretization, when this event is exported for the solver.",
max_digits=4,
verbose_name="Export Slot Length",
),
),
]
# Generated by Django 4.2.13 on 2025-02-10 10:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("AKModel", "0064_event_export_slot"),
]
operations = [
migrations.CreateModel(
name="EventParticipant",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
blank=True,
help_text="Name to identify a participant by (in case of questions from the organizers)",
max_length=64,
verbose_name="Nickname",
),
),
(
"institution",
models.CharField(
blank=True,
help_text="Uni etc.",
max_length=128,
verbose_name="Institution",
),
),
(
"event",
models.ForeignKey(
help_text="Associated event",
on_delete=django.db.models.deletion.CASCADE,
to="AKModel.event",
verbose_name="Event",
),
),
],
options={
"verbose_name": "Participant",
"verbose_name_plural": "Participants",
"ordering": ["name"],
},
),
migrations.CreateModel(
name="AKPreference",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"preference",
models.PositiveSmallIntegerField(
choices=[
(0, "Ignore"),
(1, "Prefer"),
(2, "Strong prefer"),
(3, "Required"),
],
default=0,
help_text="Preference level for the AK",
verbose_name="Preference",
),
),
(
"ak",
models.ForeignKey(
help_text="AK this preference belongs to",
on_delete=django.db.models.deletion.CASCADE,
to="AKModel.ak",
verbose_name="AK",
),
),
(
"event",
models.ForeignKey(
help_text="Associated event",
on_delete=django.db.models.deletion.CASCADE,
to="AKModel.event",
verbose_name="Event",
),
),
(
"participant",
models.ForeignKey(
help_text="Participant this preference belongs to",
on_delete=django.db.models.deletion.CASCADE,
to="AKModel.eventparticipant",
verbose_name="Participant",
),
),
],
options={
"verbose_name": "AK Preference",
"verbose_name_plural": "AK Preferences",
},
),
migrations.AddField(
model_name="availability",
name="participant",
field=models.ForeignKey(
blank=True,
help_text="Participant whose availability this is",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="availabilities",
to="AKModel.eventparticipant",
verbose_name="Participant",
),
),
]
# Generated by Django 4.2.13 on 2025-02-10 22:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("AKModel", "0065_eventparticipant_akpreference_and_more"),
]
operations = [
migrations.AddField(
model_name="akpreference",
name="slot",
field=models.ForeignKey(
default=None,
help_text="AKSlot this preference belongs to",
on_delete=django.db.models.deletion.CASCADE,
to="AKModel.akslot",
verbose_name="AKSlot",
),
preserve_default=False,
),
migrations.AlterUniqueTogether(
name="akpreference",
unique_together={("event", "participant", "slot")},
),
migrations.RemoveField(
model_name="akpreference",
name="ak",
),
]
# Generated by Django 4.2.13 on 2025-02-11 00:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
(
"AKModel",
"0066_akpreference_slot_alter_akpreference_unique_together_and_more",
),
]
operations = [
migrations.AddField(
model_name="eventparticipant",
name="requirements",
field=models.ManyToManyField(
blank=True,
help_text="Participant's Requirements",
to="AKModel.akrequirement",
verbose_name="Requirements",
),
),
migrations.AlterField(
model_name="akpreference",
name="slot",
field=models.ForeignKey(
help_text="AK Slot this preference belongs to",
on_delete=django.db.models.deletion.CASCADE,
to="AKModel.akslot",
verbose_name="AK Slot",
),
),
]
# Generated by Django 5.2.1 on 2025-06-17 15:57
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0067_eventparticipant_requirements_and_more'),
('AKPreference', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='eventparticipant',
name='event',
),
migrations.RemoveField(
model_name='eventparticipant',
name='requirements',
),
migrations.RemoveField(
model_name='availability',
name='participant',
),
migrations.AddField(
model_name='event',
name='poll_hidden',
field=models.BooleanField(default=True, help_text='Hides preference poll for non-staff users', verbose_name='Poll Hidden'),
),
migrations.AddField(
model_name='event',
name='poll_published_at',
field=models.DateTimeField(blank=True, help_text='Timestamp at which the preference poll was published', null=True, verbose_name='Poll published at'),
),
migrations.DeleteModel(
name='AKPreference',
),
migrations.DeleteModel(
name='EventParticipant',
),
migrations.AddField(
model_name='availability',
name='participant',
field=models.ForeignKey(blank=True, help_text='Participant whose availability this is', null=True,
on_delete=django.db.models.deletion.CASCADE, related_name='availabilities',
to='AKPreference.eventparticipant', verbose_name='Participant'),
),
]
This diff is collapsed.
from rest_framework import serializers
from AKModel.models import AK, Room, AKSlot, AKTrack, AKCategory, AKOwner
from AKModel.models import AK, AKCategory, AKOwner, AKSlot, AKTrack, Room
class StringListField(serializers.ListField):
"""List field containing strings."""
child = serializers.CharField()
class IntListField(serializers.ListField):
"""List field containing integers."""
child = serializers.IntegerField()
class AKOwnerSerializer(serializers.ModelSerializer):
......
......@@ -4,8 +4,8 @@
{% block content %}
<pre>
title;duration;who;requirements;prerequisites;conflicts;availabilities;category;track;reso;notes;
{% for slot in slots %}{{ slot.ak.short_name }};{{ slot.duration }};{{ slot.ak.owners.all|join:", " }};{{ slot.ak.requirements.all|join:", " }};{{ slot.ak.prerequisites.all|join:", " }};{{ slot.ak.conflicts.all|join:", " }};{% for a in slot.ak.availabilities.all %}{{ a.start | timezone:event.timezone | date:"l H:i" }} - {{ a.end | timezone:event.timezone | date:"l H:i" }}, {% endfor %};{{ slot.ak.category }};{{ slot.ak.track }};{{ slot.ak.reso }};{{ slot.ak.notes }};
title;duration;who;requirements;prerequisites;conflicts;availabilities;category;types;track;reso;notes;
{% for slot in slots %}{{ slot.ak.short_name }};{{ slot.duration }};{{ slot.ak.owners.all|join:", " }};{{ slot.ak.requirements.all|join:", " }};{{ slot.ak.prerequisites.all|join:", " }};{{ slot.ak.conflicts.all|join:", " }};{% for a in slot.ak.availabilities.all %}{{ a.start | timezone:event.timezone | date:"l H:i" }} - {{ a.end | timezone:event.timezone | date:"l H:i" }}, {% endfor %};{{ slot.ak.category }};{{ slot.ak.types.all|join:", " }};{{ slot.ak.track }};{{ slot.ak.reso }};{{ slot.ak.notes }};
{% endfor %}
</pre>
{% endblock %}
{% extends "admin/base_site.html" %}
{% load tags_AKModel %}
{% load i18n %}
{% load tz %}
{% load fontawesome_6 %}
{% block title %}{% trans "AKs by Owner" %}: {{owner}}{% endblock %}
{% block content %}
{% timezone event.timezone %}
<h2>[{{event}}] <a href="{% url 'admin:AKModel_akowner_change' owner.pk %}">{{owner}}</a> - {% trans "AKs" %}</h2>
<div class="row mt-4">
<table class="table table-striped">
{% for ak in owner.ak_set.all %}
<tr>
<td>{{ ak }}</td>
{% if "AKSubmission"|check_app_installed %}
<td class="text-end">
<a href="{{ ak.detail_url }}" data-bs-toggle="tooltip"
title="{% trans 'Details' %}"
class="btn btn-primary">{% fa6_icon 'info' 'fas' %}</a>
{% if event.active %}
<a href="{{ ak.edit_url }}" data-bs-toggle="tooltip"
title="{% trans 'Edit' %}"
class="btn btn-success">{% fa6_icon 'pencil-alt' 'fas' %}</a>
{% endif %}
{% endif %}
</td>
</tr>
{% empty %}
<tr><td>{% trans "This user does not have any AKs currently" %}</td></tr>
{% endfor %}
</table>
</div>
{% endtimezone %}
{% endblock %}
......@@ -3,8 +3,11 @@ 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()
......@@ -71,6 +74,21 @@ 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()
......
......@@ -5,10 +5,21 @@ 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:
......@@ -29,9 +40,10 @@ class BasicViewTests:
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 = []
......@@ -41,16 +53,26 @@ class BasicViewTests:
"""
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
username="Test Staff User",
email="teststaff@example.com",
password="staffpw",
is_staff=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
username="Test Admin User",
email="testadmin@example.com",
password="adminpw",
is_staff=True,
is_superuser=True,
is_active=True,
)
self.deactivated_user = user_model.objects.create(
username='Test Deactivated User', email='testdeactivated@example.com', password='deactivatedpw',
is_staff=True, is_active=False
username="Test Deactivated User",
email="testdeactivated@example.com",
password="deactivatedpw",
is_staff=True,
is_active=False,
)
def _name_and_url(self, view_name):
......@@ -62,7 +84,9 @@ 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
......@@ -74,7 +98,7 @@ class BasicViewTests:
: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))
messages: List[Message] = list(get_messages(response.wsgi_request))
msg_count = "No message shown to user"
msg_content = "Wrong message, expected '{expected_message}'"
......@@ -95,10 +119,16 @@ class BasicViewTests:
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: # 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()}")
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):
"""
......@@ -107,11 +137,16 @@ class BasicViewTests:
# Not logged in? Views should not be visible
self.client.logout()
for view_name_info in self.VIEWS_STAFF_ONLY:
expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2]
expected_response_code = (
302 if len(view_name_info) == 2 else view_name_info[2]
)
view_name_with_prefix, url = self._name_and_url(view_name_info)
response = self.client.get(url)
self.assertEqual(response.status_code, expected_response_code,
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)
......@@ -119,20 +154,30 @@ class BasicViewTests:
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)")
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()}")
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_info in self.VIEWS_STAFF_ONLY:
expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2]
expected_response_code = (
302 if len(view_name_info) == 2 else view_name_info[2]
)
view_name_with_prefix, url = self._name_and_url(view_name_info)
response = self.client.get(url)
self.assertEqual(response.status_code, expected_response_code,
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, val):
"""
......@@ -182,16 +227,26 @@ 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}")
......@@ -200,30 +255,42 @@ class ModelViewTests(BasicViewTests, TestCase):
"""
Basic view test cases for views from AKModel plus some custom tests
"""
fixtures = ['model.json']
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', {'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', {}),
("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):
......@@ -234,24 +301,32 @@ class ModelViewTests(BasicViewTests, TestCase):
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":
_, url = self._name_and_url(('admin:new_event_wizard_start', {}))
_, url = self._name_and_url(("admin:new_event_wizard_start", {}))
elif model[1] == "room":
_, url = self._name_and_url(('admin:room-new', {}))
_, url = self._name_and_url(("admin:room-new", {}))
# Otherwise, just call the creation form view
else:
_, url = self._name_and_url((f'admin:AKModel_{model[1]}_add', {}))
_, url = self._name_and_url((f"admin:AKModel_{model[1]}_add", {}))
response = self.client.get(url)
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:
_, url = self._name_and_url(
(f'admin:AKModel_{model[1]}_change', {'object_id': m.pk})
(f"admin:AKModel_{model[1]}_change", {"object_id": m.pk})
)
response = self.client.get(url)
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):
"""
......@@ -260,17 +335,27 @@ class ModelViewTests(BasicViewTests, TestCase):
"""
self.client.force_login(self.admin_user)
export_url = reverse_lazy("admin:ak_wiki_export", kwargs={'slug': 'kif42'})
export_url = reverse_lazy("admin:ak_wiki_export", kwargs={"slug": "kif42"})
response = self.client.get(export_url)
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")
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",
)
......@@ -4,9 +4,10 @@ from django.urls import include, path
from rest_framework.routers import DefaultRouter
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.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, \
PollPublishView, PollUnpublishView, 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
......@@ -44,6 +45,11 @@ 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'),
]
......@@ -103,6 +109,8 @@ def get_admin_urls_event(admin_site):
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('poll/publish/', admin_site.admin_view(PollPublishView.as_view()), name="poll-publish"),
path('poll/unpublish/', admin_site.admin_view(PollUnpublishView.as_view()), name="poll-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()),
......
......@@ -4,6 +4,7 @@ import os
import tempfile
from itertools import zip_longest
from django.contrib import messages
from django.db.models.functions import Now
from django.utils.dateparse import parse_datetime
......@@ -58,7 +59,7 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
Create a list of tuples cosisting of an AK and a list of upcoming AKs (list length depending on setting)
"""
next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None)
return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=[])]
return list(zip_longest(ak_list, next_aks_list, fillvalue=[]))
# Get all relevant AKs (wishes separately, and either all AKs or only those who should directly or indirectly
# be presented when restriction setting was chosen)
......@@ -157,6 +158,31 @@ class PlanUnpublishView(IntermediateAdminActionView):
self.entities.update(plan_published_at=None, plan_hidden=True)
class PollPublishView(IntermediateAdminActionView):
"""
Admin action view: Publish the preference poll of one or multitple event(s)
"""
title = _('Publish preference poll')
model = Event
confirmation_message = _('Publish the poll(s) of:')
success_message = _('Preference poll published')
def action(self, form):
self.entities.update(poll_published_at=Now(), poll_hidden=False)
class PollUnpublishView(IntermediateAdminActionView):
"""
Admin action view: Unpublish the preference poll of one or multitple event(s)
"""
title = _('Unpublish preference poll')
model = Event
confirmation_message = _('Unpublish the preference poll(s) of:')
success_message = _('Preference poll unpublished')
def action(self, form):
self.entities.update(poll_published_at=None, poll_hidden=True)
class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
"""
Admin view: Allow to edit the default slots of an event
......
......@@ -76,10 +76,15 @@ class EventRoomsWidget(TemplateStatusWidget):
def render_actions(self, context: {}) -> list[dict]:
actions = super().render_actions(context)
# Action has to be added here since it depends on the event for URL building
import_room_url = reverse_lazy("admin:room-import", kwargs={"event_slug": context["event"].slug})
for action in actions:
if action["url"] == import_room_url:
return actions
actions.append(
{
"text": _("Import Rooms from CSV"),
"url": reverse_lazy("admin:room-import", kwargs={"event_slug": context["event"].slug}),
"url": import_room_url,
}
)
return actions
......@@ -147,6 +152,17 @@ class EventAKsWidget(TemplateStatusWidget):
},
]
)
if apps.is_installed("AKSolverInterface"):
actions.extend([
{
"text": _("Export AKs as JSON"),
"url": reverse_lazy("admin:ak_json_export", kwargs={"event_slug": context["event"].slug}),
},
{
"text": _("Import AK schedule from JSON"),
"url": reverse_lazy("admin:ak_schedule_json_import", kwargs={"event_slug": context["event"].slug}),
},
])
return actions
......
from django.test import TestCase
from AKModel.tests import BasicViewTests
from AKModel.tests.test_views import BasicViewTests
class PlanViewTests(BasicViewTests, TestCase):
......
from datetime import timedelta
from datetime import datetime, timedelta
from django.conf import settings
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.datetime_safe import datetime
from django.views.generic import ListView, DetailView
from django.views.generic import DetailView, ListView
from AKModel.models import AKSlot, Room, AKTrack
from AKModel.metaviews.admin import FilterByEventSlugMixin
from AKModel.models import AKSlot, AKTrack, Room
class PlanIndexView(FilterByEventSlugMixin, ListView):
......@@ -152,7 +151,7 @@ class PlanTrackView(FilterByEventSlugMixin, DetailView):
context = super().get_context_data(object_list=object_list, **kwargs)
# Restrict AKSlot list to given track
# while joining AK, room and category information to reduce the amount of necessary SQL queries
context["slots"] = AKSlot.objects.\
filter(event=self.event, ak__track=context['track']).\
context["slots"] = AKSlot.objects. \
filter(event=self.event, ak__track=context['track']). \
select_related('ak', 'room', 'ak__category')
return context
......@@ -10,6 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/
"""
import decimal
import os
from django.utils.translation import gettext_lazy as _
......@@ -38,6 +39,8 @@ INSTALLED_APPS = [
'AKScheduling.apps.AkschedulingConfig',
'AKPlan.apps.AkplanConfig',
'AKOnline.apps.AkonlineConfig',
'AKPreference.apps.AkpreferenceConfig',
'AKSolverInterface.apps.AksolverinterfaceConfig',
'AKModel.apps.AKAdminConfig',
'django.contrib.auth',
'django.contrib.contenttypes',
......@@ -217,6 +220,15 @@ PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS = 2 * 60 * 60
DASHBOARD_SHOW_RECENT = True
# How many entries max?
DASHBOARD_RECENT_MAX = 25
# How many events should be featured in the dashboard
# (active events will always be featured, even if their number is higher than this threshold)
DASHBOARD_MAX_FEATURED_EVENTS = 3
# In the export to the solver we need to calculate the integer number
# of discrete time slots covered by an AK. This is done by rounding
# the 'exact' number up. To avoid 'overshooting' by 1
# due to FLOP inaccuracies, we subtract this small epsilon before rounding.
EXPORT_CEIL_OFFSET_EPS = decimal.Decimal(1e-4)
# Registration/login behavior
SIMPLE_BACKEND_REDIRECT_URL = "/user/"
......@@ -224,7 +236,7 @@ LOGIN_REDIRECT_URL = SIMPLE_BACKEND_REDIRECT_URL
# Content Security Policy
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'")
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", "'unsafe-eval'")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "fonts.googleapis.com")
CSP_IMG_SRC = ("'self'", "data:")
CSP_FRAME_SRC = ("'self'", )
......
......@@ -37,3 +37,5 @@ if apps.is_installed("AKDashboard"):
urlpatterns.append(path('', include('AKDashboard.urls', namespace='dashboard')))
if apps.is_installed("AKPlan"):
urlpatterns.append(path('', include('AKPlan.urls', namespace='plan')))
if apps.is_installed("AKPreference"):
urlpatterns.append(path('', include('AKPreference.urls', namespace='poll')))