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
  • ak-import
  • feature/clear-schedule-button
  • feature/json-export-via-rest-framework
  • feature/json-schedule-import-tests
  • feature/preference-polling
  • feature/preference-polling-form
  • feature/preference-polling-form-rebased
  • feature/preference-polling-rebased
  • fix/add-room-import-only-once
  • main
  • merge-to-upstream
  • renovate/django-5.x
  • renovate/django-debug-toolbar-4.x
  • renovate/django-simple-history-3.x
  • renovate/mysqlclient-2.x
15 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
  • komasolver
  • main
  • renovate/django-5.x
  • renovate/django-debug-toolbar-5.x
  • renovate/django_csp-4.x
  • renovate/djangorestframework-3.x
  • renovate/sphinxcontrib-apidoc-0.x
  • renovate/tzdata-2025.x
  • renovate/uwsgi-2.x
9 results
Show changes
Commits on Source (10)
...@@ -267,6 +267,14 @@ class Availability(models.Model): ...@@ -267,6 +267,14 @@ class Availability(models.Model):
return Availability(start=timeframe_start, end=timeframe_end, event=event, person=person, return Availability(start=timeframe_start, end=timeframe_end, event=event, person=person,
room=room, ak=ak, ak_category=ak_category) room=room, ak=ak, ak_category=ak_category)
@classmethod
def is_event_covered(cls, event, availabilities: List['Availability']) -> bool:
# NOTE: Cannot use `Availability.with_event_length` as its end is the
# event end + 1 day
full_event = Availability(event=event, start=event.start, end=event.end)
avail_union = Availability.union(availabilities)
return not avail_union or avail_union[0].contains(full_event)
class Meta: class Meta:
verbose_name = _('Availability') verbose_name = _('Availability')
verbose_name_plural = _('Availabilities') verbose_name_plural = _('Availabilities')
......
...@@ -272,3 +272,12 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm): ...@@ -272,3 +272,12 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm):
# Filter possible values for m2m when event is specified # Filter possible values for m2m when event is specified
if hasattr(self.instance, "event") and self.instance.event is not None: if hasattr(self.instance, "event") and self.instance.event is not None:
self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event) self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event)
class JSONImportForm(AdminIntermediateForm):
json_data = forms.CharField(
required=True,
widget=forms.Textarea,
label=_("JSON data"),
help_text=_("JSON data from the scheduling solver"),
)
import itertools import itertools
import json
from datetime import timedelta from datetime import timedelta
from django.db import models from django.db import models
...@@ -162,6 +163,66 @@ class Event(models.Model): ...@@ -162,6 +163,66 @@ class Event(models.Model):
.filter(availabilities__count=0, owners__count__gt=0) .filter(availabilities__count=0, owners__count__gt=0)
) )
def time_slots(self, *, slots_in_an_hour=1.0):
from AKModel.availability.models import Availability
rooms = Room.objects.filter(event=self)
slot_duration = timedelta(hours=(1.0 / slots_in_an_hour))
slot_index = 0
current_slot = self.start
current_block = []
previous_slot = None
room_availabilities = list({availability
for room in rooms
for availability in room.availabilities.all()})
while current_slot < self.end:
slot = Availability(event=self,
start=current_slot,
end=current_slot + slot_duration)
if any((availability.contains(slot)
for availability in room_availabilities)):
if previous_slot is not None and previous_slot + slot_duration < current_slot:
yield current_block
current_block = []
current_block.append(slot_index)
previous_slot = current_slot
slot_index += 1
current_slot += slot_duration
yield current_block
def time_slot(self, *, time_slot_index, slots_in_an_hour=1.0):
from AKModel.availability.models import Availability
slot_duration = timedelta(hours=(1.0 / slots_in_an_hour))
start = self.start + time_slot_index * slot_duration
return Availability(event=self,
start=start,
end=start + slot_duration)
def schedule_from_json(self, schedule):
schedule = json.loads(schedule)
slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"]
for scheduled_slot in schedule["scheduled_aks"]:
slot = AKSlot.objects.get(scheduled_slot["ak_id"])
slot.room = scheduled_slot["room_id"]
start = min(scheduled_slot["time_slot_ids"])
end = max(scheduled_slot["time_slot_ids"])
slot.start = self.time_slot(time_slot_index=start,
slots_in_an_hour=slots_in_an_hour)
slot.end = self.time_slot(time_slot_index=end + 1,
slots_in_an_hour=slots_in_an_hour)
class AKOwner(models.Model): class AKOwner(models.Model):
""" An AKOwner describes the person organizing/holding an AK. """ An AKOwner describes the person organizing/holding an AK.
...@@ -513,6 +574,29 @@ class Room(models.Model): ...@@ -513,6 +574,29 @@ class Room(models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
def as_json(self) -> str:
from AKModel.availability.models import Availability
# check if room is available for the whole event
# -> no time constraint needs to be introduced
if Availability.is_event_covered(self.event, self.availabilities.all()):
time_constraints = []
else:
time_constraints = [f"availability-room-{self.pk}"]
data = {
"id": self.pk,
"info": {
"name": self.name,
},
"capacity": self.capacity,
"fulfilled_room_constraints": [constraint.name
for constraint in self.properties.all()],
"time_constraints": time_constraints
}
return json.dumps(data)
class AKSlot(models.Model): class AKSlot(models.Model):
""" An AK Mapping matches an AK to a room during a certain time. """ An AK Mapping matches an AK to a room during a certain time.
...@@ -608,6 +692,44 @@ class AKSlot(models.Model): ...@@ -608,6 +692,44 @@ class AKSlot(models.Model):
self.duration = min(self.duration, event_duration_hours) self.duration = min(self.duration, event_duration_hours)
super().save(force_insert, force_update, using, update_fields) super().save(force_insert, force_update, using, update_fields)
def as_json(self) -> str:
from AKModel.availability.models import Availability
# check if ak resp. owner is available for the whole event
# -> no time constraint needs to be introduced
if not self.fixed and Availability.is_event_covered(self.event, self.ak.availabilities.all()):
ak_time_constraints = []
else:
ak_time_constraints = [f"availability-ak-{self.ak.pk}"]
def _owner_time_constraints(owner: AKOwner):
if Availability.is_event_covered(self.event, owner.availabilities.all()):
return []
else:
return [f"availability-person-{owner.pk}"]
data = {
"id": self.pk,
"duration": int(self.duration * self.slots_in_an_hour),
"properties": {},
"room_constraints": [constraint.name
for constraint in self.ak.requirements.all()],
"time_constraints": ["resolution"] if self.ak.reso else [],
"info": {
"name": self.ak.name,
"head": ", ".join([str(owner)
for owner in self.ak.owners.all()]),
"description": self.ak.description,
"reso": self.ak.reso,
},
}
data["time_constraints"].extend(ak_time_constraints)
for owner in self.ak.owners.all():
data["time_constraints"].extend(_owner_time_constraints(owner))
return json.dumps(data)
class AKOrgaMessage(models.Model): class AKOrgaMessage(models.Model):
""" """
......
{% extends "admin/base_site.html" %}
{% load tz %}
{% block content %}
<pre>
{"aks": [
{% for slot in slots %}{{ slot.as_json }}{% if not forloop.last %},
{% endif %}{% endfor %}
],
"rooms": [
{% for room in rooms %}{{ room.as_json }}{% if not forloop.last %},
{% endif %}{% endfor %}
],
"participants": {{ participants }},
"timeslots": {{ timeslots }}
}
</pre>
{% endblock %}
...@@ -5,8 +5,9 @@ from rest_framework.routers import DefaultRouter ...@@ -5,8 +5,9 @@ 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, DefaultSlotEditorView, \
AKsByUserView AKsByUserView, AKJSONImportView
from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKJSONExportView, 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
...@@ -96,6 +97,10 @@ def get_admin_urls_event(admin_site): ...@@ -96,6 +97,10 @@ def get_admin_urls_event(admin_site):
name="aks_by_owner"), name="aks_by_owner"),
path('<slug:event_slug>/ak-csv-export/', admin_site.admin_view(AKCSVExportView.as_view()), path('<slug:event_slug>/ak-csv-export/', admin_site.admin_view(AKCSVExportView.as_view()),
name="ak_csv_export"), name="ak_csv_export"),
path('<slug:event_slug>/ak-json-export/', admin_site.admin_view(AKJSONExportView.as_view()),
name="ak_json_export"),
path('<slug:event_slug>/ak-json-import/', admin_site.admin_view(AKJSONImportView.as_view()),
name="ak_json_import"),
path('<slug:slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()), path('<slug:slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()),
name="ak_wiki_export"), name="ak_wiki_export"),
path('<slug:event_slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()), path('<slug:event_slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()),
......
import json
from datetime import timedelta
from typing import List
from django.contrib import messages from django.contrib import messages
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
...@@ -5,7 +9,7 @@ from django.views.generic import ListView, DetailView ...@@ -5,7 +9,7 @@ from django.views.generic import ListView, DetailView
from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \ from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \
IntermediateAdminActionView IntermediateAdminActionView
from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner
class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView): class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
...@@ -37,6 +41,111 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView): ...@@ -37,6 +41,111 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
return super().get_queryset().order_by("ak__track") return super().get_queryset().order_by("ak__track")
class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
"""
View: Export all AK slots of this event in JSON format ordered by tracks
"""
template_name = "admin/AKModel/ak_json_export.html"
model = AKSlot
context_object_name = "slots"
title = _("AK JSON Export")
def get_queryset(self):
return super().get_queryset().order_by("ak__track")
def get_context_data(self, **kwargs):
from AKModel.availability.models import Availability
SLOTS_IN_AN_HOUR = 1
rooms = Room.objects.filter(event=self.event)
participants = []
timeslots = {
"info": {"duration": (1.0 / SLOTS_IN_AN_HOUR), },
"blocks": [],
}
context = super().get_context_data(**kwargs)
context["rooms"] = rooms
context["participants"] = json.dumps(participants)
for slot in context["slots"]:
slot.slots_in_an_hour = SLOTS_IN_AN_HOUR
ak_availabilities = {
slot.ak.pk: Availability.union(slot.ak.availabilities.all())
for slot in context["slots"]
}
room_availabilities = {
room.pk: Availability.union(room.availabilities.all())
for room in rooms
}
person_availabilities = {
person.pk: Availability.union(person.availabilities.all())
for person in AKOwner.objects.filter(event=self.event)
}
ak_fixed = {
ak: values.get()
for ak in ak_availabilities.keys()
if (values := AKSlot.objects.select_related().filter(ak__pk=ak, fixed=True)).exists()
}
def _test_slot_contained(slot: Availability, availabilities: List[Availability]) -> bool:
return any(availability.contains(slot) for availability in availabilities)
def _test_event_covered(slot: Availability, availabilities: List[Availability]) -> bool:
return not Availability.is_event_covered(self.event, availabilities)
def _test_fixed_ak(ak, slot) -> bool:
if not ak in ak_fixed:
return False
fixed_slot = Availability(self.event, start=ak_fixed[ak].start, end=ak_fixed[ak].end)
return fixed_slot.overlaps(slot, strict=True)
def _test_add_constraint(slot: Availability, availabilities: List[Availability]) -> bool:
return _test_event_covered(slot, availabilities) and _test_slot_contained(slot, availabilities)
for block in self.event.time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR):
current_block = []
for slot_index in block:
slot = self.event.time_slot(time_slot_index=slot_index,
slots_in_an_hour=SLOTS_IN_AN_HOUR)
constraints = []
if self.event.reso_deadline is None or slot.end < self.event.reso_deadline:
constraints.append("resolution")
for ak, availabilities in ak_availabilities.items():
if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak, slot):
constraints.append(f"availability-ak-{ak}")
for person, availabilities in person_availabilities.items():
if _test_add_constraint(slot, availabilities):
constraints.append(f"availability-person-{person}")
for person, availabilities in room_availabilities.items():
if _test_add_constraint(slot, availabilities):
constraints.append(f"availability-room-{room}")
current_block.append({
"id": slot_index,
"info": {
"start": slot.simplified,
},
"fulfilled_time_constraints": constraints,
})
timeslots["blocks"].append(current_block)
context["timeslots"] = json.dumps(timeslots)
return context
class AKWikiExportView(AdminViewMixin, DetailView): class AKWikiExportView(AdminViewMixin, DetailView):
""" """
View: Export AKs of this event in wiki syntax View: Export AKs of this event in wiki syntax
......
...@@ -12,7 +12,7 @@ from django.views.generic import TemplateView, DetailView ...@@ -12,7 +12,7 @@ from django.views.generic import TemplateView, DetailView
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, JSONImportForm
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
...@@ -245,3 +245,13 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView): ...@@ -245,3 +245,13 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView):
model = AKOwner model = AKOwner
context_object_name = 'owner' context_object_name = 'owner'
template_name = "admin/AKModel/aks_by_user.html" template_name = "admin/AKModel/aks_by_user.html"
class AKJSONImportView(EventSlugMixin, IntermediateAdminView):
form_class = JSONImportForm
title = _("AK JSON Import")
def form_valid(self, form):
self.event.schedule_from_json(form.data["json_data"])
return redirect("admin:event_status", self.event.slug)
...@@ -133,10 +133,18 @@ class EventAKsWidget(TemplateStatusWidget): ...@@ -133,10 +133,18 @@ class EventAKsWidget(TemplateStatusWidget):
"text": _("Manage ak tracks"), "text": _("Manage ak tracks"),
"url": reverse_lazy("admin:tracks_manage", kwargs={"event_slug": context["event"].slug}), "url": reverse_lazy("admin:tracks_manage", kwargs={"event_slug": context["event"].slug}),
}, },
{
"text": _("Import AK schedule from JSON"),
"url": reverse_lazy("admin:ak_json_import", kwargs={"event_slug": context["event"].slug}),
},
{ {
"text": _("Export AKs as CSV"), "text": _("Export AKs as CSV"),
"url": reverse_lazy("admin:ak_csv_export", kwargs={"event_slug": context["event"].slug}), "url": reverse_lazy("admin:ak_csv_export", kwargs={"event_slug": context["event"].slug}),
}, },
{
"text": _("Export AKs as JSON"),
"url": reverse_lazy("admin:ak_json_export", kwargs={"event_slug": context["event"].slug}),
},
{ {
"text": _("Export AKs for Wiki"), "text": _("Export AKs for Wiki"),
"url": reverse_lazy("admin:ak_wiki_export", kwargs={"slug": context["event"].slug}), "url": reverse_lazy("admin:ak_wiki_export", kwargs={"slug": context["event"].slug}),
......
...@@ -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: 2024-05-27 01:57+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"
...@@ -38,7 +38,7 @@ msgstr "Veranstaltung" ...@@ -38,7 +38,7 @@ 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"
...@@ -63,12 +63,12 @@ msgid "AK Wall" ...@@ -63,12 +63,12 @@ msgid "AK Wall"
msgstr "AK-Wall" msgstr "AK-Wall"
#: AKPlan/templates/AKPlan/plan_index.html:130 #: AKPlan/templates/AKPlan/plan_index.html:130
#: AKPlan/templates/AKPlan/plan_wall.html:130 #: 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:137
#: 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"
...@@ -99,7 +99,7 @@ msgstr "Eigenschaften" ...@@ -99,7 +99,7 @@ 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?"
......
...@@ -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-08-16 16:30+0200\n" "POT-Creation-Date: 2024-05-27 01:57+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"
...@@ -17,10 +17,10 @@ msgstr "" ...@@ -17,10 +17,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: AKPlanning/settings.py:148 #: AKPlanning/settings.py:147
msgid "German" msgid "German"
msgstr "Deutsch" msgstr "Deutsch"
#: AKPlanning/settings.py:149 #: AKPlanning/settings.py:148
msgid "English" msgid "English"
msgstr "Englisch" msgstr "Englisch"
...@@ -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-08-16 16:30+0200\n" "POT-Creation-Date: 2024-05-27 01:57+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"
...@@ -17,16 +17,16 @@ msgstr "" ...@@ -17,16 +17,16 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: AKSubmission/forms.py:93 #: AKSubmission/forms.py:95
#, python-format #, python-format
msgid "\"%(duration)s\" is not a valid duration" msgid "\"%(duration)s\" is not a valid duration"
msgstr "\"%(duration)s\" ist keine gültige Dauer" msgstr "\"%(duration)s\" ist keine gültige Dauer"
#: AKSubmission/forms.py:159 #: AKSubmission/forms.py:155
msgid "Duration(s)" msgid "Duration(s)"
msgstr "Dauer(n)" msgstr "Dauer(n)"
#: AKSubmission/forms.py:161 #: AKSubmission/forms.py:157
msgid "" msgid ""
"Enter at least one planned duration (in hours). If your AK should have " "Enter at least one planned duration (in hours). If your AK should have "
"multiple slots, use multiple lines" "multiple slots, use multiple lines"
......