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

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
Show changes
Showing
with 661 additions and 141 deletions
{% extends "AKPlan/plan_base.html" %}
{% load fontawesome_5 %}
{% load fontawesome_6 %}
{% load i18n %}
{% load static %}
{% load tz %}
{% load tags_AKPlan %}
{% block fullcalendar %}
......@@ -27,7 +28,11 @@
right: ''
},
aspectRatio: 2,
themeSystem: 'bootstrap',
themeSystem: 'bootstrap5',
buttonIcons: {
prev: 'ignore fa-solid fa-angle-left',
next: 'ignore fa-solid fa-angle-right',
},
// Only show calendar view for the dates of the connected event
visibleRange: {
start: '{{ event.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
......@@ -36,6 +41,7 @@
scrollTime: '08:00:00',
allDaySlot: false,
nowIndicator: true,
now: "{% timestamp_now event.timezone %}",
eventTextColor: '#fff',
eventColor: '#127ba3',
// Create entries for all scheduled slots
......
{% extends "AKPlan/plan_base.html" %}
{% load fontawesome_5 %}
{% load fontawesome_6 %}
{% load i18n %}
{% load static %}
{% load tz %}
{% load tags_AKPlan %}
{% block fullcalendar %}
......@@ -21,7 +22,11 @@
center: 'title',
right: 'resourceTimelineDay,resourceTimelineEvent'
},
themeSystem: 'bootstrap',
themeSystem: 'bootstrap5',
buttonIcons: {
prev: 'ignore fa-solid fa-angle-left',
next: 'ignore fa-solid fa-angle-right',
},
// Adapt to user selected locale
locale: '{{ LANGUAGE_CODE }}',
initialView: 'resourceTimelineEvent',
......@@ -47,6 +52,7 @@
editable: false,
allDaySlot: false,
nowIndicator: true,
now: "{% timestamp_now event.timezone %}",
eventTextColor: '#fff',
eventColor: '#127ba3',
resourceAreaWidth: '15%',
......@@ -77,11 +83,11 @@
{% block content %}
<div class="float-right">
<div class="float-end">
<ul class="nav nav-pills">
{% if event.room_set.count > 0 %}
{% if rooms|length > 0 %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button"
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button"
aria-haspopup="true"
aria-expanded="false">{% trans "Rooms" %}</a>
<div class="dropdown-menu" style="">
......@@ -92,13 +98,13 @@
</div>
</li>
{% endif %}
{% if event.aktrack_set.count > 0 %}
{% if tracks|length > 0 %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button"
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button"
aria-haspopup="true"
aria-expanded="false">{% trans "Tracks" %}</a>
<div class="dropdown-menu">
{% for t in event.aktrack_set.all %}
{% for t in tracks %}
<a class="dropdown-item"
href="{% url "plan:plan_track" event_slug=event.slug pk=t.pk %}">{{ t }}</a>
{% endfor %}
......@@ -108,7 +114,7 @@
{% if event.active %}
<li class="nav-item">
<a class="nav-link active"
href="{% url 'plan:plan_wall' event_slug=event.slug %}">{% fa5_icon 'desktop' 'fas' %}&nbsp;&nbsp;{% trans "AK Wall" %}</a>
href="{% url 'plan:plan_wall' event_slug=event.slug %}">{% fa6_icon 'desktop' 'fas' %}&nbsp;&nbsp;{% trans "AK Wall" %}</a>
</li>
{% endif %}
</ul>
......
{% extends "AKPlan/plan_detail.html" %}
{% load fontawesome_5 %}
{% load fontawesome_6 %}
{% load tags_AKModel %}
{% load tz %}
......@@ -16,12 +16,12 @@
{% block encode %}
[
{% for slot in room.akslot_set.all %}
{% for slot in slots %}
{% if slot.start %}
{'title': '{{ slot.ak }}',
'start': '{{ slot.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
'end': '{{ slot.end | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
'url': '{% url 'submit:ak_detail' event_slug=event.slug pk=slot.ak.pk %}',
'url': '{{ slot.ak.detail_url }}',
'borderColor': '{{ slot.ak.track.color }}',
'color': '{{ slot.ak.category.color }}',
},
......@@ -43,10 +43,10 @@
{% block content %}
<div class="float-right">
<div class="float-end">
<ul class="nav nav-pills">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "Rooms" %}</a>
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "Rooms" %}</a>
<div class="dropdown-menu" style="">
{% for r in event.room_set.all %}
<a class="dropdown-item" href="{% url "plan:plan_room" event_slug=event.slug pk=r.pk %}">{{ r }}</a>
......@@ -58,9 +58,9 @@
<h1>{% trans "Room" %}: {{ room.name }} {% if room.location != '' %}({{ room.location }}){% endif %}</h1>
{% if "AKOnline"|check_app_installed and room.virtualroom and room.virtualroom.url != '' %}
<a class="btn btn-success" href="{{ room.virtualroom.url }}">
{% fa5_icon 'external-link-alt' 'fas' %} {% trans "Go to virtual room" %}
{% if "AKOnline"|check_app_installed and room.virtual and room.virtual.url != '' %}
<a class="btn btn-success" target="_parent" href="{{ room.virtual.url }}">
{% fa6_icon 'external-link-alt' 'fas' %} {% trans "Go to virtual room" %}
</a>
{% endif %}
......
......@@ -19,7 +19,7 @@
{'title': '{{ slot.ak }} @ {{ slot.room }}',
'start': '{{ slot.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
'end': '{{ slot.end | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
'url': '{% url 'submit:ak_detail' event_slug=event.slug pk=slot.ak.pk %}',
'url': '{{ slot.ak.detail_url }}',
'color': '{{ track.color }}',
'borderColor': '{{ slot.ak.category.color }}',
},
......@@ -30,10 +30,10 @@
{% block content %}
<div class="float-right">
<div class="float-end">
<ul class="nav nav-pills">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "Tracks" %}</a>
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "Tracks" %}</a>
<div class="dropdown-menu">
{% for t in event.aktrack_set.all %}
<a class="dropdown-item" href="{% url "plan:plan_track" event_slug=event.slug pk=t.pk %}">{{ t }}</a>
......
{% load compress %}
{% load static %}
{% load i18n %}
{% load bootstrap4 %}
{% load fontawesome_5 %}
{% load django_bootstrap5 %}
{% load fontawesome_6 %}
{% load tags_AKModel %}
{% load tags_AKPlan %}
{% load tz %}
......@@ -13,13 +15,21 @@
<title>{% block title %}AK Planning{% endblock %}</title>
{# Load Bootstrap CSS and JavaScript as well as font awesome #}
{% bootstrap_css %}
{% bootstrap_javascript jquery='slim' %}
{% fontawesome_5_static %}
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static 'common/vendor/bootswatch-lumen/theme.scss' %}">
{% fontawesome_6_css %}
<link rel="stylesheet" href="{% static 'common/css/custom.css' %}">
{% endcompress %}
<link rel="stylesheet" href="{% static 'common/css/custom.css' %}">
{% compress js %}
{% bootstrap_javascript %}
<script src="{% static 'common/vendor/jquery/jquery-3.6.3.min.js' %}"></script>
{% fontawesome_6_js %}
{% endcompress %}
{% include "AKPlan/load_fullcalendar.html" %}
{% include "AKModel/load_fullcalendar.html" %}
{% get_current_language as LANGUAGE_CODE %}
<script>
document.addEventListener('DOMContentLoaded', function () {
......@@ -28,7 +38,11 @@
var plan = new FullCalendar.Calendar(planEl, {
timeZone: '{{ event.timezone }}',
headerToolbar: false,
themeSystem: 'bootstrap',
themeSystem: 'bootstrap5',
buttonIcons: {
prev: 'ignore fa-solid fa-angle-left',
next: 'ignore fa-solid fa-angle-right',
},
// Adapt to user selected locale
locale: '{{ LANGUAGE_CODE }}',
slotDuration: '01:00',
......@@ -37,12 +51,15 @@
start: '{{ start | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
end: '{{ end | timezone:event.timezone | date:"Y-m-d H:i:s"}}',
},
slotMinTime: '{{ earliest_start_hour }}:00:00',
slotMaxTime: '{{ latest_end_hour }}:00:00',
eventDidMount: function(info) {
$(info.el).tooltip({title: info.event.extendedProps.description});
},
editable: false,
allDaySlot: false,
nowIndicator: true,
now: "{% timestamp_now event.timezone %}",
eventTextColor: '#fff',
eventColor: '#127ba3',
height: '90%',
......@@ -63,12 +80,48 @@
plan.render();
// Scroll to current time
if($(".fc-timeline-now-indicator-line").length) {
$('.fc-scroller').scrollLeft($('.fc-timeline-now-indicator-line').position().left);
if($(".fc-timeline-now-indicator-line").length) {
$('.fc-scroller').scrollLeft($('.fc-timeline-now-indicator-line').position().left);
}
// == Auto Reload ==
// function from: https://stackoverflow.com/questions/5448545/how-to-retrieve-get-parameters-from-javascript/
function findGetParameter(parameterName) {
var result = null,
tmp = [];
location.search
.substr(1)
.split("&")
.forEach(function (item) {
tmp = item.split("=");
if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
});
return result;
}
// Check whether an autoreload frequency was specified and treat it as full minutes
const autoreload_frequency = Math.ceil(findGetParameter("autoreload"));
const cbxAutoReload = $('#cbxAutoReload');
if(autoreload_frequency>0) {
window.setTimeout ( function() { window.location.reload(); }, autoreload_frequency * 60 * 1000);
console.log("Autoreload active");
cbxAutoReload.prop('checked', true);
}
else {
cbxAutoReload.prop('checked', false);
}
cbxAutoReload.change(function () {
let url = window.location.href.split('?')[0];
if(cbxAutoReload.prop('checked')) {
url = url + "?autoreload=5";
}
window.location.replace(url);
});
});
</script>
</head>
<body>
{% timezone event.timezone %}
......@@ -90,6 +143,9 @@
<div id="planCalendar"></div>
</div>
</div>
<div style="position: absolute;bottom: 1vh;left:1vw;background-color: #FFFFFF;padding: 1vh;">
<input type="checkbox" name="autoreload" id="cbxAutoReload"> <label for="cbxAutoReload">{% trans "Reload page automatically?" %}</label>
</div>
{% endtimezone %}
</body>
......
......@@ -4,7 +4,7 @@
<table class="table table-striped">
{% for akslot in slots %}
<tr>
<td class="breakWord"><b><a href="{% url 'submit:ak_detail' event_slug=event.slug pk=akslot.ak.pk %}">{{ akslot.ak.name }}</a></b></td>
<td class="breakWord"><b><a href="{{ akslot.ak.detail_url }}">{{ akslot.ak.name }}</a></b></td>
<td>{{ akslot.start | time:"H:i" }} - {{ akslot.end | time:"H:i" }}</td>
<td class="breakWord">{% if akslot.room and akslot.room.pk != '' %}
<a href="{% url 'plan:plan_room' event_slug=event.slug pk=akslot.room.pk %}">{{ akslot.room }}</a>
......
# gradients based on http://bsou.io/posts/color-gradients-with-python
def hex_to_rgb(hex):
def hex_to_rgb(hex): #pylint: disable=redefined-builtin
"""
Convert hex color to RGB color code
:param hex: hex encoded color
......@@ -23,8 +23,7 @@ def rgb_to_hex(rgb):
"""
# Components need to be integers for hex to make sense
rgb = [int(x) for x in rgb]
return "#"+"".join(["0{0:x}".format(v) if v < 16 else
"{0:x}".format(v) for v in rgb])
return "#"+"".join([f"0{v:x}" if v < 16 else f"{v:x}" for v in rgb])
def linear_blend(start_hex, end_hex, position):
......
from datetime import datetime
from django import template
from django.utils.formats import date_format
from AKPlan.templatetags.color_gradients import darken
from AKPlanning import settings
......@@ -8,6 +11,19 @@ register = template.Library()
@register.filter
def highlight_change_colors(akslot):
"""
Adjust color to highlight recent changes if needed
:param akslot: akslot to determine color for
:type akslot: AKSlot
:return: color that should be used (either default color of the category or some kind of red)
:rtype: str
"""
# Do not highlight in preview mode or when changes occurred before the plan was published
if akslot.event.plan_hidden or (akslot.event.plan_published_at is not None
and akslot.event.plan_published_at > akslot.updated):
return akslot.ak.category.color
seconds_since_update = akslot.seconds_since_last_update
# Last change long ago? Use default color
......@@ -17,4 +33,14 @@ def highlight_change_colors(akslot):
# Recent change? Calculate gradient blend between red and
recentness = seconds_since_update / settings.PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS
return darken("#b71540", recentness)
# return linear_blend("#b71540", "#000000", recentness)
@register.simple_tag
def timestamp_now(tz):
"""
Get the current timestamp for the given timezone
:param tz: timezone to be used for the timestamp
:return: current timestamp in given timezone
"""
return date_format(datetime.now().astimezone(tz), "c")
# Create your tests here.
from django.test import TestCase
from AKModel.tests import BasicViewTests
class PlanViewTests(BasicViewTests, TestCase):
"""
Tests for AKPlan
"""
fixtures = ['model.json']
APP_NAME = 'plan'
VIEWS = [
('plan_overview', {'event_slug': 'kif42'}),
('plan_wall', {'event_slug': 'kif42'}),
('plan_room', {'event_slug': 'kif42', 'pk': 2}),
('plan_track', {'event_slug': 'kif42', 'pk': 1}),
]
def test_plan_hidden(self):
"""
Test correct handling of plan visibility
"""
_, url = self._name_and_url(('plan_overview', {'event_slug': 'kif23'}))
self.client.logout()
response = self.client.get(url)
self.assertContains(response, "Plan is not visible (yet).",
msg_prefix="Plan is visible even though it shouldn't be")
self.client.force_login(self.staff_user)
response = self.client.get(url)
self.assertNotContains(response, "Plan is not visible (yet).",
msg_prefix="Plan is not visible for staff user")
def test_wall_redirect(self):
"""
Test: Make sure that user is redirected from wall to overview when plan is hidden
"""
_, url_wall = self._name_and_url(('plan_wall', {'event_slug': 'kif23'}))
_, url_plan = self._name_and_url(('plan_overview', {'event_slug': 'kif23'}))
response = self.client.get(url_wall)
self.assertRedirects(response, url_plan,
msg_prefix=f"Redirect away from wall not working ({url_wall} -> {url_plan})")
from datetime import timedelta
from datetime import datetime, timedelta
from django.conf import settings
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.datetime_safe import datetime
from django.views.generic import ListView, DetailView
from django.views.generic import DetailView, ListView
from AKModel.models import AKSlot, Room, AKTrack
from AKModel.views import FilterByEventSlugMixin
from AKModel.metaviews.admin import FilterByEventSlugMixin
from AKModel.models import AKSlot, AKTrack, Room
class PlanIndexView(FilterByEventSlugMixin, ListView):
"""
Default plan view
Shows two lists of current and upcoming AKs and a graphical full plan below
"""
model = AKSlot
template_name = "AKPlan/plan_index.html"
context_object_name = "akslots"
......@@ -18,7 +22,7 @@ class PlanIndexView(FilterByEventSlugMixin, ListView):
def get_queryset(self):
# Ignore slots not scheduled yet
return super().get_queryset().filter(start__isnull=False)
return super().get_queryset().filter(start__isnull=False).select_related('ak', 'room', 'ak__category')
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
......@@ -54,10 +58,21 @@ class PlanIndexView(FilterByEventSlugMixin, ListView):
if settings.PLAN_SHOW_HIERARCHY:
context["buildings"] = sorted(buildings)
context["tracks"] = self.event.aktrack_set.all()
return context
class PlanScreenView(PlanIndexView):
"""
Plan view optimized for screens and projectors
This again shows current and upcoming AKs as well as a graphical plan,
but no navigation elements and trys to use the available space as best as possible
such that no scrolling is needed.
The view contains a frontend functionality for auto-reload.
"""
template_name = "AKPlan/plan_wall.html"
def get(self, request, *args, **kwargs):
......@@ -67,11 +82,12 @@ class PlanScreenView(PlanIndexView):
return redirect(reverse_lazy("plan:plan_overview", kwargs={"event_slug": self.event.slug}))
return s
"""
# pylint: disable=attribute-defined-outside-init
def get_queryset(self):
# Determine interesting range (some hours ago until some hours in the future as specified in the settings)
now = datetime.now().astimezone(self.event.timezone)
# Wall during event: Adjust, show only parts in the future
if self.event.start < now < self.event.end:
# Determine interesting range (some hours ago until some hours in the future as specified in the settings)
self.start = now - timedelta(hours=settings.PLAN_WALL_HOURS_RETROSPECT)
else:
self.start = self.event.start
......@@ -79,32 +95,63 @@ class PlanScreenView(PlanIndexView):
# Restrict AK slots to relevant ones
# This will automatically filter all rooms not needed for the selected range in the orginal get_context method
return super().get_queryset().filter(start__gt=self.start)
"""
akslots = super().get_queryset().filter(start__gt=self.start)
# Find the earliest hour AKs start and end (handle 00:00 as 24:00)
self.earliest_start_hour = 23
self.latest_end_hour = 1
for akslot in akslots.all():
start_hour = akslot.start.astimezone(self.event.timezone).hour
if start_hour < self.earliest_start_hour:
# Use hour - 1 to improve visibility of date change
self.earliest_start_hour = max(start_hour - 1, 0)
end_hour = akslot.end.astimezone(self.event.timezone).hour
# Special case: AK starts before but ends after midnight -- show until midnight
if end_hour < start_hour:
self.latest_end_hour = 24
elif end_hour > self.latest_end_hour:
# Always use hour + 1, since AK may end at :xy and not always at :00
self.latest_end_hour = min(end_hour + 1, 24)
return akslots
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
context["start"] = self.event.start
context["start"] = self.start
context["end"] = self.event.end
context["earliest_start_hour"] = self.earliest_start_hour
context["latest_end_hour"] = self.latest_end_hour
return context
class PlanRoomView(FilterByEventSlugMixin, DetailView):
"""
Plan view for a single room
"""
template_name = "AKPlan/plan_room.html"
model = Room
context_object_name = "room"
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
# Restrict AKSlot list to the given room
# while joining AK, room and category information to reduce the amount of necessary SQL queries
context["slots"] = AKSlot.objects.filter(room=context['room']).select_related('ak', 'ak__category', 'ak__track')
return context
class PlanTrackView(FilterByEventSlugMixin, DetailView):
"""
Plan view for a single track
"""
template_name = "AKPlan/plan_track.html"
model = AKTrack
context_object_name = "track"
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
context["slots"] = []
for ak in context["track"].ak_set.all():
context["slots"].extend(ak.akslot_set.all())
# Restrict AKSlot list to given track
# while joining AK, room and category information to reduce the amount of necessary SQL queries
context["slots"] = AKSlot.objects. \
filter(event=self.event, ak__track=context['track']). \
select_related('ak', 'room', 'ak__category')
return context
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-29 22:48+0000\n"
"POT-Creation-Date: 2023-08-16 16:30+0200\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"
......@@ -17,10 +17,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: AKPlanning/settings.py:134
#: AKPlanning/settings.py:148
msgid "German"
msgstr "Deutsch"
#: AKPlanning/settings.py:135
#: AKPlanning/settings.py:149
msgid "English"
msgstr "Englisch"
......@@ -45,17 +45,20 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'debug_toolbar',
'bootstrap4',
'fontawesome_5',
'django_bootstrap5',
'fontawesomefree',
'fontawesome_6',
'timezone_field',
'rest_framework',
'simple_history',
'registration',
'bootstrap_datepicker_plus',
'django_tex',
'compressor',
'docs',
]
MIDDLEWARE = [
'django.middleware.gzip.GZipMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
......@@ -99,6 +102,8 @@ TEMPLATES = [
WSGI_APPLICATION = 'AKPlanning.wsgi.application'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
......@@ -136,8 +141,6 @@ TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
LANGUAGES = [
......@@ -159,29 +162,38 @@ STATICFILES_DIRS = (
'static_common',
)
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
)
# Settings for Bootstrap
BOOTSTRAP4 = {
# Use custom CSS
"css_url": {
"href": STATIC_URL + "common/css/bootstrap.css",
},
BOOTSTRAP5 = {
"javascript_url": {
"url": STATIC_URL + "common/vendor/bootstrap/bootstrap-4.3.1.min.js",
},
"jquery_url": {
"url": STATIC_URL + "common/vendor/jquery/jquery-3.3.1.min.js",
},
"jquery_slim_url": {
"url": STATIC_URL + "common/vendor/jquery/jquery-3.3.1.slim.min.js",
},
"popper_url": {
"url": STATIC_URL + "common/vendor/popper/popper-1.14.7.min.js",
"url": STATIC_URL + "common/vendor/bootstrap/bootstrap-5.0.2.bundle.min.js",
},
}
# Settings for FontAwesome
FONTAWESOME_5_CSS_URL = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.css"
FONTAWESOME_5_PREFIX = "fa"
FONTAWESOME_6_CSS_URL = STATIC_URL + "fontawesomefree/css/all.min.css"
FONTAWESOME_6_PREFIX = "fa"
# Compressor and minifier config
COMPRESS_ENABLED = True
COMPRESS_CSS_HASHING_METHOD = 'content'
COMPRESS_PRECOMPILERS = (
('text/x-scss', 'django_libsass.SassCompiler'),
)
COMPRESS_FILTERS = {
'css': [
'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.cssmin.rCSSMinFilter',
],
'js': [
'compressor.filters.jsmin.JSMinFilter',
]
}
# Treat wishes as seperate category in submission views?
WISHES_AS_CATEGORY = True
......@@ -205,6 +217,9 @@ PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS = 2 * 60 * 60
DASHBOARD_SHOW_RECENT = True
# How many entries max?
DASHBOARD_RECENT_MAX = 25
# How many events should be featured in the dashboard
# (active events will always be featured, even if their number is higher than this threshold)
DASHBOARD_MAX_FEATURED_EVENTS = 3
# Registration/login behavior
SIMPLE_BACKEND_REDIRECT_URL = "/user/"
......@@ -212,10 +227,19 @@ LOGIN_REDIRECT_URL = SIMPLE_BACKEND_REDIRECT_URL
# Content Security Policy
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'")
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", "'unsafe-eval'")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "fonts.googleapis.com")
CSP_IMG_SRC = ("'self'", "data:")
CSP_FRAME_SRC = ("'self'", )
CSP_FONT_SRC = ("'self'", "data:", "fonts.gstatic.com")
# Emails
SEND_MAILS = True
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Documentation
DOCS_ROOT = os.path.join(BASE_DIR, 'docs/_build/html')
DOCS_ACCESS = 'public'
include(optional("settings/*.py"))
......@@ -15,10 +15,15 @@ DATABASES = {
'USER': 'django',
'PASSWORD': 'mysql',
'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
},
'TEST': {
'NAME': 'test',
'NAME': 'tests',
'CHARSET': "utf8mb4",
'COLLATION': 'utf8mb4_unicode_ci',
},
}
}
TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
TEST_OUTPUT_FILE_NAME = 'unit.xml'
......@@ -35,4 +35,8 @@ DATABASES = {
}
}
### EMAILS ###
SEND_MAILS = True
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# TODO: caching
"""AKPlanning URL Configuration
"""
AKPlanning URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.2/topics/http/urls/
......@@ -13,13 +14,15 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
import debug_toolbar
from django.apps import apps
from django.contrib import admin
from django.urls import path, include
import debug_toolbar
from django.urls import path, include, re_path
urlpatterns = [
path('admin/', admin.site.urls),
re_path(r'^docs/', include('docs.urls')),
path('accounts/', include('django.contrib.auth.urls')),
path('accounts/', include('registration.backends.simple.urls')),
path('', include('AKModel.urls', namespace='model')),
......
# Register your models here.
......@@ -7,48 +7,70 @@ from django.views.generic import ListView
from rest_framework import viewsets, mixins, serializers, permissions
from AKModel.availability.models import Availability
from AKModel.models import Room, AKSlot, ConstraintViolation
from AKModel.views import EventSlugMixin
from AKModel.models import Room, AKSlot, ConstraintViolation, DefaultSlot
from AKModel.metaviews.admin import EventSlugMixin
class ResourceSerializer(serializers.ModelSerializer):
"""
REST Framework Serializer for Rooms to produce format required for fullcalendar resources
"""
class Meta:
model = Room
fields = ['id', 'title']
title = serializers.SerializerMethodField('transform_title')
def transform_title(self, obj):
@staticmethod
def transform_title(obj):
"""
Adapt title, add capacity information if room has a restriction (capacity is not -1)
"""
if obj.capacity > 0:
return f"{obj.title} [{obj.capacity}]"
return obj.title
class ResourcesViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (permissions.DjangoModelPermissionsOrAnonReadOnly,)
class ResourcesViewSet(EventSlugMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
API View: Rooms (resources to schedule for in fullcalendar)
Read-only, adaption to fullcalendar format through :class:`ResourceSerializer`
"""
permission_classes = (permissions.DjangoModelPermissions,)
serializer_class = ResourceSerializer
def get_queryset(self):
return Room.objects.filter(event=self.event)
return Room.objects.filter(event=self.event).order_by('location', 'name')
class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
"""
API View: Slots (events to schedule in fullcalendar)
Read-only, JSON formatted response is created manually since it requires a bunch of "custom" fields that have
different names compared to the normal model or are not present at all and need to be computed to create the
required format for fullcalendar.
"""
model = AKSlot
def get_queryset(self):
return super().get_queryset().filter(event=self.event, room__isnull=False)
return super().get_queryset().select_related('ak').filter(event=self.event, room__isnull=False)
def render_to_response(self, context, **response_kwargs):
return JsonResponse(
[{
"slotID": slot.pk,
"title": f'{slot.ak.short_name}: \n{slot.ak.owners_list}',
"title": f'{slot.ak.short_name}:\n{slot.ak.owners_list}',
"description": slot.ak.details,
"resourceId": slot.room.id,
"start": timezone.localtime(slot.start, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"),
"end": timezone.localtime(slot.end, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"),
"backgroundColor": slot.ak.category.color,
"borderColor": "#2c3e50" if slot.fixed else '#e74c3c' if slot.constraintviolation_set.count() > 0 else slot.ak.category.color,
"borderColor":
"#2c3e50" if slot.fixed
else '#e74c3c' if slot.constraintviolation_set.count() > 0
else slot.ak.category.color,
"constraint": 'roomAvailable',
"editable": not slot.fixed,
'url': str(reverse('admin:AKModel_akslot_change', args=[slot.pk])),
......@@ -59,6 +81,13 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView):
"""
API view: Availabilities of rooms
Read-only, JSON formatted response is created manually since it requires a bunch of "custom" fields that have
different names compared to the normal model or are not present at all and need to be computed to create the
required format for fullcalendar.
"""
model = Availability
context_object_name = "availabilities"
......@@ -80,7 +109,41 @@ class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView):
)
class DefaultSlotsView(LoginRequiredMixin, EventSlugMixin, ListView):
"""
API view: default slots
Read-only, JSON formatted response is created manually since it requires a bunch of "custom" fields that have
different names compared to the normal model or are not present at all and need to be computed to create the
required format for fullcalendar.
"""
model = DefaultSlot
context_object_name = "default_slots"
def get_queryset(self):
return super().get_queryset().filter(event=self.event)
def render_to_response(self, context, **response_kwargs):
all_room_ids = [r.pk for r in self.event.room_set.all()]
return JsonResponse(
[{
"title": "",
"resourceIds": all_room_ids,
"start": timezone.localtime(a.start, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"),
"end": timezone.localtime(a.end, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"),
"display": 'background',
"groupId": 'defaultSlot',
"backgroundColor": '#69b6d4'
} for a in context["default_slots"]],
safe=False,
**response_kwargs
)
class EventSerializer(serializers.ModelSerializer):
"""
REST framework serializer to adapt between AKSlot model and the event format of fullcalendar
"""
class Meta:
model = AKSlot
fields = ['id', 'start', 'end', 'roomId']
......@@ -90,17 +153,31 @@ class EventSerializer(serializers.ModelSerializer):
roomId = serializers.IntegerField(source='room.pk')
def update(self, instance, validated_data):
# Ignore timezone of input (treat it as timezone-less) and set the event timezone
# By working like this, the client does not need to know about timezones, since every timestamp it deals with
# has the timezone offsets already applied
start = timezone.make_aware(timezone.make_naive(validated_data.get('start')), instance.event.timezone)
end = timezone.make_aware(timezone.make_naive(validated_data.get('end')), instance.event.timezone)
instance.start = start
instance.room = get_object_or_404(Room, pk=validated_data.get('room')["pk"])
# Also, adapt from start & end format of fullcalendar to our start & duration model
diff = end - start
instance.duration = round(diff.days * 24 + (diff.seconds / 3600), 2)
# Updated room if needed (pk changed -- otherwise, no need for an additional database lookup)
new_room_id = validated_data.get('room')["pk"]
if instance.room is None or instance.room.pk != new_room_id:
instance.room = get_object_or_404(Room, pk=new_room_id)
instance.save()
return instance
class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet):
class EventsViewSet(EventSlugMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
"""
API view: Update scheduling of a slot (event in fullcalendar format)
Write-only (will however reply with written values to PUT request)
"""
permission_classes = (permissions.DjangoModelPermissions,)
serializer_class = EventSerializer
......@@ -112,17 +189,26 @@ class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet):
class ConstraintViolationSerializer(serializers.ModelSerializer):
"""
REST Framework Serializer for constraint violations
"""
class Meta:
model = ConstraintViolation
fields = ['pk', 'type_display', 'aks', 'ak_slots', 'ak_owner', 'room', 'requirement', 'category', 'comment', 'timestamp_display', 'manually_resolved', 'level_display', 'details']
fields = ['pk', 'type_display', 'aks', 'ak_slots', 'ak_owner', 'room', 'requirement', 'category', 'comment',
'timestamp_display', 'manually_resolved', 'level_display', 'details', 'edit_url']
class ConstraintViolationsViewSet(EventSlugMixin, viewsets.ModelViewSet):
class ConstraintViolationsViewSet(EventSlugMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
API View: Constraint Violations of an event
Read-only, fields and model selected in :class:`ConstraintViolationSerializer`
"""
permission_classes = (permissions.DjangoModelPermissions,)
serializer_class = ConstraintViolationSerializer
def get_object(self):
return get_object_or_404(ConstraintViolation, pk=self.kwargs["pk"])
def get_queryset(self):
return ConstraintViolation.objects.filter(event=self.event)
# Optimize query to reduce database load
return (ConstraintViolation.objects.select_related('event', 'room')
.prefetch_related('aks', 'ak_slots', 'ak_owner', 'requirement', 'category')
.filter(event=self.event).order_by('manually_resolved', '-type', '-timestamp'))
......@@ -2,4 +2,7 @@ from django.apps import AppConfig
class AkschedulingConfig(AppConfig):
"""
App configuration (default, only specifies name of the app)
"""
name = 'AKScheduling'
from django import forms
from django.utils.translation import gettext_lazy as _
from AKModel.models import AK
class AKInterestForm(forms.ModelForm):
"""
Form for quickly changing the interest count and notes of an AK
"""
required_css_class = 'required'
class Meta:
model = AK
fields = ['interest',
'notes',
]
class AKAddSlotForm(forms.Form):
"""
Form to create a new slot for an existing AK directly from scheduling view
"""
start = forms.CharField(label=_("Start"), disabled=True)
end = forms.CharField(label=_("End"), disabled=True)
duration = forms.CharField(label=_("Duration"), disabled=True)
room = forms.IntegerField(label=_("Room"), disabled=True, widget=forms.HiddenInput())
room_name = forms.CharField(label=_("Room"), disabled=True)
def __init__(self, event):
super().__init__()
self.fields['ak'] = forms.ModelChoiceField(event.ak_set.all(), label=_("AK"))
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-09 18:23+0000\n"
"POT-Creation-Date: 2024-04-25 00:24+0200\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"
......@@ -17,111 +17,226 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: AKScheduling/forms.py:24
msgid "Start"
msgstr "Start"
#: AKScheduling/forms.py:25
msgid "End"
msgstr "Ende"
#: AKScheduling/forms.py:26
msgid "Duration"
msgstr ""
#: AKScheduling/forms.py:27
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:171
msgid "Room"
msgstr "Raum"
#: AKScheduling/forms.py:31
msgid "AK"
msgstr "AK"
#: AKScheduling/models.py:92
#, python-format
msgid ""
"Not enough space for AK interest (Interest: %(interest)d, Capacity: "
"%(capacity)d)"
msgstr ""
"Nicht genug Platz für AK-Interesse (Interesse: %(interest)d, Kapazität: "
"%(capacity)d)"
#: AKScheduling/models.py:106
#, python-format
msgid ""
"Space is too close to AK interest (Interest: %(interest)d, Capacity: "
"%(capacity)d)"
msgstr ""
"Verfügbarer Platz zu dicht an Interesse (Interesse: %(interest)d, Kapazität: "
"%(capacity)d)"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:11
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:11
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:10
msgid "Scheduling for"
msgstr "Scheduling für"
msgid "Constraint Violations for"
msgstr ""
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:74
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:128
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:44
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:105
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:240
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:375
msgid "No violations"
msgstr "Keine Verletzungen"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:81
msgid "Cannot load current violations from server"
msgstr "Kann die aktuellen Verletzungen nicht vom Server laden"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:106
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:82
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:346
msgid "Violation(s)"
msgstr "Verletzung(en)"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:109
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:85
msgid "Auto reload?"
msgstr "Automatisch neu laden?"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:113
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:89
msgid "Reload now"
msgstr "Jetzt neu laden"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:118
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:95
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:228
msgid "Violation"
msgstr "Verletzung"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:119
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:96
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:369
msgid "Problem"
msgstr "Problem"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:120
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:97
msgid "Details"
msgstr "Details"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:121
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:98
msgid "Since"
msgstr "Seit"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:134
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:243
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:197
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:111
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:256
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:332
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:48
#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34
msgid "Event Status"
msgstr "Event-Status"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:136
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:113
msgid "Scheduling"
msgstr "Scheduling"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:129
#: AKScheduling/templates/admin/AKScheduling/interest.html:32
msgid "Submit"
msgstr "Abschicken"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:11
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:21
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:329
msgid "Scheduling for"
msgstr "Scheduling für"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:135
msgid "Name of new ak track"
msgstr "Name des neuen AK-Tracks"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:145
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:151
msgid "Could not create ak track"
msgstr "Konnte neuen AK-Track nicht anlegen"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:171
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:177
msgid "Could not update ak track name"
msgstr "Konnte Namen des AK-Tracks nicht ändern"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:177
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:183
msgid "Do you really want to delete this ak track?"
msgstr "Soll dieser AK-Track wirklich gelöscht werden?"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:191
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:197
msgid "Could not delete ak track"
msgstr "AK-Track konnte nicht gelöscht werden"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:203
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:216
msgid "Manage AK Tracks"
msgstr "AK-Tracks verwalten"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:204
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:217
msgid "Add ak track"
msgstr "AK-Track hinzufügen"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:209
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:222
msgid "AKs without track"
msgstr "AKs ohne Track"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:87
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:106
msgid "Day (Horizontal)"
msgstr "Tag (horizontal)"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:94
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:113
msgid "Day (Vertical)"
msgstr "Tag (vertikal)"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:105
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:124
msgid "Event (Horizontal)"
msgstr "Event (horizontal)"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:114
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:133
msgid "Event (Vertical)"
msgstr "Event (vertikal)"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:141
msgid "Room"
msgstr "Raum"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:271
msgid "Please choose AK"
msgstr "Bitte AK auswählen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:291
msgid "Could not create slot"
msgstr "Konnte Slot nicht anlegen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:307
msgid "Add slot"
msgstr "Slot hinzufügen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:315
msgid "Add"
msgstr "Hinzufügen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:316
msgid "Cancel"
msgstr "Abbrechen"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:343
msgid "Unscheduled"
msgstr "Nicht gescheduled"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:368
msgid "Level"
msgstr "Level"
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:14
msgid "AKs with public notes"
msgstr "AKs mit öffentlichen Kommentaren"
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:21
msgid "AKs without availabilities"
msgstr "AKs ohne Verfügbarkeiten"
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:28
msgid "Create default availabilities"
msgstr "Standardverfügbarkeiten anlegen"
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:31
msgid "AK wishes with slots"
msgstr "AK-Wünsche mit Slots"
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:38
msgid "Delete slots for wishes"
msgstr ""
#: AKScheduling/templates/admin/AKScheduling/special_attention.html:40
msgid "AKs without slots"
msgstr "AKs ohne Slots"
#: AKScheduling/templates/admin/AKScheduling/status/cvs.html:6
msgid ""
"\n"
" <h3>Constraint Violation</h3>\n"
" "
msgid_plural ""
"\n"
" <h3>Constraint Violations</h3>\n"
" "
msgstr[0] ""
"\n"
" <h3>Constraintverletzung</h3>\n"
" "
msgstr[1] ""
"\n"
" <h3>Constraintverletzungen</h3>\n"
" "
#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:7
msgid "Unscheduled AK Slots"
......@@ -130,3 +245,68 @@ msgstr "Noch nicht geschedulte AK-Slots"
#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:11
msgid "Count"
msgstr "Anzahl"
#: AKScheduling/views.py:150
msgid "Interest updated"
msgstr "Interesse aktualisiert"
#: AKScheduling/views.py:201
msgid "Wishes"
msgstr "Wünsche"
#: AKScheduling/views.py:219
msgid "Cleanup: Delete unscheduled slots for wishes"
msgstr "Aufräumen: Noch nicht geplante Slots für Wünsche löschen"
#: AKScheduling/views.py:226
#, python-brace-format
msgid ""
"The following {count} unscheduled slots of wishes will be deleted:\n"
"\n"
" {slots}"
msgstr ""
"Die folgenden {count} noch nicht geplanten Slots von Wünschen werden "
"gelöscht:\n"
"\n"
" {slots}"
#: AKScheduling/views.py:233
msgid "Unscheduled slots for wishes successfully deleted"
msgstr "Noch nicht geplante Slots für Wünsche erfolgreich gelöscht"
#: AKScheduling/views.py:247
msgid "Create default availabilities for AKs"
msgstr "Standardverfügbarkeiten für AKs anlegen"
#: AKScheduling/views.py:254
#, python-brace-format
msgid ""
"The following {count} AKs don't have any availability information. Create "
"default availability for them:\n"
"\n"
" {aks}"
msgstr ""
"Die folgenden {count} AKs haben keine Verfügbarkeitsinformationen. "
"Standardverfügbarkeiten für sie anlegen:\n"
"\n"
" {aks}"
#: AKScheduling/views.py:274
#, python-brace-format
msgid "Could not create default availabilities for AK: {ak}"
msgstr "Konnte keine Verfügbarkeit anlegen für AK: {ak}"
#: AKScheduling/views.py:279
#, python-brace-format
msgid "Created default availabilities for {count} AKs"
msgstr "Standardverfügbarkeiten für {count} AKs angelegt"
#: AKScheduling/views.py:290
msgid "Constraint Violations"
msgstr "Constraintverletzungen"
#~ msgid "Bitte AK auswählen"
#~ msgstr "Please sel"
#~ msgid "Cannot load current violations from server"
#~ msgstr "Kann die aktuellen Verletzungen nicht vom Server laden"