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
  • main
  • renovate/django-5.x
  • renovate/jsonschema-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 1089 additions and 38 deletions
...@@ -37,3 +37,5 @@ if apps.is_installed("AKDashboard"): ...@@ -37,3 +37,5 @@ if apps.is_installed("AKDashboard"):
urlpatterns.append(path('', include('AKDashboard.urls', namespace='dashboard'))) urlpatterns.append(path('', include('AKDashboard.urls', namespace='dashboard')))
if apps.is_installed("AKPlan"): if apps.is_installed("AKPlan"):
urlpatterns.append(path('', include('AKPlan.urls', namespace='plan'))) urlpatterns.append(path('', include('AKPlan.urls', namespace='plan')))
if apps.is_installed("AKPreference"):
urlpatterns.append(path('', include('AKPreference.urls', namespace='poll')))
from django import forms
from django.contrib import admin
from AKPreference.models import AKPreference, EventParticipant
from AKModel.admin import PrepopulateWithNextActiveEventMixin, EventRelatedFieldListFilter
from AKModel.models import AK
@admin.register(EventParticipant)
class EventParticipantAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmin):
"""
Admin interface for EventParticipant
"""
model = EventParticipant
list_display = ['name', 'institution', 'event']
list_filter = ['event', 'institution']
list_editable = []
ordering = ['name']
class AKPreferenceAdminForm(forms.ModelForm):
"""
Adapted admin form for AK preferences for usage in :class:`AKPreferenceAdmin`)
"""
class Meta:
widgets = {
'participant': forms.Select,
'ak': forms.Select,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter possible values for foreign keys & m2m when event is specified
if hasattr(self.instance, "event") and self.instance.event is not None:
self.fields["participant"].queryset = EventParticipant.objects.filter(event=self.instance.event)
self.fields["ak"].queryset = AK.objects.filter(event=self.instance.event)
@admin.register(AKPreference)
class AKPreferenceAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmin):
"""
Admin interface for AK preferences.
Uses an adapted form (see :class:`AKPreferenceAdminForm`)
"""
model = AKPreference
form = AKPreferenceAdminForm
list_display = ['preference', 'participant', 'ak', 'event']
list_filter = ['event', ('ak', EventRelatedFieldListFilter), ('participant', EventRelatedFieldListFilter)]
list_editable = []
ordering = ['participant', 'preference', 'ak']
from django.apps import AppConfig
class AkpreferenceConfig(AppConfig):
"""
App configuration (default, only specifies name of the app)
"""
name = "AKPreference"
from django import forms
from django.utils.translation import gettext_lazy as _
from AKModel.availability.forms import AvailabilitiesFormMixin
from AKModel.availability.models import Availability
from AKModel.models import AKRequirement
from AKPreference.models import EventParticipant
class EventParticipantForm(AvailabilitiesFormMixin, forms.ModelForm):
"""Form to add EventParticipants"""
required_css_class = "required"
class Meta:
model = EventParticipant
fields = [
"name",
"institution",
"requirements",
"event",
]
widgets = {
"requirements": forms.CheckboxSelectMultiple,
"event": forms.HiddenInput,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.initial = {**self.initial, **kwargs["initial"]}
self.fields["requirements"].queryset = AKRequirement.objects.filter(
event=self.initial.get("event"), relevant_for_participants=True
)
# Don't ask for requirements if there are no requirements configured for this event
if self.fields["requirements"].queryset.count() == 0:
self.fields.pop("requirements")
def clean_availabilities(self):
"""
Automatically improve availabilities entered.
If the user did not specify availabilities assume the full event duration is possible
:return: cleaned availabilities
(either user input or one availability for the full length of the event if user input was empty)
"""
availabilities = super().clean_availabilities()
if len(availabilities) == 0:
availabilities.append(
Availability.with_event_length(event=self.cleaned_data["event"])
)
return availabilities
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-21 16:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: AKPreference/models.py:11 AKPreference/models.py:93
msgid "Participant"
msgstr "Teilnehmende*r"
#: AKPreference/models.py:12
msgid "Participants"
msgstr "Teilnehmende"
#: AKPreference/models.py:15
msgid "Nickname"
msgstr "Spitzname"
#: AKPreference/models.py:16
msgid ""
"Name to identify a participant by (in case of questions from the organizers)"
msgstr "Name, zur Identifikation bei Rückfragen von den Organisator*innen"
#: AKPreference/models.py:17
msgid "Institution"
msgstr "Institution"
#: AKPreference/models.py:17
msgid "Uni etc."
msgstr "Uni etc."
#: AKPreference/models.py:19 AKPreference/models.py:90
msgid "Event"
msgstr "Event"
#: AKPreference/models.py:20 AKPreference/models.py:91
msgid "Associated event"
msgstr "Zugehöriges Event"
#: AKPreference/models.py:22
msgid "Requirements"
msgstr "Anforderungen"
#: AKPreference/models.py:23
msgid "Participant's Requirements"
msgstr "Anforderungen des*der Teilnehmer*in"
#: AKPreference/models.py:26
#, python-brace-format
msgid "Anonymous {pk}"
msgstr "Anonym {pk}"
#: AKPreference/models.py:85
msgid "AK Preference"
msgstr "AK Präferenz"
#: AKPreference/models.py:86
msgid "AK Preferences"
msgstr "AK-Präferenzen"
#: AKPreference/models.py:94
msgid "Participant this preference belongs to"
msgstr "Teilnehmer*in, zu dem*der die Präferenz gehört"
#: AKPreference/models.py:96
msgid "AK"
msgstr "AK"
#: AKPreference/models.py:97
msgid "AK this preference belongs to"
msgstr "AK zu dem die Präferenz gehört"
#: AKPreference/models.py:103
msgid "Ignore"
msgstr "Ignorieren"
#: AKPreference/models.py:104
msgid "Interested"
msgstr "Interessiert"
#: AKPreference/models.py:105
msgid "Great interest"
msgstr "Großes Interesse"
#: AKPreference/models.py:106
msgid "Required"
msgstr "Erforderlich"
#: AKPreference/models.py:108
msgid "Preference"
msgstr "Präferenz"
#: AKPreference/models.py:109
msgid "Preference level for the AK"
msgstr "Präferenz-Level für diesen AK"
#: AKPreference/models.py:113
msgid "Timestamp"
msgstr "Zeitpunkt"
#: AKPreference/models.py:113
msgid "Time of creation"
msgstr "Erstellungszeitpunkt"
#: AKPreference/templates/AKPreference/poll.html:11
#: AKPreference/templates/AKPreference/poll_not_configured.html:7
msgid "AKs"
msgstr "AKs"
#: AKPreference/templates/AKPreference/poll.html:11
msgid "Preferences"
msgstr "Präferenzen"
#: AKPreference/templates/AKPreference/poll.html:34
#: AKPreference/templates/AKPreference/poll.html:41
#: AKPreference/templates/AKPreference/poll_not_configured.html:7
#: AKPreference/templates/AKPreference/poll_not_configured.html:11
msgid "AK Preference Poll"
msgstr "Abfrage von AK-Präferenzen"
#: AKPreference/templates/AKPreference/poll.html:47
msgid "Your AK preferences"
msgstr "Deine AK Präferenzen"
#: AKPreference/templates/AKPreference/poll.html:49
msgid "Please enter your preferences."
msgstr "Trage bitte deine Präferenzen zu den AKs ein."
#: AKPreference/templates/AKPreference/poll.html:70
#: AKPreference/templates/AKPreference/poll.html:86
msgid "Intends to submit a resolution"
msgstr "Beabsichtigt eine Resolution einzureichen"
#: AKPreference/templates/AKPreference/poll.html:75
msgid "Responsible"
msgstr "Verantwortlich"
#: AKPreference/templates/AKPreference/poll.html:100
msgid "Careful: after saving your preferences, you cannot edit them again!"
msgstr "Achtung: Nach dem Abschicken kannst du deine Präferenzen nicht noch einmal editieren!"
#: AKPreference/templates/AKPreference/poll.html:103
msgid "Submit"
msgstr "Eintragen"
#: AKPreference/templates/AKPreference/poll.html:107
msgid "Reset Form"
msgstr "Formular leeren"
#: AKPreference/templates/AKPreference/poll.html:111
msgid "Cancel"
msgstr "Abbrechen"
#: AKPreference/templates/AKPreference/poll_base.html:13
msgid "Write to organizers of this event for questions and comments"
msgstr "Fragen oder Kommentare? Schreib den Orgas dieses Events eine Mail"
#: AKPreference/templates/AKPreference/poll_not_configured.html:20
msgid ""
"System is not yet configured for AK preference polling. Please try again "
"later."
msgstr ""
"Das System ist bisher nicht für die Erfassung von AK-Präferenzen "
"konfiguriert. Bitte versuche es später wieder."
#: AKPreference/views.py:37
msgid "AK preferences were registered successfully"
msgstr "AK-Präferenzen erfolgreich registriert"
#: AKPreference/views.py:139
msgid ""
"Something went wrong. Your preferences were not saved. Please try again or "
"contact the organizers."
msgstr ""
"Etwas ging schief. Deine Präferenzen konnten nicht gespeichert werden. "
"Versuche es bitte erneut oder kontaktiere die Orgas."
# Generated by Django 5.2.1 on 2025-06-14 18:57
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("AKModel", "0067_eventparticipant_requirements_and_more"),
]
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",
),
),
(
"requirements",
models.ManyToManyField(
blank=True,
help_text="Participant's Requirements",
to="AKModel.akrequirement",
verbose_name="Requirements",
),
),
],
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, "Interested"),
(2, "Great interest"),
(3, "Required"),
],
default=0,
help_text="Preference level for the AK",
verbose_name="Preference",
),
),
(
"timestamp",
models.DateTimeField(
auto_now_add=True,
help_text="Time of creation",
verbose_name="Timestamp",
),
),
(
"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="AKPreference.eventparticipant",
verbose_name="Participant",
),
),
],
options={
"verbose_name": "AK Preference",
"verbose_name_plural": "AK Preferences",
"ordering": ["-timestamp"],
"unique_together": {("event", "participant", "ak")},
},
),
]
from django.db import models
from django.utils.translation import gettext_lazy as _
from AKModel.models import AK, AKRequirement, Event
class EventParticipant(models.Model):
""" A participant describes a person taking part in an event."""
class Meta:
verbose_name = _('Participant')
verbose_name_plural = _('Participants')
ordering = ['name']
name = models.CharField(max_length=64, blank=True, verbose_name=_('Nickname'),
help_text=_('Name to identify a participant by (in case of questions from the organizers)'))
institution = models.CharField(max_length=128, blank=True, verbose_name=_('Institution'), help_text=_('Uni etc.'))
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
requirements = models.ManyToManyField(to=AKRequirement, blank=True, verbose_name=_('Requirements'),
help_text=_("Participant's Requirements"))
def __str__(self) -> str:
string = _("Anonymous {pk}").format(pk=self.pk) if not self.name else self.name
if self.institution:
string += f" ({self.institution})"
return string
@property
def availabilities(self):
"""
Get all availabilities associated to this EventParticipant
:return: availabilities
:rtype: QuerySet[Availability]
"""
return "Availability".objects.filter(participant=self)
def get_time_constraints(self) -> list[str]:
"""Construct list of required time constraint labels."""
# local import to prevent cyclic import
# pylint: disable=import-outside-toplevel
from AKModel.availability.models import Availability
avails = self.availabilities.all()
participant_required_prefs = AKPreference.objects.filter(
event=self.event,
participant=self,
preference=AKPreference.PreferenceLevel.REQUIRED,
)
if (
avails
and not Availability.is_event_covered(self.event, avails)
and participant_required_prefs.exists()
):
# participant has restricted availability and is actually required for AKs
return [f"availability-participant-{self.pk}"]
return []
def get_room_constraints(self) -> list[str]:
"""Construct list of required room constraint labels."""
return list(self.requirements.values_list("name", flat=True).order_by())
@property
def export_preferences(self):
"""Preferences of this participant with positive score."""
return (
AKPreference.objects
.filter(
participant=self, preference__gt=0
)
.order_by()
.select_related('ak')
.prefetch_related('ak__akslot_set')
)
class AKPreference(models.Model):
"""Model representing the preference of a participant to an AK."""
class Meta:
verbose_name = _('AK Preference')
verbose_name_plural = _('AK Preferences')
unique_together = [['event', 'participant', 'ak']]
ordering = ["-timestamp"]
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
participant = models.ForeignKey(to=EventParticipant, on_delete=models.CASCADE, verbose_name=_('Participant'),
help_text=_('Participant this preference belongs to'))
ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'),
help_text=_('AK this preference belongs to'))
class PreferenceLevel(models.IntegerChoices):
"""
Possible preference values
"""
IGNORE = 0, _('Ignore')
PREFER = 1, _('Interested')
STRONG_PREFER = 2, _("Great interest")
REQUIRED = 3, _("Required")
preference = models.PositiveSmallIntegerField(verbose_name=_('Preference'), choices=PreferenceLevel.choices,
help_text=_('Preference level for the AK'),
blank=False,
default=PreferenceLevel.IGNORE)
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_('Timestamp'), help_text=_('Time of creation'))
def __str__(self) -> str:
return f"Preference {self.get_preference_display()} [of '{self.participant}' for AK '{self.ak}']"
@property
def required(self) -> bool:
"""Whether this preference is a 'REQUIRED'"""
return self.preference == self.PreferenceLevel.REQUIRED
@property
def preference_score(self) -> int:
"""Score of this preference for the solver"""
return self.preference if self.preference != self.PreferenceLevel.REQUIRED else -1
from rest_framework import serializers
from AKModel.models import AKSlot
from AKModel.serializers import StringListField
from AKPreference.models import AKPreference, EventParticipant
class ExportAKPreferenceSerializer(serializers.ModelSerializer):
"""Export serializer for AKPreference objects.
Used to serialize AKPreference objects for the export to a solver.
Part of the implementation of the format of the KoMa solver:
https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
"""
class Meta:
model = AKPreference
fields = ["required", "preference_score"]
read_only_fields = ["required", "preference_score"]
class ExportAKPreferencePerSlotSerializer(serializers.BaseSerializer):
"""Export serializer to associate AKPreferences with the AK's AKSlot objects.
The AKPreference model stores the preference per AK object.
The solver however needs the serialization to be *per AKSlot*, i.e.
we need to 'repeat' all preferences for each slot of an AK.
Part of the implementation of the format of the KoMa solver:
https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
"""
def create(self, validated_data):
raise ValueError("`ExportAKPreferencePerSlotSerializer` is read-only.")
def to_internal_value(self, data):
raise ValueError("`ExportAKPreferencePerSlotSerializer` is read-only.")
def update(self, instance, validated_data):
raise ValueError("`ExportAKPreferencePerSlotSerializer` is read-only.")
def to_representation(self, instance):
preference_queryset = instance
def _insert_akslot_id(serialization_dict: dict, slot: AKSlot) -> dict:
"""Insert id of the slot into the dict and return it."""
# The naming scheme of the solver is confusing.
# Our 'AKSlot' corresponds to the 'AK' of the solver,
# so `ak_id` is the id of the corresponding AKSlot.
serialization_dict["ak_id"] = slot.pk
return serialization_dict
return_lst = []
for pref in preference_queryset:
pref_serialization = ExportAKPreferenceSerializer(pref).data
return_lst.extend([
_insert_akslot_id(pref_serialization, slot)
for slot in pref.ak.akslot_set.all()
])
return return_lst
class ExportParticipantInfoSerializer(serializers.ModelSerializer):
"""Serializer of EventParticipant objects for the 'info' field.
Used in `ExportParticipantSerializer` to serialize EventParticipant objects
for the export to a solver. Part of the implementation of the
format of the KoMa solver:
https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
"""
name = serializers.CharField(source="__str__")
class Meta:
model = EventParticipant
fields = ["name"]
read_only_fields = ["name"]
class ExportParticipantSerializer(serializers.ModelSerializer):
"""Export serializer for EventParticipant objects.
Used to serialize EventParticipant objects for the export to a solver.
Part of the implementation of the format of the KoMa solver:
https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
"""
room_constraints = StringListField(source="get_room_constraints")
time_constraints = StringListField(source="get_time_constraints")
preferences = ExportAKPreferencePerSlotSerializer(source="export_preferences")
info = ExportParticipantInfoSerializer(source="*")
class Meta:
model = EventParticipant
fields = ["id", "info", "room_constraints", "time_constraints", "preferences"]
read_only_fields = ["id", "info", "room_constraints", "time_constraints", "preferences"]
{% extends 'AKPreference/poll_base.html' %}
{% load i18n %}
{% load django_bootstrap5 %}
{% load fontawesome_6 %}
{% load static %}
{% load tz %}
{% load static %}
{% block title %}{% trans "AKs" %}: {{ event.name }} - {% trans "Preferences" %}{% endblock %}
{% block imports %}
{% include "AKModel/load_fullcalendar_availabilities.html" %}
<link rel="stylesheet" type='text/css' href="{% static 'common/css/poll.css' %}">
<script>
{% get_current_language as LANGUAGE_CODE %}
document.addEventListener('DOMContentLoaded', function () {
createAvailabilityEditors(
'{{ event.timezone }}',
'{{ LANGUAGE_CODE }}',
'{{ event.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
'{{ event.end | timezone:event.timezone | date:"Y-m-d H:i:s" }}'
);
});
</script>
{% endblock %}
{% block breadcrumbs %}
{% include "AKPreference/poll_breadcrumbs.html" %}
<li class="breadcrumb-item active">{% trans "AK Preference Poll" %}</li>
{% endblock %}
{% block content %}
{% include "messages.html" %}
{% block headline %}
<h2>{% trans 'AK Preference Poll' %}</h2>
{% endblock %}
<form method="POST" class="post-form">{% csrf_token %}
{% block form_contents %}
{% bootstrap_form participant_form %}
<section>
<h3>{% trans "Your AK preferences" %}</h3>
<p>{% trans "Please enter your preferences." %}</p>
{{ formset.management_form }}
<div class="container radio-flex">
{% for category, forms in grouped_forms %}
<details open>
<summary
class="ak-category-header">{{ category.name }}</summary>
<div class="row row-cols-1 row-cols-lg-2">
{% for innerform in forms %}
{% bootstrap_form_errors innerform type='non_fields' %}
{% for field in innerform.hidden_fields %}
{{ field }}
{% endfor %}
<div class="container radio-group col">
<div class="container radio-info">
<details>
<summary class="ak-header">{{ innerform.preference.label }}
{% if innerform.ak_obj.reso %}
<span class="badge bg-dark rounded-pill"
title="{% trans 'Intends to submit a resolution' %}">{% fa6_icon "scroll" 'fas' %}</span>
{% endif %}
</summary>
<p>{{ innerform.ak_obj.description }}</p>
{% if innerform.ak_obj.owners_list %}
<p>{% trans "Responsible" %}: {{ innerform.ak_obj.owners_list }}</p>
{% endif %}
{% if show_types %}
<p>
{% for aktype in innerform.ak_obj.types.all %}
<span class="badge bg-light">{{ aktype.name }}</span>&nbsp;
{% endfor %}
</p>
{% endif %}
{% if innerform.ak_obj.reso %}
<!-- TODO: reso as an icon -->
<p><i>{% trans "Intends to submit a resolution" %}.</i></p>
{% endif %}
</details>
</div>
{% bootstrap_field innerform.preference show_label=False show_help=False field_class="pref-cls" %}
</div>
{% endfor %}
</div>
</details>
{% endfor %}
</div>
</section>
{% endblock %}
<h2 class="text-warning mb-4 text-end">{% trans "Careful: after saving your preferences, you cannot edit them again!" %}</h2>
<button type="submit" class="save btn btn-primary float-end">
{% fa6_icon "check" 'fas' %} {% trans "Submit" %}
</button>
<button type="reset" class="btn btn-danger">
{% fa6_icon "undo-alt" 'fas' %} {% trans "Reset Form" %}
</button>
<a href="{% url 'dashboard:dashboard_event' slug=event.slug %}" class="btn btn-secondary">
{% fa6_icon "times" 'fas' %} {% trans "Cancel" %}
</a>
</form>
{% endblock %}
{% block bottom_script %}
<script src="{% static 'common/vendor/chosen-js/chosen.jquery.js' %}"></script>
<script>
$(function () {
$('.chosen-select').chosen();
});
</script>
{% endblock %}
{% extends "base.html" %}
{% load fontawesome_6 %}
{% load i18n %}
{% block breadcrumbs %}
{% include "AKPreference/poll_breadcrumbs.html" %}
{% endblock %}
{% block footer_custom %}
{% if event.contact_email %}
<h4>
<a href="mailto:{{ event.contact_email }}">{% fa6_icon "envelope" 'fas' %} {% trans "Write to organizers of this event for questions and comments" %}</a>
</h4>
{% endif %}
{% endblock %}
{% load tags_AKModel %}
<li class="breadcrumb-item">
{% if 'AKDashboard'|check_app_installed %}
<a href="{% url 'dashboard:dashboard' %}">AKPlanning</a>
{% else %}
AKPlanning
{% endif %}
</li>
<li class="breadcrumb-item">
{% if 'AKDashboard'|check_app_installed %}
<a href="{% url 'dashboard:dashboard_event' slug=event.slug %}">{{ event }}</a>
{% else %}
{{ event }}
{% endif %}
</li>
{% extends 'AKPreference/poll_base.html' %}
{% load i18n %}
{% load fontawesome_6 %}
{% load static %}
{% block title %}{% trans "AKs" %}: {{ event.name }} - {% trans "AK Preference Poll" %}{% endblock %}
{% block breadcrumbs %}
{% include "AKPreference/poll_breadcrumbs.html" %}
<li class="breadcrumb-item active">{% trans "AK Preference Poll" %}</li>
{% endblock %}
{% block content %}
<h1>{{ event.name }}</h1>
{% include "messages.html" %}
<div class="alert alert-warning" style="margin-top:20px;margin-bottom: 20px;">
{% trans "System is not yet configured for AK preference polling. Please try again later." %}
</div>
{% endblock %}
from django.urls import reverse
from django.test import TestCase
from AKModel.models import Event
from AKModel.tests.test_views import BasicViewTests
class PollViewTests(BasicViewTests, TestCase):
"""
Tests for AKPreference Poll
"""
fixtures = ['model.json']
APP_NAME = 'poll'
def test_poll_redirect(self):
"""
Test: Make sure that user is redirected from poll to dashboard when poll is hidden
"""
event = Event.objects.get(slug='kif42')
_, url_poll = self._name_and_url(('poll', {'event_slug': event.slug}))
url_dashboard = reverse("dashboard:dashboard_event", kwargs={"slug": event.slug})
event.poll_hidden = True
event.save()
self.client.logout()
response = self.client.get(url_poll)
self.assertRedirects(response, url_dashboard,
msg_prefix=f"Redirect away from poll not working ({url_poll} -> {url_dashboard})")
self.client.force_login(self.staff_user)
response = self.client.get(url_poll)
self.assertEqual(
response.status_code,
200,
msg=f"{url_poll} broken",
)
self.assertTemplateUsed(response, "AKPreference/poll.html", msg_prefix="Poll is not visible for staff user")
from django.urls import include, path
from . import views
app_name = "poll"
urlpatterns = [
path(
"<slug:event_slug>/poll/",
include(
[
path("", views.PreferencePollCreateView.as_view(), name="poll"),
]
),
),
]
import json
from itertools import groupby
from django import forms
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView
from AKModel.availability.models import Availability
from AKModel.availability.serializers import AvailabilityFormSerializer
from AKModel.metaviews.admin import EventSlugMixin
from AKModel.models import AK
from AKPreference.models import AKPreference
from .forms import EventParticipantForm
class PreferencePollCreateView(EventSlugMixin, SuccessMessageMixin, FormView):
"""
View: Show a form to register the AK preference of a participant.
The form creates the `EventParticipant` instance as well as the
AKPreferences for each AK of the event.
For the creation of the event participant, a `EventParticipantForm` is used.
For the preferences, a ModelFormset is created.
"""
form_class = forms.Form
model = AKPreference
form_class = forms.modelform_factory(
model=AKPreference, fields=["preference", "ak", "event"]
)
template_name = "AKPreference/poll.html"
success_message = _("AK preferences were registered successfully")
def _create_modelformset(self):
return forms.modelformset_factory(
model=AKPreference,
fields=["preference", "ak", "event"],
widgets={
"ak": forms.HiddenInput,
"event": forms.HiddenInput,
"preference": forms.RadioSelect,
},
extra=0,
)
def get(self, request, *args, **kwargs):
s = super().get(request, *args, **kwargs)
# Don't show preference form when event is not active or poll is hidden -> redirect to dashboard
if not self.event.active or (self.event.poll_hidden and not request.user.is_staff):
return redirect(self.get_success_url())
return s
def get_success_url(self):
return reverse_lazy(
"dashboard:dashboard_event", kwargs={"slug": self.event.slug}
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
ak_set = (
AK.objects.filter(event=self.event)
.order_by()
.all()
.prefetch_related('owners')
)
initial_lst = [
{"ak": ak, "event": self.event} for ak in ak_set
]
context["formset"] = self._create_modelformset()(
queryset=AKPreference.objects.none(),
initial=initial_lst,
)
context["formset"].extra = len(initial_lst)
for form, init in zip(context["formset"], initial_lst, strict=True):
form.fields["preference"].label = init["ak"].name
form.fields["preference"].help_text = (
"Description: " + init["ak"].description
)
form.ak_obj = init["ak"]
sorted_forms = sorted(
context["formset"],
key=lambda f: (f.ak_obj.category.name, f.ak_obj.id)
)
grouped_forms = [
(category, list(forms))
for category, forms in groupby(sorted_forms, key=lambda f: f.ak_obj.category)
]
context["grouped_forms"] = grouped_forms
availabilities_serialization = AvailabilityFormSerializer(
(
[Availability.with_event_length(event=self.event)],
self.event,
)
)
context["participant_form"] = EventParticipantForm(
initial={
"event": self.event,
"availabilities": json.dumps(availabilities_serialization.data),
}
)
context['show_types'] = self.event.aktype_set.count() > 0
return context
def post(self, request, *args, **kwargs):
self._load_event()
model_formset_cls = self._create_modelformset()
formset = model_formset_cls(request.POST)
participant_form = EventParticipantForm(
data=request.POST, initial={"event": self.event}
)
if formset.is_valid() and participant_form.is_valid():
return self.form_valid(form=(formset, participant_form))
return self.form_invalid(form=(formset, participant_form))
def form_valid(self, form):
try:
formset, participant_form = form
participant = participant_form.save()
instances = formset.save(commit=False)
for instance in instances:
instance.participant = participant
instance.save()
success_message = self.get_success_message(participant_form.cleaned_data)
if success_message:
messages.success(self.request, success_message)
except:
messages.error(
self.request,
_(
"Something went wrong. Your preferences were not saved. "
"Please try again or contact the organizers."
),
)
return self.form_invalid(form=form)
return redirect(self.get_success_url())
...@@ -55,7 +55,9 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView): ...@@ -55,7 +55,9 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
model = AKSlot model = AKSlot
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_related('ak').filter(event=self.event, room__isnull=False) return super().get_queryset().select_related('ak').filter(
event=self.event, room__isnull=False, start__isnull=False
)
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
return JsonResponse( return JsonResponse(
......
...@@ -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: 2024-04-25 00:24+0200\n" "POT-Creation-Date: 2025-06-18 12:09+0000\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"
...@@ -27,14 +27,14 @@ msgstr "Ende" ...@@ -27,14 +27,14 @@ msgstr "Ende"
#: AKScheduling/forms.py:26 #: AKScheduling/forms.py:26
msgid "Duration" msgid "Duration"
msgstr "" msgstr "Dauer"
#: AKScheduling/forms.py:27 #: AKScheduling/forms.py:27 AKScheduling/forms.py:28
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:171 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:172
msgid "Room" msgid "Room"
msgstr "Raum" msgstr "Raum"
#: AKScheduling/forms.py:31 #: AKScheduling/forms.py:32
msgid "AK" msgid "AK"
msgstr "AK" msgstr "AK"
...@@ -62,13 +62,13 @@ msgstr "" ...@@ -62,13 +62,13 @@ msgstr ""
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:44 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:44
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:105 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:105
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:240 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:241
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:375 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:378
msgid "No violations" msgid "No violations"
msgstr "Keine Verletzungen" msgstr "Keine Verletzungen"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:82 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:82
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:346 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:347
msgid "Violation(s)" msgid "Violation(s)"
msgstr "Verletzung(en)" msgstr "Verletzung(en)"
...@@ -81,12 +81,12 @@ msgid "Reload now" ...@@ -81,12 +81,12 @@ msgid "Reload now"
msgstr "Jetzt neu laden" msgstr "Jetzt neu laden"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:95 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:95
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:228 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:229
msgid "Violation" msgid "Violation"
msgstr "Verletzung" msgstr "Verletzung"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:96 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:96
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:369 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:372
msgid "Problem" msgid "Problem"
msgstr "Problem" msgstr "Problem"
...@@ -100,8 +100,8 @@ msgstr "Seit" ...@@ -100,8 +100,8 @@ msgstr "Seit"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:111 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:111
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:256 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:256
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:332 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:333
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:48 #: AKScheduling/templates/admin/AKScheduling/special_attention.html:58
#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34 #: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34
msgid "Event Status" msgid "Event Status"
msgstr "Event-Status" msgstr "Event-Status"
...@@ -116,7 +116,7 @@ msgstr "Abschicken" ...@@ -116,7 +116,7 @@ msgstr "Abschicken"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:11 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:11
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:21 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:21
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:329 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:330
msgid "Scheduling for" msgid "Scheduling for"
msgstr "Scheduling für" msgstr "Scheduling für"
...@@ -168,31 +168,31 @@ msgstr "Event (horizontal)" ...@@ -168,31 +168,31 @@ msgstr "Event (horizontal)"
msgid "Event (Vertical)" msgid "Event (Vertical)"
msgstr "Event (vertikal)" msgstr "Event (vertikal)"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:271 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:272
msgid "Please choose AK" msgid "Please choose AK"
msgstr "Bitte AK auswählen" msgstr "Bitte AK auswählen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:291 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:292
msgid "Could not create slot" msgid "Could not create slot"
msgstr "Konnte Slot nicht anlegen" msgstr "Konnte Slot nicht anlegen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:307 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:308
msgid "Add slot" msgid "Add slot"
msgstr "Slot hinzufügen" msgstr "Slot hinzufügen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:315 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:316
msgid "Add" msgid "Add"
msgstr "Hinzufügen" msgstr "Hinzufügen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:316 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:317
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:343 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:344
msgid "Unscheduled" msgid "Unscheduled"
msgstr "Nicht gescheduled" msgstr "Nicht gescheduled"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:368 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:371
msgid "Level" msgid "Level"
msgstr "Level" msgstr "Level"
...@@ -200,23 +200,23 @@ msgstr "Level" ...@@ -200,23 +200,23 @@ msgstr "Level"
msgid "AKs with public notes" msgid "AKs with public notes"
msgstr "AKs mit öffentlichen Kommentaren" msgstr "AKs mit öffentlichen Kommentaren"
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:21 #: AKScheduling/templates/admin/AKScheduling/special_attention.html:24
msgid "AKs without availabilities" msgid "AKs without availabilities"
msgstr "AKs ohne Verfügbarkeiten" msgstr "AKs ohne Verfügbarkeiten"
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:28 #: AKScheduling/templates/admin/AKScheduling/special_attention.html:33
msgid "Create default availabilities" msgid "Create default availabilities"
msgstr "Standardverfügbarkeiten anlegen" msgstr "Standardverfügbarkeiten anlegen"
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:31 #: AKScheduling/templates/admin/AKScheduling/special_attention.html:36
msgid "AK wishes with slots" msgid "AK wishes with slots"
msgstr "AK-Wünsche mit Slots" msgstr "AK-Wünsche mit Slots"
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:38 #: AKScheduling/templates/admin/AKScheduling/special_attention.html:46
msgid "Delete slots for wishes" msgid "Delete slots for wishes"
msgstr "" msgstr ""
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:40 #: AKScheduling/templates/admin/AKScheduling/special_attention.html:48
msgid "AKs without slots" msgid "AKs without slots"
msgstr "AKs ohne Slots" msgstr "AKs ohne Slots"
...@@ -246,19 +246,19 @@ msgstr "Noch nicht geschedulte AK-Slots" ...@@ -246,19 +246,19 @@ msgstr "Noch nicht geschedulte AK-Slots"
msgid "Count" msgid "Count"
msgstr "Anzahl" msgstr "Anzahl"
#: AKScheduling/views.py:150 #: AKScheduling/views.py:152
msgid "Interest updated" msgid "Interest updated"
msgstr "Interesse aktualisiert" msgstr "Interesse aktualisiert"
#: AKScheduling/views.py:201 #: AKScheduling/views.py:210
msgid "Wishes" msgid "Wishes"
msgstr "Wünsche" msgstr "Wünsche"
#: AKScheduling/views.py:219 #: AKScheduling/views.py:228
msgid "Cleanup: Delete unscheduled slots for wishes" msgid "Cleanup: Delete unscheduled slots for wishes"
msgstr "Aufräumen: Noch nicht geplante Slots für Wünsche löschen" msgstr "Aufräumen: Noch nicht geplante Slots für Wünsche löschen"
#: AKScheduling/views.py:226 #: AKScheduling/views.py:235
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"The following {count} unscheduled slots of wishes will be deleted:\n" "The following {count} unscheduled slots of wishes will be deleted:\n"
...@@ -270,15 +270,15 @@ msgstr "" ...@@ -270,15 +270,15 @@ msgstr ""
"\n" "\n"
" {slots}" " {slots}"
#: AKScheduling/views.py:233 #: AKScheduling/views.py:242
msgid "Unscheduled slots for wishes successfully deleted" msgid "Unscheduled slots for wishes successfully deleted"
msgstr "Noch nicht geplante Slots für Wünsche erfolgreich gelöscht" msgstr "Noch nicht geplante Slots für Wünsche erfolgreich gelöscht"
#: AKScheduling/views.py:247 #: AKScheduling/views.py:256
msgid "Create default availabilities for AKs" msgid "Create default availabilities for AKs"
msgstr "Standardverfügbarkeiten für AKs anlegen" msgstr "Standardverfügbarkeiten für AKs anlegen"
#: AKScheduling/views.py:254 #: AKScheduling/views.py:263
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"The following {count} AKs don't have any availability information. Create " "The following {count} AKs don't have any availability information. Create "
...@@ -291,20 +291,29 @@ msgstr "" ...@@ -291,20 +291,29 @@ msgstr ""
"\n" "\n"
" {aks}" " {aks}"
#: AKScheduling/views.py:274 #: AKScheduling/views.py:283
#, python-brace-format #, python-brace-format
msgid "Could not create default availabilities for AK: {ak}" msgid "Could not create default availabilities for AK: {ak}"
msgstr "Konnte keine Verfügbarkeit anlegen für AK: {ak}" msgstr "Konnte keine Verfügbarkeit anlegen für AK: {ak}"
#: AKScheduling/views.py:279 #: AKScheduling/views.py:288
#, python-brace-format #, python-brace-format
msgid "Created default availabilities for {count} AKs" msgid "Created default availabilities for {count} AKs"
msgstr "Standardverfügbarkeiten für {count} AKs angelegt" msgstr "Standardverfügbarkeiten für {count} AKs angelegt"
#: AKScheduling/views.py:290 #: AKScheduling/views.py:299
msgid "Constraint Violations" msgid "Constraint Violations"
msgstr "Constraintverletzungen" msgstr "Constraintverletzungen"
#~ msgid "Constraint violations for"
#~ msgstr "Constraintverletzungen für"
#~ msgid "AKs requiring special attention for"
#~ msgstr "AKs die besondere Aufmerksamkeit erfordern für"
#~ msgid "Enter interest"
#~ msgstr "Interesse eingeben"
#~ msgid "Bitte AK auswählen" #~ msgid "Bitte AK auswählen"
#~ msgstr "Please sel" #~ msgstr "Please sel"
......
...@@ -288,6 +288,8 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs) ...@@ -288,6 +288,8 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs)
for slot in slots_of_this_ak: for slot in slots_of_this_ak:
room = slot.room room = slot.room
if room is None:
continue
room_requirements = room.properties.all() room_requirements = room.properties.all()
for requirement in instance.requirements.all(): for requirement in instance.requirements.all():
...@@ -363,8 +365,8 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): ...@@ -363,8 +365,8 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
new_violations = [] new_violations = []
# For all slots in this room... # For all slots in this room...
if instance.room: if instance.room and instance.start:
for other_slot in instance.room.akslot_set.all(): for other_slot in instance.room.akslot_set.filter(start__isnull=False):
if other_slot != instance: if other_slot != instance:
# ... find overlapping slots... # ... find overlapping slots...
if instance.overlaps(other_slot): if instance.overlaps(other_slot):
......