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
  • koma/feature/preference-polling-form
  • komasolver
  • main
  • renovate/django_csp-4.x
4 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
  • fix/responsive-cols-in-polls
  • main
  • renovate/django-5.x
  • renovate/django-debug-toolbar-4.x
  • renovate/django-simple-history-3.x
  • renovate/mysqlclient-2.x
13 results
Show changes
Showing
with 1320 additions and 120 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.1.6 on 2025-05-11 15:21
from django.db import migrations, models
def create_slugs(apps, schema_editor):
"""
Automatically generate slugs from existing type names
"""
AKType = apps.get_model("AKModel", "AKType")
for ak_type in AKType.objects.all():
slug = ak_type.name.lower().split(" ")[0]
ak_type.slug = slug[:30] if len(slug) > 30 else slug
ak_type.save()
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0067_eventparticipant_requirements_and_more'),
]
operations = [
migrations.AddField(
model_name='aktype',
name='slug',
field=models.SlugField(max_length=30, null=True, verbose_name='Slug'),
),
migrations.RunPython(create_slugs, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='aktype',
unique_together={('event', 'name')},
),
migrations.AlterField(
model_name='aktype',
name='slug',
field=models.SlugField(max_length=30, verbose_name='Slug'),
),
migrations.AlterUniqueTogether(
name='aktype',
unique_together={('event', 'name'), ('event', 'slug')},
),
]
# 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'),
),
]
# Generated by Django 5.2.1 on 2025-06-17 18:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0068_aktype_slug'),
('AKModel', '0068_event_export_delete_preferences_participants'),
]
operations = [
]
# Generated by Django 5.2.3 on 2025-06-18 10:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0069_merge_20250617_1844'),
]
operations = [
migrations.AddField(
model_name='akrequirement',
name='relevant_for_participants',
field=models.BooleanField(default=False, help_text='Show this requirement when collecting participant preferences', verbose_name='Relevant for Participants?'),
),
]
This diff is collapsed.
from rest_framework import serializers 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): class AKOwnerSerializer(serializers.ModelSerializer):
......
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
{% block content %} {% block content %}
<pre> <pre>
title;duration;who;requirements;prerequisites;conflicts;availabilities;category;track;reso;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.track }};{{ slot.ak.reso }};{{ slot.ak.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 %} {% endfor %}
</pre> </pre>
{% endblock %} {% 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 %}
...@@ -19,6 +19,10 @@ ...@@ -19,6 +19,10 @@
\faUser~ {{ translations.who }} \faUser~ {{ translations.who }}
{% if show_types %}
\faList~ {{ translations.types }}
{% endif %}
\faClock~ {{ translations.duration }} \faClock~ {{ translations.duration }}
\faScroll~{{ translations.reso }} \faScroll~{{ translations.reso }}
...@@ -45,6 +49,10 @@ ...@@ -45,6 +49,10 @@
\faUser~ {{ ak.owners_list | latex_escape }} \faUser~ {{ ak.owners_list | latex_escape }}
{% if show_types %}
\faList~ {{ak.types_list }}
{% endif %}
{% if not result_presentation_mode %} {% if not result_presentation_mode %}
\faClock~ {{ak.durations_list}} \faClock~ {{ak.durations_list}}
{% endif %} {% endif %}
......
...@@ -3,8 +3,11 @@ from django.apps import apps ...@@ -3,8 +3,11 @@ from django.apps import apps
from django.conf import settings from django.conf import settings
from django.utils.html import format_html, mark_safe, conditional_escape from django.utils.html import format_html, mark_safe, conditional_escape
from django.templatetags.static import static from django.templatetags.static import static
from django.template.defaultfilters import date
from fontawesome_6.app_settings import get_css from fontawesome_6.app_settings import get_css
from AKModel.models import Event
register = template.Library() register = template.Library()
...@@ -71,6 +74,21 @@ def wiki_owners_export(owners, event): ...@@ -71,6 +74,21 @@ def wiki_owners_export(owners, event):
return ", ".join(to_link(owner) for owner in owners.all()) 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 # get list of relevant css fontawesome css files for this instance
css = get_css() css = get_css()
......
...@@ -5,10 +5,21 @@ from django.contrib.auth import get_user_model ...@@ -5,10 +5,21 @@ from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages from django.contrib.messages import get_messages
from django.contrib.messages.storage.base import Message from django.contrib.messages.storage.base import Message
from django.test import TestCase from django.test import TestCase
from django.urls import reverse_lazy, reverse from django.urls import reverse, reverse_lazy
from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, Room, AKSlot, AKOrgaMessage, \ from AKModel.models import (
ConstraintViolation, DefaultSlot AK,
AKCategory,
AKOrgaMessage,
AKOwner,
AKRequirement,
AKSlot,
AKTrack,
ConstraintViolation,
DefaultSlot,
Event,
Room,
)
class BasicViewTests: class BasicViewTests:
...@@ -29,9 +40,10 @@ class BasicViewTests: ...@@ -29,9 +40,10 @@ class BasicViewTests:
since the test framework does not understand the concept of abstract test definitions and would handle this class since the test framework does not understand the concept of abstract test definitions and would handle this class
as real test case otherwise, distorting the test results. as real test case otherwise, distorting the test results.
""" """
# pylint: disable=no-member # pylint: disable=no-member
VIEWS = [] VIEWS = []
APP_NAME = '' APP_NAME = ""
VIEWS_STAFF_ONLY = [] VIEWS_STAFF_ONLY = []
EDIT_TESTCASES = [] EDIT_TESTCASES = []
...@@ -41,16 +53,26 @@ class BasicViewTests: ...@@ -41,16 +53,26 @@ class BasicViewTests:
""" """
user_model = get_user_model() user_model = get_user_model()
self.staff_user = user_model.objects.create( self.staff_user = user_model.objects.create(
username='Test Staff User', email='teststaff@example.com', password='staffpw', username="Test Staff User",
is_staff=True, is_active=True email="teststaff@example.com",
password="staffpw",
is_staff=True,
is_active=True,
) )
self.admin_user = user_model.objects.create( self.admin_user = user_model.objects.create(
username='Test Admin User', email='testadmin@example.com', password='adminpw', username="Test Admin User",
is_staff=True, is_superuser=True, is_active=True email="testadmin@example.com",
password="adminpw",
is_staff=True,
is_superuser=True,
is_active=True,
) )
self.deactivated_user = user_model.objects.create( self.deactivated_user = user_model.objects.create(
username='Test Deactivated User', email='testdeactivated@example.com', password='deactivatedpw', username="Test Deactivated User",
is_staff=True, is_active=False email="testdeactivated@example.com",
password="deactivatedpw",
is_staff=True,
is_active=False,
) )
def _name_and_url(self, view_name): def _name_and_url(self, view_name):
...@@ -62,7 +84,9 @@ class BasicViewTests: ...@@ -62,7 +84,9 @@ class BasicViewTests:
:return: full view name with prefix if applicable, url of the view :return: full view name with prefix if applicable, url of the view
:rtype: str, str :rtype: str, str
""" """
view_name_with_prefix = f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0] view_name_with_prefix = (
f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0]
)
url = reverse(view_name_with_prefix, kwargs=view_name[1]) url = reverse(view_name_with_prefix, kwargs=view_name[1])
return view_name_with_prefix, url return view_name_with_prefix, url
...@@ -74,7 +98,7 @@ class BasicViewTests: ...@@ -74,7 +98,7 @@ class BasicViewTests:
:param expected_message: message that should be shown :param expected_message: message that should be shown
:param msg_prefix: prefix for the error message when test fails :param msg_prefix: prefix for the error message when test fails
""" """
messages:List[Message] = list(get_messages(response.wsgi_request)) messages: List[Message] = list(get_messages(response.wsgi_request))
msg_count = "No message shown to user" msg_count = "No message shown to user"
msg_content = "Wrong message, expected '{expected_message}'" msg_content = "Wrong message, expected '{expected_message}'"
...@@ -95,10 +119,16 @@ class BasicViewTests: ...@@ -95,10 +119,16 @@ class BasicViewTests:
view_name_with_prefix, url = self._name_and_url(view_name) view_name_with_prefix, url = self._name_and_url(view_name)
try: try:
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"{view_name_with_prefix} ({url}) broken") self.assertEqual(
except Exception: # pylint: disable=broad-exception-caught response.status_code,
self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):" 200,
f"\n\n{traceback.format_exc()}") msg=f"{view_name_with_prefix} ({url}) broken",
)
except Exception: # pylint: disable=broad-exception-caught
self.fail(
f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
f"\n\n{traceback.format_exc()}"
)
def test_access_control_staff_only(self): def test_access_control_staff_only(self):
""" """
...@@ -107,11 +137,16 @@ class BasicViewTests: ...@@ -107,11 +137,16 @@ class BasicViewTests:
# Not logged in? Views should not be visible # Not logged in? Views should not be visible
self.client.logout() self.client.logout()
for view_name_info in self.VIEWS_STAFF_ONLY: for view_name_info in self.VIEWS_STAFF_ONLY:
expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2] expected_response_code = (
302 if len(view_name_info) == 2 else view_name_info[2]
)
view_name_with_prefix, url = self._name_and_url(view_name_info) view_name_with_prefix, url = self._name_and_url(view_name_info)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, expected_response_code, self.assertEqual(
msg=f"{view_name_with_prefix} ({url}) accessible by non-staff") response.status_code,
expected_response_code,
msg=f"{view_name_with_prefix} ({url}) accessible by non-staff",
)
# Logged in? Views should be visible # Logged in? Views should be visible
self.client.force_login(self.staff_user) self.client.force_login(self.staff_user)
...@@ -119,20 +154,30 @@ class BasicViewTests: ...@@ -119,20 +154,30 @@ class BasicViewTests:
view_name_with_prefix, url = self._name_and_url(view_name_info) view_name_with_prefix, url = self._name_and_url(view_name_info)
try: try:
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, self.assertEqual(
msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)") response.status_code,
200,
msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)",
)
except Exception: # pylint: disable=broad-exception-caught except Exception: # pylint: disable=broad-exception-caught
self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):" self.fail(
f"\n\n{traceback.format_exc()}") f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
f"\n\n{traceback.format_exc()}"
)
# Disabled user? Views should not be visible # Disabled user? Views should not be visible
self.client.force_login(self.deactivated_user) self.client.force_login(self.deactivated_user)
for view_name_info in self.VIEWS_STAFF_ONLY: for view_name_info in self.VIEWS_STAFF_ONLY:
expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2] expected_response_code = (
302 if len(view_name_info) == 2 else view_name_info[2]
)
view_name_with_prefix, url = self._name_and_url(view_name_info) view_name_with_prefix, url = self._name_and_url(view_name_info)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, expected_response_code, self.assertEqual(
msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user") response.status_code,
expected_response_code,
msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user",
)
def _to_sendable_value(self, val): def _to_sendable_value(self, val):
""" """
...@@ -182,16 +227,26 @@ class BasicViewTests: ...@@ -182,16 +227,26 @@ class BasicViewTests:
self.client.logout() self.client.logout()
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"{name}: Could not load edit form via GET ({url})") self.assertEqual(
response.status_code,
200,
msg=f"{name}: Could not load edit form via GET ({url})",
)
form = response.context[form_name] form = response.context[form_name]
data = {k:self._to_sendable_value(v) for k,v in form.initial.items()} data = {k: self._to_sendable_value(v) for k, v in form.initial.items()}
response = self.client.post(url, data=data) response = self.client.post(url, data=data)
if expected_code == 200: if expected_code == 200:
self.assertEqual(response.status_code, 200, msg=f"{name}: Did not return 200 ({url}") self.assertEqual(
response.status_code, 200, msg=f"{name}: Did not return 200 ({url}"
)
elif expected_code == 302: elif expected_code == 302:
self.assertRedirects(response, target_url, msg_prefix=f"{name}: Did not redirect ({url} -> {target_url}") self.assertRedirects(
response,
target_url,
msg_prefix=f"{name}: Did not redirect ({url} -> {target_url}",
)
if expected_message != "": if expected_message != "":
self._assert_message(response, expected_message, msg_prefix=f"{name}") self._assert_message(response, expected_message, msg_prefix=f"{name}")
...@@ -200,30 +255,42 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -200,30 +255,42 @@ class ModelViewTests(BasicViewTests, TestCase):
""" """
Basic view test cases for views from AKModel plus some custom tests Basic view test cases for views from AKModel plus some custom tests
""" """
fixtures = ['model.json']
fixtures = ["model.json"]
ADMIN_MODELS = [ ADMIN_MODELS = [
(Event, 'event'), (AKOwner, 'akowner'), (AKCategory, 'akcategory'), (AKTrack, 'aktrack'), (Event, "event"),
(AKRequirement, 'akrequirement'), (AK, 'ak'), (Room, 'room'), (AKSlot, 'akslot'), (AKOwner, "akowner"),
(AKOrgaMessage, 'akorgamessage'), (ConstraintViolation, 'constraintviolation'), (AKCategory, "akcategory"),
(DefaultSlot, 'defaultslot') (AKTrack, "aktrack"),
(AKRequirement, "akrequirement"),
(AK, "ak"),
(Room, "room"),
(AKSlot, "akslot"),
(AKOrgaMessage, "akorgamessage"),
(ConstraintViolation, "constraintviolation"),
(DefaultSlot, "defaultslot"),
] ]
VIEWS_STAFF_ONLY = [ VIEWS_STAFF_ONLY = [
('admin:index', {}), ("admin:index", {}),
('admin:event_status', {'event_slug': 'kif42'}), ("admin:event_status", {"event_slug": "kif42"}),
('admin:event_requirement_overview', {'event_slug': 'kif42'}), ("admin:event_requirement_overview", {"event_slug": "kif42"}),
('admin:ak_csv_export', {'event_slug': 'kif42'}), ("admin:ak_csv_export", {"event_slug": "kif42"}),
('admin:ak_wiki_export', {'slug': 'kif42'}), ("admin:ak_wiki_export", {"slug": "kif42"}),
('admin:ak_delete_orga_messages', {'event_slug': 'kif42'}), ("admin:ak_delete_orga_messages", {"event_slug": "kif42"}),
('admin:ak_slide_export', {'event_slug': 'kif42'}), ("admin:ak_slide_export", {"event_slug": "kif42"}),
('admin:default-slots-editor', {'event_slug': 'kif42'}), ("admin:default-slots-editor", {"event_slug": "kif42"}),
('admin:room-import', {'event_slug': 'kif42'}), ("admin:room-import", {"event_slug": "kif42"}),
('admin:new_event_wizard_start', {}), ("admin:new_event_wizard_start", {}),
] ]
EDIT_TESTCASES = [ EDIT_TESTCASES = [
{'view': 'admin:default-slots-editor', 'kwargs': {'event_slug': 'kif42'}, "admin": True}, {
"view": "admin:default-slots-editor",
"kwargs": {"event_slug": "kif42"},
"admin": True,
},
] ]
def test_admin(self): def test_admin(self):
...@@ -234,24 +301,32 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -234,24 +301,32 @@ class ModelViewTests(BasicViewTests, TestCase):
for model in self.ADMIN_MODELS: for model in self.ADMIN_MODELS:
# Special treatment for a subset of views (where we exchanged default functionality, e.g., create views) # Special treatment for a subset of views (where we exchanged default functionality, e.g., create views)
if model[1] == "event": if model[1] == "event":
_, url = self._name_and_url(('admin:new_event_wizard_start', {})) _, url = self._name_and_url(("admin:new_event_wizard_start", {}))
elif model[1] == "room": elif model[1] == "room":
_, url = self._name_and_url(('admin:room-new', {})) _, url = self._name_and_url(("admin:room-new", {}))
# Otherwise, just call the creation form view # Otherwise, just call the creation form view
else: else:
_, url = self._name_and_url((f'admin:AKModel_{model[1]}_add', {})) _, url = self._name_and_url((f"admin:AKModel_{model[1]}_add", {}))
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"Add form for model {model[1]} ({url}) broken") self.assertEqual(
response.status_code,
200,
msg=f"Add form for model {model[1]} ({url}) broken",
)
for model in self.ADMIN_MODELS: for model in self.ADMIN_MODELS:
# Test the update view using the first existing instance of each model # Test the update view using the first existing instance of each model
m = model[0].objects.first() m = model[0].objects.first()
if m is not None: if m is not None:
_, url = self._name_and_url( _, url = self._name_and_url(
(f'admin:AKModel_{model[1]}_change', {'object_id': m.pk}) (f"admin:AKModel_{model[1]}_change", {"object_id": m.pk})
) )
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"Edit form for model {model[1]} ({url}) broken") self.assertEqual(
response.status_code,
200,
msg=f"Edit form for model {model[1]} ({url}) broken",
)
def test_wiki_export(self): def test_wiki_export(self):
""" """
...@@ -260,17 +335,27 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -260,17 +335,27 @@ class ModelViewTests(BasicViewTests, TestCase):
""" """
self.client.force_login(self.admin_user) self.client.force_login(self.admin_user)
export_url = reverse_lazy("admin:ak_wiki_export", kwargs={'slug': 'kif42'}) export_url = reverse_lazy("admin:ak_wiki_export", kwargs={"slug": "kif42"})
response = self.client.get(export_url) response = self.client.get(export_url)
self.assertEqual(response.status_code, 200, "Export not working at all") self.assertEqual(response.status_code, 200, "Export not working at all")
export_count = 0 export_count = 0
for _, aks in response.context["categories_with_aks"]: for _, aks in response.context["categories_with_aks"]:
for ak in aks: for ak in aks:
self.assertEqual(ak.include_in_export, True, self.assertEqual(
f"AK with export flag set to False (pk={ak.pk}) included in export") ak.include_in_export,
self.assertNotEqual(ak.pk, 1, "AK known to be excluded from export (PK 1) included in export") True,
f"AK with export flag set to False (pk={ak.pk}) included in export",
)
self.assertNotEqual(
ak.pk,
1,
"AK known to be excluded from export (PK 1) included in export",
)
export_count += 1 export_count += 1
self.assertEqual(export_count, AK.objects.filter(event_id=2, include_in_export=True).count(), self.assertEqual(
"Wiki export contained the wrong number of AKs") export_count,
AK.objects.filter(event_id=2, include_in_export=True).count(),
"Wiki export contained the wrong number of AKs",
)
...@@ -4,9 +4,10 @@ from django.urls import include, path ...@@ -4,9 +4,10 @@ from django.urls import include, path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
import AKModel.views.api import AKModel.views.api
from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, \ from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, \
AKsByUserView PollPublishView, PollUnpublishView, DefaultSlotEditorView, AKsByUserView
from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKWikiExportView, \
AKMessageDeleteView
from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \ from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \
NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, NewEventWizardSettingsView NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, NewEventWizardSettingsView
from AKModel.views.room import RoomBatchCreationView from AKModel.views.room import RoomBatchCreationView
...@@ -44,6 +45,11 @@ if apps.is_installed("AKSubmission"): ...@@ -44,6 +45,11 @@ if apps.is_installed("AKSubmission"):
from AKSubmission.api import increment_interest_counter from AKSubmission.api import increment_interest_counter
extra_paths.append(path('api/ak/<pk>/indicate-interest/', increment_interest_counter, name='submission-ak-indicate-interest')) 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 = [ event_specific_paths = [
path('api/', include(api_router.urls), name='api'), path('api/', include(api_router.urls), name='api'),
] ]
...@@ -103,6 +109,8 @@ def get_admin_urls_event(admin_site): ...@@ -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('<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/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('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()), path('<slug:event_slug>/defaultSlots/', admin_site.admin_view(DefaultSlotEditorView.as_view()),
name="default-slots-editor"), name="default-slots-editor"),
path('<slug:event_slug>/importRooms/', admin_site.admin_view(RoomBatchCreationView.as_view()), path('<slug:event_slug>/importRooms/', admin_site.admin_view(RoomBatchCreationView.as_view()),
......
...@@ -8,13 +8,13 @@ from django.contrib import messages ...@@ -8,13 +8,13 @@ from django.contrib import messages
from django.db.models.functions import Now from django.db.models.functions import Now
from django.utils.dateparse import parse_datetime from django.utils.dateparse import parse_datetime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView, DetailView from django.views.generic import DetailView, ListView, TemplateView
from django_tex.core import render_template_with_context, run_tex_in_directory from django_tex.core import render_template_with_context, run_tex_in_directory
from django_tex.response import PDFResponse from django_tex.response import PDFResponse
from AKModel.forms import SlideExportForm, DefaultSlotEditorForm from AKModel.forms import SlideExportForm, DefaultSlotEditorForm
from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin
from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner, AKSlot, AKType
class UserView(TemplateView): class UserView(TemplateView):
...@@ -35,6 +35,19 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView): ...@@ -35,6 +35,19 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
title = _('Export AK Slides') title = _('Export AK Slides')
form_class = SlideExportForm form_class = SlideExportForm
def get_form(self, form_class=None):
# Filter type choices to those of the current event
# or completely hide the field if no types are specified for this event
form = super().get_form(form_class)
if self.event.aktype_set.count() > 0:
form.fields['types'].choices = [
(ak_type.id, ak_type.name) for ak_type in self.event.aktype_set.all()
]
else:
form.fields['types'].widget = form.fields['types'].hidden_widget()
form.fields['types_all_selected_only'].widget = form.fields['types_all_selected_only'].hidden_widget()
return form
def form_valid(self, form): def form_valid(self, form):
# pylint: disable=invalid-name # pylint: disable=invalid-name
template_name = 'admin/AKModel/export/slides.tex' template_name = 'admin/AKModel/export/slides.tex'
...@@ -51,6 +64,7 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView): ...@@ -51,6 +64,7 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
'reso': _("Reso intention?"), 'reso': _("Reso intention?"),
'category': _("Category (for Wishes)"), 'category': _("Category (for Wishes)"),
'wishes': _("Wishes"), 'wishes': _("Wishes"),
'types': _("Types"),
} }
def build_ak_list_with_next_aks(ak_list): def build_ak_list_with_next_aks(ak_list):
...@@ -58,23 +72,36 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView): ...@@ -58,23 +72,36 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
Create a list of tuples cosisting of an AK and a list of upcoming AKs (list length depending on setting) Create a list of tuples cosisting of an AK and a list of upcoming AKs (list length depending on setting)
""" """
next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None) next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None)
return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=[])] return list(zip_longest(ak_list, next_aks_list, fillvalue=[]))
# Create a list of types to filter AKs by (if at least one type was selected)
types = None
types_filter_string = ""
show_types = self.event.aktype_set.count() > 0
if len(form.cleaned_data['types']) > 0:
types = AKType.objects.filter(id__in=form.cleaned_data['types'])
names_string = ', '.join(AKType.objects.get(pk=t).name for t in form.cleaned_data['types'])
types_filter_string = f"[{_('Type(s)')}: {names_string}]"
types_all_selected_only = form.cleaned_data['types_all_selected_only']
# Get all relevant AKs (wishes separately, and either all AKs or only those who should directly or indirectly # Get all relevant AKs (wishes separately, and either all AKs or only those who should directly or indirectly
# be presented when restriction setting was chosen) # be presented when restriction setting was chosen)
categories_with_aks, ak_wishes = self.event.get_categories_with_aks(wishes_seperately=True, filter_func=lambda 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))) ak: not RESULT_PRESENTATION_MODE or (ak.present or (ak.present is None and ak.category.present_by_default)),
types=types,
types_all_selected_only=types_all_selected_only)
# Create context for LaTeX rendering # Create context for LaTeX rendering
context = { context = {
'title': self.event.name, 'title': self.event.name,
'categories_with_aks': [(category, build_ak_list_with_next_aks(ak_list)) for category, ak_list in 'categories_with_aks': [(category, build_ak_list_with_next_aks(ak_list)) for category, ak_list in
categories_with_aks], categories_with_aks],
'subtitle': _("AKs"), 'subtitle': _("AKs") + " " + types_filter_string,
"wishes": build_ak_list_with_next_aks(ak_wishes), "wishes": build_ak_list_with_next_aks(ak_wishes),
"translations": translations, "translations": translations,
"result_presentation_mode": RESULT_PRESENTATION_MODE, "result_presentation_mode": RESULT_PRESENTATION_MODE,
"space_for_notes_in_wishes": SPACE_FOR_NOTES_IN_WISHES, "space_for_notes_in_wishes": SPACE_FOR_NOTES_IN_WISHES,
"show_types": show_types,
} }
source = render_template_with_context(template_name, context) source = render_template_with_context(template_name, context)
...@@ -130,6 +157,28 @@ class CVSetLevelWarningView(IntermediateAdminActionView): ...@@ -130,6 +157,28 @@ class CVSetLevelWarningView(IntermediateAdminActionView):
def action(self, form): def action(self, form):
self.entities.update(level=ConstraintViolation.ViolationLevel.WARNING) self.entities.update(level=ConstraintViolation.ViolationLevel.WARNING)
class ClearScheduleView(IntermediateAdminActionView, ListView):
"""
Admin action view: Clear schedule
"""
title = _('Clear schedule')
model = AKSlot
confirmation_message = _('Clear schedule. The following scheduled AKSlots will be reset')
success_message = _('Schedule cleared')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.entities = AKSlot.objects.none()
def get_queryset(self, *args, **kwargs):
query_set = super().get_queryset(*args, **kwargs)
# do not reset fixed AKs
query_set = query_set.filter(fixed=False)
return query_set
def action(self, form):
"""Reset rooms and start for all selected slots."""
self.entities.update(room=None, start=None)
class PlanPublishView(IntermediateAdminActionView): class PlanPublishView(IntermediateAdminActionView):
""" """
...@@ -157,6 +206,31 @@ class PlanUnpublishView(IntermediateAdminActionView): ...@@ -157,6 +206,31 @@ class PlanUnpublishView(IntermediateAdminActionView):
self.entities.update(plan_published_at=None, plan_hidden=True) 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): class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView):
""" """
Admin view: Allow to edit the default slots of an event Admin view: Allow to edit the default slots of an event
......
...@@ -76,10 +76,15 @@ class EventRoomsWidget(TemplateStatusWidget): ...@@ -76,10 +76,15 @@ class EventRoomsWidget(TemplateStatusWidget):
def render_actions(self, context: {}) -> list[dict]: def render_actions(self, context: {}) -> list[dict]:
actions = super().render_actions(context) actions = super().render_actions(context)
# Action has to be added here since it depends on the event for URL building # 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( actions.append(
{ {
"text": _("Import Rooms from CSV"), "text": _("Import Rooms from CSV"),
"url": reverse_lazy("admin:room-import", kwargs={"event_slug": context["event"].slug}), "url": import_room_url,
} }
) )
return actions return actions
...@@ -147,6 +152,17 @@ class EventAKsWidget(TemplateStatusWidget): ...@@ -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 return actions
......
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-15 20:03+0200\n" "POT-Creation-Date: 2025-06-16 12:44+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -38,45 +38,65 @@ msgstr "Veranstaltung" ...@@ -38,45 +38,65 @@ msgstr "Veranstaltung"
#: AKPlan/templates/AKPlan/plan_index.html:59 #: AKPlan/templates/AKPlan/plan_index.html:59
#: AKPlan/templates/AKPlan/plan_room.html:13 #: AKPlan/templates/AKPlan/plan_room.html:13
#: AKPlan/templates/AKPlan/plan_room.html:59 #: AKPlan/templates/AKPlan/plan_room.html:59
#: AKPlan/templates/AKPlan/plan_wall.html:65 #: AKPlan/templates/AKPlan/plan_wall.html:67
msgid "Room" msgid "Room"
msgstr "Raum" msgstr "Raum"
#: AKPlan/templates/AKPlan/plan_index.html:80 #: AKPlan/templates/AKPlan/plan_index.html:120
#: AKPlan/templates/AKPlan/plan_room.html:11 #: AKPlan/templates/AKPlan/plan_room.html:11
#: AKPlan/templates/AKPlan/plan_track.html:9 #: AKPlan/templates/AKPlan/plan_track.html:9
msgid "AK Plan" msgid "AK Plan"
msgstr "AK-Plan" msgstr "AK-Plan"
#: AKPlan/templates/AKPlan/plan_index.html:92 #: AKPlan/templates/AKPlan/plan_index.html:134
#: AKPlan/templates/AKPlan/plan_room.html:49 #: AKPlan/templates/AKPlan/plan_room.html:49
msgid "Rooms" msgid "Rooms"
msgstr "Räume" msgstr "Räume"
#: AKPlan/templates/AKPlan/plan_index.html:105 #: AKPlan/templates/AKPlan/plan_index.html:147
#: AKPlan/templates/AKPlan/plan_track.html:36 #: AKPlan/templates/AKPlan/plan_track.html:36
msgid "Tracks" msgid "Tracks"
msgstr "Tracks" msgstr "Tracks"
#: AKPlan/templates/AKPlan/plan_index.html:117 #: AKPlan/templates/AKPlan/plan_index.html:159
msgid "AK Wall" msgid "AK Wall"
msgstr "AK-Wall" msgstr "AK-Wall"
#: AKPlan/templates/AKPlan/plan_index.html:130 #: AKPlan/templates/AKPlan/plan_index.html:165
#: AKPlan/templates/AKPlan/plan_wall.html:130 msgid "Plan:"
msgstr "Plan:"
#: AKPlan/templates/AKPlan/plan_index.html:171
msgid "Filter by types"
msgstr "Nach Typen filtern"
#: AKPlan/templates/AKPlan/plan_index.html:174
msgid "Types:"
msgstr "Typen:"
#: AKPlan/templates/AKPlan/plan_index.html:182
msgid "AKs without type"
msgstr "AKs ohne Typ"
#: AKPlan/templates/AKPlan/plan_index.html:186
msgid "No AKs with additional other types"
msgstr "Keine AKs, die noch zusätzlich andere Typen haben"
#: AKPlan/templates/AKPlan/plan_index.html:198
#: AKPlan/templates/AKPlan/plan_wall.html:132
msgid "Current AKs" msgid "Current AKs"
msgstr "Aktuelle AKs" msgstr "Aktuelle AKs"
#: AKPlan/templates/AKPlan/plan_index.html:137 #: AKPlan/templates/AKPlan/plan_index.html:205
#: AKPlan/templates/AKPlan/plan_wall.html:135 #: AKPlan/templates/AKPlan/plan_wall.html:137
msgid "Next AKs" msgid "Next AKs"
msgstr "Nächste AKs" msgstr "Nächste AKs"
#: AKPlan/templates/AKPlan/plan_index.html:145 #: AKPlan/templates/AKPlan/plan_index.html:213
msgid "This event is not active." msgid "This event is not active."
msgstr "Dieses Event ist nicht aktiv." msgstr "Dieses Event ist nicht aktiv."
#: AKPlan/templates/AKPlan/plan_index.html:158 #: AKPlan/templates/AKPlan/plan_index.html:226
#: AKPlan/templates/AKPlan/plan_room.html:77 #: AKPlan/templates/AKPlan/plan_room.html:77
#: AKPlan/templates/AKPlan/plan_track.html:58 #: AKPlan/templates/AKPlan/plan_track.html:58
msgid "Plan is not visible (yet)." msgid "Plan is not visible (yet)."
...@@ -99,10 +119,14 @@ msgstr "Eigenschaften" ...@@ -99,10 +119,14 @@ msgstr "Eigenschaften"
msgid "Track" msgid "Track"
msgstr "Track" msgstr "Track"
#: AKPlan/templates/AKPlan/plan_wall.html:145 #: AKPlan/templates/AKPlan/plan_wall.html:147
msgid "Reload page automatically?" msgid "Reload page automatically?"
msgstr "Seite automatisch neu laden?" msgstr "Seite automatisch neu laden?"
#: AKPlan/templates/AKPlan/slots_table.html:14 #: AKPlan/templates/AKPlan/slots_table.html:14
msgid "No AKs" msgid "No AKs"
msgstr "Keine AKs" msgstr "Keine AKs"
#: AKPlan/views.py:64
msgid "Invalid type filter"
msgstr "Ungültiger Typ-Filter"