Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • koma/feature/preference-polling-form
  • komasolver
  • main
  • renovate/django_csp-4.x
4 results

Target

Select target project
  • konstantin/akplanning
  • matedealer/akplanning
  • kif/akplanning
  • mirco/akplanning
  • lordofthevoid/akplanning
  • voidptr/akplanning
  • xayomer/akplanning-fork
  • mollux/akplanning
  • neumantm/akplanning
  • mmarx/akplanning
  • nerf/akplanning
  • felix_bonn/akplanning
  • sebastian.uschmann/akplanning
13 results
Select Git revision
  • ak-import
  • feature/clear-schedule-button
  • feature/export-filtering
  • feature/json-export-via-rest-framework
  • feature/json-schedule-import-tests
  • feature/preference-polling-form
  • fix/add-room-import-only-once
  • fix/responsive-cols-in-polls
  • main
  • renovate/django-5.x
  • renovate/django-debug-toolbar-4.x
  • renovate/django-simple-history-3.x
  • renovate/mysqlclient-2.x
13 results
Show changes
Showing
with 444 additions and 95 deletions
# Create your views here. from django.contrib import messages
from django.http import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
from AKModel.metaviews import status_manager
from AKModel.metaviews.status import TemplateStatusWidget
from AKModel.views.room import RoomCreationView
from AKOnline.forms import RoomWithVirtualForm
class RoomCreationWithVirtualView(RoomCreationView):
"""
View to create both rooms and optionally virtual rooms by filling one form
"""
form_class = RoomWithVirtualForm
template_name = 'admin/AKOnline/room_create_with_virtual.html'
room = None
def form_valid(self, form):
# This will create the room and additionally a virtual room if the url field is not blank
# objects['room'] will always a room instance afterwards, objects['virtual'] may be empty
objects = form.save()
self.room = objects['room']
# Create a (translated) success message containing information about the created room
messages.success(self.request, _("Created Room '%(room)s'" % {'room': objects['room']})) #pylint: disable=consider-using-f-string, line-too-long
if objects['virtual'] is not None:
# Create a (translated) success message containing information about the created virtual room
messages.success(self.request, _("Created related Virtual Room '%(vroom)s'" % {'vroom': objects['virtual']})) #pylint: disable=consider-using-f-string, line-too-long
return HttpResponseRedirect(self.get_success_url())
@status_manager.register(name="event_virtual_rooms")
class EventVirtualRoomsWidget(TemplateStatusWidget):
"""
Status page widget to contain information about all virtual rooms belonging to the given event
"""
required_context_type = "event"
title = _("Virtual Rooms")
template_name = "admin/AKOnline/status/event_virtual_rooms.html"
# Register your models here.
...@@ -2,4 +2,7 @@ from django.apps import AppConfig ...@@ -2,4 +2,7 @@ from django.apps import AppConfig
class AkplanConfig(AppConfig): class AkplanConfig(AppConfig):
"""
App configuration (default, only specifies name of the app)
"""
name = 'AKPlan' name = 'AKPlan'
...@@ -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: 2022-05-22 08:02+0000\n" "POT-Creation-Date: 2025-06-16 12:44+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -27,56 +27,76 @@ msgstr "Plan" ...@@ -27,56 +27,76 @@ msgstr "Plan"
msgid "Write to organizers of this event for questions and comments" msgid "Write to organizers of this event for questions and comments"
msgstr "Fragen oder Kommentare? Schreib den Orgas dieses Events eine Mail" msgstr "Fragen oder Kommentare? Schreib den Orgas dieses Events eine Mail"
#: AKPlan/templates/AKPlan/plan_index.html:32 #: AKPlan/templates/AKPlan/plan_index.html:36
msgid "Day" msgid "Day"
msgstr "Tag" msgstr "Tag"
#: AKPlan/templates/AKPlan/plan_index.html:42 #: AKPlan/templates/AKPlan/plan_index.html:46
msgid "Event" msgid "Event"
msgstr "Veranstaltung" msgstr "Veranstaltung"
#: AKPlan/templates/AKPlan/plan_index.html:55 #: 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:52 #: AKPlan/templates/AKPlan/plan_wall.html:67
msgid "Room" msgid "Room"
msgstr "Raum" msgstr "Raum"
#: AKPlan/templates/AKPlan/plan_index.html:76 #: AKPlan/templates/AKPlan/plan_index.html:120
#: AKPlan/templates/AKPlan/plan_room.html:11 #: AKPlan/templates/AKPlan/plan_room.html:11
#: AKPlan/templates/AKPlan/plan_track.html:9 #: AKPlan/templates/AKPlan/plan_track.html:9
msgid "AK Plan" msgid "AK Plan"
msgstr "AK-Plan" msgstr "AK-Plan"
#: AKPlan/templates/AKPlan/plan_index.html:88 #: AKPlan/templates/AKPlan/plan_index.html:134
#: AKPlan/templates/AKPlan/plan_room.html:49 #: AKPlan/templates/AKPlan/plan_room.html:49
msgid "Rooms" msgid "Rooms"
msgstr "Räume" msgstr "Räume"
#: AKPlan/templates/AKPlan/plan_index.html:101 #: AKPlan/templates/AKPlan/plan_index.html:147
#: AKPlan/templates/AKPlan/plan_track.html:36 #: AKPlan/templates/AKPlan/plan_track.html:36
msgid "Tracks" msgid "Tracks"
msgstr "Tracks" msgstr "Tracks"
#: AKPlan/templates/AKPlan/plan_index.html:113 #: AKPlan/templates/AKPlan/plan_index.html:159
msgid "AK Wall" msgid "AK Wall"
msgstr "AK-Wall" msgstr "AK-Wall"
#: AKPlan/templates/AKPlan/plan_index.html:126 #: AKPlan/templates/AKPlan/plan_index.html:165
#: AKPlan/templates/AKPlan/plan_wall.html:116 msgid "Plan:"
msgstr "Plan:"
#: AKPlan/templates/AKPlan/plan_index.html:171
msgid "Filter by types"
msgstr "Nach Typen filtern"
#: AKPlan/templates/AKPlan/plan_index.html:174
msgid "Types:"
msgstr "Typen:"
#: AKPlan/templates/AKPlan/plan_index.html:182
msgid "AKs without type"
msgstr "AKs ohne Typ"
#: AKPlan/templates/AKPlan/plan_index.html:186
msgid "No AKs with additional other types"
msgstr "Keine AKs, die noch zusätzlich andere Typen haben"
#: AKPlan/templates/AKPlan/plan_index.html:198
#: AKPlan/templates/AKPlan/plan_wall.html:132
msgid "Current AKs" msgid "Current AKs"
msgstr "Aktuelle AKs" msgstr "Aktuelle AKs"
#: AKPlan/templates/AKPlan/plan_index.html:133 #: AKPlan/templates/AKPlan/plan_index.html:205
#: AKPlan/templates/AKPlan/plan_wall.html:121 #: AKPlan/templates/AKPlan/plan_wall.html:137
msgid "Next AKs" msgid "Next AKs"
msgstr "Nächste AKs" msgstr "Nächste AKs"
#: AKPlan/templates/AKPlan/plan_index.html:141 #: AKPlan/templates/AKPlan/plan_index.html:213
msgid "This event is not active." msgid "This event is not active."
msgstr "Dieses Event ist nicht aktiv." msgstr "Dieses Event ist nicht aktiv."
#: AKPlan/templates/AKPlan/plan_index.html:154 #: AKPlan/templates/AKPlan/plan_index.html:226
#: AKPlan/templates/AKPlan/plan_room.html:77 #: AKPlan/templates/AKPlan/plan_room.html:77
#: AKPlan/templates/AKPlan/plan_track.html:58 #: AKPlan/templates/AKPlan/plan_track.html:58
msgid "Plan is not visible (yet)." msgid "Plan is not visible (yet)."
...@@ -99,10 +119,14 @@ msgstr "Eigenschaften" ...@@ -99,10 +119,14 @@ msgstr "Eigenschaften"
msgid "Track" msgid "Track"
msgstr "Track" msgstr "Track"
#: AKPlan/templates/AKPlan/plan_wall.html:131 #: AKPlan/templates/AKPlan/plan_wall.html:147
msgid "Reload page automatically?" msgid "Reload page automatically?"
msgstr "Seite automatisch neu laden?" msgstr "Seite automatisch neu laden?"
#: AKPlan/templates/AKPlan/slots_table.html:14 #: AKPlan/templates/AKPlan/slots_table.html:14
msgid "No AKs" msgid "No AKs"
msgstr "Keine AKs" msgstr "Keine AKs"
#: AKPlan/views.py:64
msgid "Invalid type filter"
msgstr "Ungültiger Typ-Filter"
# Create your models here.
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
'resourceId': '{{ slot.room.title }}', 'resourceId': '{{ slot.room.title }}',
'backgroundColor': '{{ slot|highlight_change_colors }}', 'backgroundColor': '{{ slot|highlight_change_colors }}',
'borderColor': '{{ slot.ak.category.color }}', 'borderColor': '{{ slot.ak.category.color }}',
'url': '{% url 'submit:ak_detail' event_slug=event.slug pk=slot.ak.pk %}' 'url': '{{ slot.ak.detail_url }}'
}, },
{% endif %} {% endif %}
{% endfor %} {% endfor %}
......
...@@ -20,7 +20,11 @@ ...@@ -20,7 +20,11 @@
// No header, not buttons // No header, not buttons
headerToolbar: false, headerToolbar: false,
aspectRatio: 2.5, aspectRatio: 2.5,
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 // Only show calendar view for the dates of the connected event
visibleRange: { visibleRange: {
start: '{{ ak.event.start | timezone:ak.event.timezone | date:"Y-m-d H:i:s" }}', start: '{{ ak.event.start | timezone:ak.event.timezone | date:"Y-m-d H:i:s" }}',
......
{% extends "base.html" %} {% extends "base.html" %}
{% load fontawesome_5 %} {% load fontawesome_6 %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
{% block footer_custom %} {% block footer_custom %}
{% if event.contact_email %} {% if event.contact_email %}
<h4> <h4>
<a href="mailto:{{ event.contact_email }}">{% fa5_icon "envelope" "far" %} {% trans "Write to organizers of this event for questions and comments" %}</a> <a href="mailto:{{ event.contact_email }}">{% fa6_icon "envelope" "far" %} {% trans "Write to organizers of this event for questions and comments" %}</a>
</h4> </h4>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% extends "AKPlan/plan_base.html" %} {% extends "AKPlan/plan_base.html" %}
{% load fontawesome_5 %} {% load fontawesome_6 %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load tz %} {% load tz %}
...@@ -28,7 +28,11 @@ ...@@ -28,7 +28,11 @@
right: '' right: ''
}, },
aspectRatio: 2, 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 // Only show calendar view for the dates of the connected event
visibleRange: { visibleRange: {
start: '{{ event.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}', start: '{{ event.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
......
{% extends "AKPlan/plan_base.html" %} {% extends "AKPlan/plan_base.html" %}
{% load fontawesome_5 %} {% load fontawesome_6 %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load tz %} {% load tz %}
...@@ -22,7 +22,11 @@ ...@@ -22,7 +22,11 @@
center: 'title', center: 'title',
right: 'resourceTimelineDay,resourceTimelineEvent' 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 // Adapt to user selected locale
locale: '{{ LANGUAGE_CODE }}', locale: '{{ LANGUAGE_CODE }}',
initialView: 'resourceTimelineEvent', initialView: 'resourceTimelineEvent',
...@@ -66,6 +70,46 @@ ...@@ -66,6 +70,46 @@
} }
}); });
</script> </script>
{% if type_filtering_active %}
{# Type filter script #}
<script type="module">
const { createApp } = Vue
createApp({
delimiters: ["[[", "]]"],
data() {
return {
types: JSON.parse("{{ types | escapejs }}"),
strict: {{ types_filter_strict|yesno:"true,false" }},
empty: {{ types_filter_empty|yesno:"true,false" }}
}
},
methods: {
onFilterChange(type) {
// Re-generate filter url
const typeString = "types="
+ this.types
.map(t => `${t.slug}:${t.state ? 'yes' : 'no'}`)
.join(',')
+ `&strict=${this.strict ? 'yes' : 'no'}`
+ `&empty=${this.empty ? 'yes' : 'no'}`;
// Redirect to the new url including the adjusted filters
const baseUrl = window.location.origin + window.location.pathname;
window.location.href = `${baseUrl}?${typeString}`;
}
}
}).mount('#filter');
// Prevent showing of cached form values for filter inputs when using broswer navigation
window.addEventListener('pageshow', function(event) {
if (event.persisted) {
window.location.reload();
}
});
</script>
{% endif %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
...@@ -79,11 +123,13 @@ ...@@ -79,11 +123,13 @@
{% block content %} {% block content %}
<div class="float-right"> {% include "messages.html" %}
<div class="float-end">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
{% if rooms|length > 0 %} {% if rooms|length > 0 %}
<li class="nav-item dropdown"> <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-haspopup="true"
aria-expanded="false">{% trans "Rooms" %}</a> aria-expanded="false">{% trans "Rooms" %}</a>
<div class="dropdown-menu" style=""> <div class="dropdown-menu" style="">
...@@ -96,7 +142,7 @@ ...@@ -96,7 +142,7 @@
{% endif %} {% endif %}
{% if tracks|length > 0 %} {% if tracks|length > 0 %}
<li class="nav-item dropdown"> <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-haspopup="true"
aria-expanded="false">{% trans "Tracks" %}</a> aria-expanded="false">{% trans "Tracks" %}</a>
<div class="dropdown-menu"> <div class="dropdown-menu">
...@@ -110,13 +156,39 @@ ...@@ -110,13 +156,39 @@
{% if event.active %} {% if event.active %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" <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 %}?{{ query_string }}">{% fa6_icon 'desktop' 'fas' %}&nbsp;&nbsp;{% trans "AK Wall" %}</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
<h1>Plan: {{ event }}</h1> <h1 class="mb-3">{% trans "Plan:" %} {{ event }}</h1>
{% if type_filtering_active %}
{# Type filter HTML #}
<div class="card border-primary mb-3">
<div class="card-header">
{% trans 'Filter by types' %}
</div>
<div class="card-body d-flex" id="filter">
{% trans "Types:" %}
<div id="filterTypes" class="d-flex">
<div class="form-check form-switch ms-3" v-for="type in types">
<label class="form-check-label" :for="'typeFilterType' + type.slug">[[ type.name ]]</label>
<input class="form-check-input" type="checkbox" :id="'typeFilterType' + type.slug " v-model="type.state" @change="onFilterChange()">
</div>
</div>
<div class="form-check form-switch ms-5">
<label class="form-check-label" for="typeFilterEmpty">{% trans "AKs without type" %}</label>
<input class="form-check-input" type="checkbox" id="typeFilterEmpty" v-model="empty" @change="onFilterChange()">
</div>
<div class="form-check form-switch ms-5">
<label class="form-check-label" for="typeFilterStrict">{% trans "No AKs with additional other types" %}</label>
<input class="form-check-input" type="checkbox" id="typeFilterStrict" v-model="strict" @change="onFilterChange()">>
</div>
</div>
</div>
{% endif %}
{% timezone event.timezone %} {% timezone event.timezone %}
<div class="row" style="margin-top:30px;"> <div class="row" style="margin-top:30px;">
......
{% extends "AKPlan/plan_detail.html" %} {% extends "AKPlan/plan_detail.html" %}
{% load fontawesome_5 %} {% load fontawesome_6 %}
{% load tags_AKModel %} {% load tags_AKModel %}
{% load tz %} {% load tz %}
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
{'title': '{{ slot.ak }}', {'title': '{{ slot.ak }}',
'start': '{{ slot.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}', '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" }}', '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 }}', 'borderColor': '{{ slot.ak.track.color }}',
'color': '{{ slot.ak.category.color }}', 'color': '{{ slot.ak.category.color }}',
}, },
...@@ -43,10 +43,10 @@ ...@@ -43,10 +43,10 @@
{% block content %} {% block content %}
<div class="float-right"> <div class="float-end">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li class="nav-item dropdown"> <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=""> <div class="dropdown-menu" style="">
{% for r in event.room_set.all %} {% 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> <a class="dropdown-item" href="{% url "plan:plan_room" event_slug=event.slug pk=r.pk %}">{{ r }}</a>
...@@ -58,9 +58,9 @@ ...@@ -58,9 +58,9 @@
<h1>{% trans "Room" %}: {{ room.name }} {% if room.location != '' %}({{ room.location }}){% endif %}</h1> <h1>{% trans "Room" %}: {{ room.name }} {% if room.location != '' %}({{ room.location }}){% endif %}</h1>
{% if "AKOnline"|check_app_installed and room.virtualroom and room.virtualroom.url != '' %} {% if "AKOnline"|check_app_installed and room.virtual and room.virtual.url != '' %}
<a class="btn btn-success" target="_parent" href="{{ room.virtualroom.url }}"> <a class="btn btn-success" target="_parent" href="{{ room.virtual.url }}">
{% fa5_icon 'external-link-alt' 'fas' %} {% trans "Go to virtual room" %} {% fa6_icon 'external-link-alt' 'fas' %} {% trans "Go to virtual room" %}
</a> </a>
{% endif %} {% endif %}
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
{'title': '{{ slot.ak }} @ {{ slot.room }}', {'title': '{{ slot.ak }} @ {{ slot.room }}',
'start': '{{ slot.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}', '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" }}', '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 }}', 'color': '{{ track.color }}',
'borderColor': '{{ slot.ak.category.color }}', 'borderColor': '{{ slot.ak.category.color }}',
}, },
...@@ -30,10 +30,10 @@ ...@@ -30,10 +30,10 @@
{% block content %} {% block content %}
<div class="float-right"> <div class="float-end">
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li class="nav-item dropdown"> <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"> <div class="dropdown-menu">
{% for t in event.aktrack_set.all %} {% 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> <a class="dropdown-item" href="{% url "plan:plan_track" event_slug=event.slug pk=t.pk %}">{{ t }}</a>
......
{% load compress %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load bootstrap4 %} {% load django_bootstrap5 %}
{% load fontawesome_5 %} {% load fontawesome_6 %}
{% load tags_AKModel %} {% load tags_AKModel %}
{% load tags_AKPlan %} {% load tags_AKPlan %}
{% load tz %} {% load tz %}
...@@ -14,11 +15,17 @@ ...@@ -14,11 +15,17 @@
<title>{% block title %}AK Planning{% endblock %}</title> <title>{% block title %}AK Planning{% endblock %}</title>
{# Load Bootstrap CSS and JavaScript as well as font awesome #} {# Load Bootstrap CSS and JavaScript as well as font awesome #}
{% bootstrap_css %} {% compress css %}
{% bootstrap_javascript jquery='slim' %} <link rel="stylesheet" type="text/x-scss" href="{% static 'common/vendor/bootswatch-lumen/theme.scss' %}">
{% fontawesome_5_static %} {% fontawesome_6_css %}
<link rel="stylesheet" href="{% static 'common/css/custom.css' %}">
<link rel="stylesheet" href="{% static 'common/css/custom.css' %}"> {% endcompress %}
{% compress js %}
{% bootstrap_javascript %}
<script src="{% static 'common/vendor/jquery/jquery-3.6.3.min.js' %}"></script>
{% fontawesome_6_js %}
{% endcompress %}
{% include "AKModel/load_fullcalendar.html" %} {% include "AKModel/load_fullcalendar.html" %}
...@@ -31,7 +38,11 @@ ...@@ -31,7 +38,11 @@
var plan = new FullCalendar.Calendar(planEl, { var plan = new FullCalendar.Calendar(planEl, {
timeZone: '{{ event.timezone }}', timeZone: '{{ event.timezone }}',
headerToolbar: false, 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 // Adapt to user selected locale
locale: '{{ LANGUAGE_CODE }}', locale: '{{ LANGUAGE_CODE }}',
slotDuration: '01:00', slotDuration: '01:00',
...@@ -40,6 +51,8 @@ ...@@ -40,6 +51,8 @@
start: '{{ start | timezone:event.timezone | date:"Y-m-d H:i:s" }}', start: '{{ start | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
end: '{{ end | 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) { eventDidMount: function(info) {
$(info.el).tooltip({title: info.event.extendedProps.description}); $(info.el).tooltip({title: info.event.extendedProps.description});
}, },
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<table class="table table-striped"> <table class="table table-striped">
{% for akslot in slots %} {% for akslot in slots %}
<tr> <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>{{ akslot.start | time:"H:i" }} - {{ akslot.end | time:"H:i" }}</td>
<td class="breakWord">{% if akslot.room and akslot.room.pk != '' %} <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> <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 # 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 Convert hex color to RGB color code
:param hex: hex encoded color :param hex: hex encoded color
...@@ -23,8 +23,7 @@ def rgb_to_hex(rgb): ...@@ -23,8 +23,7 @@ def rgb_to_hex(rgb):
""" """
# Components need to be integers for hex to make sense # Components need to be integers for hex to make sense
rgb = [int(x) for x in rgb] rgb = [int(x) for x in rgb]
return "#"+"".join(["0{0:x}".format(v) if v < 16 else return "#"+"".join([f"0{v:x}" if v < 16 else f"{v:x}" for v in rgb])
"{0:x}".format(v) for v in rgb])
def linear_blend(start_hex, end_hex, position): def linear_blend(start_hex, end_hex, position):
......
...@@ -11,6 +11,14 @@ register = template.Library() ...@@ -11,6 +11,14 @@ register = template.Library()
@register.filter @register.filter
def highlight_change_colors(akslot): 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 # 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 if akslot.event.plan_hidden or (akslot.event.plan_published_at is not None
and akslot.event.plan_published_at > akslot.updated): and akslot.event.plan_published_at > akslot.updated):
...@@ -25,9 +33,14 @@ def highlight_change_colors(akslot): ...@@ -25,9 +33,14 @@ def highlight_change_colors(akslot):
# Recent change? Calculate gradient blend between red and # Recent change? Calculate gradient blend between red and
recentness = seconds_since_update / settings.PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS recentness = seconds_since_update / settings.PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS
return darken("#b71540", recentness) return darken("#b71540", recentness)
# return linear_blend("#b71540", "#000000", recentness)
@register.simple_tag @register.simple_tag
def timestamp_now(tz): 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") return date_format(datetime.now().astimezone(tz), "c")
from django.test import TestCase from django.test import TestCase
from AKModel.tests import BasicViewTests from AKModel.tests.test_views import BasicViewTests
class PlanViewTests(BasicViewTests, TestCase): class PlanViewTests(BasicViewTests, TestCase):
"""
Tests for AKPlan
"""
fixtures = ['model.json'] fixtures = ['model.json']
APP_NAME = 'plan' APP_NAME = 'plan'
...@@ -14,8 +17,11 @@ class PlanViewTests(BasicViewTests, TestCase): ...@@ -14,8 +17,11 @@ class PlanViewTests(BasicViewTests, TestCase):
('plan_track', {'event_slug': 'kif42', 'pk': 1}), ('plan_track', {'event_slug': 'kif42', 'pk': 1}),
] ]
def testPlanHidden(self): def test_plan_hidden(self):
view_name_with_prefix, url = self._name_and_url(('plan_overview', {'event_slug': 'kif23'})) """
Test correct handling of plan visibility
"""
_, url = self._name_and_url(('plan_overview', {'event_slug': 'kif23'}))
self.client.logout() self.client.logout()
response = self.client.get(url) response = self.client.get(url)
...@@ -26,3 +32,14 @@ class PlanViewTests(BasicViewTests, TestCase): ...@@ -26,3 +32,14 @@ class PlanViewTests(BasicViewTests, TestCase):
response = self.client.get(url) response = self.client.get(url)
self.assertNotContains(response, "Plan is not visible (yet).", self.assertNotContains(response, "Plan is not visible (yet).",
msg_prefix="Plan is not visible for staff user") 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 import json
from datetime import datetime, timedelta
from django.conf import settings from django.conf import settings
from django.contrib import messages
from django.db.models import Q
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.datetime_safe import datetime from django.views.generic import DetailView, ListView
from django.views.generic import ListView, DetailView from django.utils.translation import gettext_lazy as _
from AKModel.models import AKSlot, Room, AKTrack from AKModel.metaviews.admin import FilterByEventSlugMixin
from AKModel.views import FilterByEventSlugMixin from AKModel.models import AKSlot, AKTrack, Room, AKType
class PlanIndexView(FilterByEventSlugMixin, ListView): class PlanIndexView(FilterByEventSlugMixin, ListView):
"""
Default plan view
Shows two lists of current and upcoming AKs and a graphical full plan below
"""
model = AKSlot model = AKSlot
template_name = "AKPlan/plan_index.html" template_name = "AKPlan/plan_index.html"
context_object_name = "akslots" context_object_name = "akslots"
ordering = "start" ordering = "start"
types_filter = None
query_string = ""
def get(self, request, *args, **kwargs):
if 'types' in request.GET:
try:
# Initialize types filter, has to be done here such that it is not reused across requests
self.types_filter = {
"yes": [],
"no": [],
"no_set": set(),
"strict": False,
"empty": False,
}
# If types are given, filter the queryset accordingly
types_raw = request.GET['types'].split(',')
for t in types_raw:
type_slug, type_condition = t.split(':')
if type_condition in ["yes", "no"]:
t = AKType.objects.get(slug=type_slug, event=self.event)
self.types_filter[type_condition].append(t)
if type_condition == "no":
# Store slugs of excluded types in a set for faster lookup
self.types_filter["no_set"].add(t.slug)
else:
raise ValueError(f"Unknown type condition: {type_condition}")
if 'strict' in request.GET:
# If strict is specified and marked as "yes",
# exclude all AKs that have any of the excluded types ("no"),
# even if they have other types that are marked as "yes"
self.types_filter["strict"] = request.GET.get('strict') == 'yes'
if 'empty' in request.GET:
# If empty is specified and marked as "yes", include AKs that have no types at all
self.types_filter["empty"] = request.GET.get('empty') == 'yes'
# Will be used for generating a link to the wall view with the same filter
self.query_string = request.GET.urlencode(safe=",:")
except (ValueError, AKType.DoesNotExist):
# Display an error message if the types parameter is malformed
messages.add_message(request, messages.ERROR, _("Invalid type filter"))
self.types_filter = None
s = super().get(request, *args, **kwargs)
return s
def get_queryset(self): def get_queryset(self):
# Ignore slots not scheduled yet # Ignore slots not scheduled yet
return super().get_queryset().filter(start__isnull=False).select_related('ak', 'room', 'ak__category') qs = (super().get_queryset().filter(start__isnull=False).
select_related('event', 'ak', 'room', 'ak__category', 'ak__event'))
# Need to prefetch both event and ak__event
# since django is not aware that the two are always the same
# Apply type filter if necessary
if self.types_filter:
# Either include all AKs with the given types or without any types at all
if self.types_filter["empty"]:
qs = qs.filter(Q(ak__types__in=self.types_filter["yes"]) | Q(ak__types__isnull=True)).distinct()
# Or only those with the given types
else:
qs = qs.filter(ak__types__in=self.types_filter["yes"]).distinct()
# Afterwards, if strict, exclude all AKs that have any of the excluded types,
# even though they were included by the previous filter
if self.types_filter["strict"]:
qs = qs.exclude(ak__types__in=self.types_filter["no"]).distinct()
return qs
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs) context = super().get_context_data(object_list=object_list, **kwargs)
...@@ -34,6 +101,7 @@ class PlanIndexView(FilterByEventSlugMixin, ListView): ...@@ -34,6 +101,7 @@ class PlanIndexView(FilterByEventSlugMixin, ListView):
# Get list of current and next slots # Get list of current and next slots
for akslot in context["akslots"]: for akslot in context["akslots"]:
self._process_slot(akslot)
# Construct a list of all rooms used by these slots on the fly # Construct a list of all rooms used by these slots on the fly
if akslot.room is not None: if akslot.room is not None:
rooms.add(akslot.room) rooms.add(akslot.room)
...@@ -56,10 +124,49 @@ class PlanIndexView(FilterByEventSlugMixin, ListView): ...@@ -56,10 +124,49 @@ class PlanIndexView(FilterByEventSlugMixin, ListView):
context["tracks"] = self.event.aktrack_set.all() context["tracks"] = self.event.aktrack_set.all()
# Pass query string to template for generating a matching wall link
context["query_string"] = self.query_string
# Generate a list of all types and their current selection state for graphic filtering
types = [{"name": t.name, "slug": t.slug, "state": True} for t in self.event.aktype_set.all()]
if len(types) > 0:
context["type_filtering_active"] = True
if self.types_filter:
for t in types:
if t["slug"] in self.types_filter["no_set"]:
t["state"] = False
# Pass type list as well as filter state for strict filtering and empty types to the template
context["types"] = json.dumps(types)
context["types_filter_strict"] = False
context["types_filter_empty"] = False
if self.types_filter:
context["types_filter_empty"] = self.types_filter["empty"]
context["types_filter_strict"] = self.types_filter["strict"]#
else:
context["type_filtering_active"] = False
return context return context
def _process_slot(self, akslot):
"""
Function to be called for each slot when looping over the slots
(meant to be overridden in inherited views)
:param akslot: current slot
:type akslot: AKSlot
"""
class PlanScreenView(PlanIndexView): 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" template_name = "AKPlan/plan_wall.html"
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
...@@ -69,11 +176,12 @@ class PlanScreenView(PlanIndexView): ...@@ -69,11 +176,12 @@ class PlanScreenView(PlanIndexView):
return redirect(reverse_lazy("plan:plan_overview", kwargs={"event_slug": self.event.slug})) return redirect(reverse_lazy("plan:plan_overview", kwargs={"event_slug": self.event.slug}))
return s return s
""" # pylint: disable=attribute-defined-outside-init
def get_queryset(self): 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) 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: 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) self.start = now - timedelta(hours=settings.PLAN_WALL_HOURS_RETROSPECT)
else: else:
self.start = self.event.start self.start = self.event.start
...@@ -82,32 +190,61 @@ class PlanScreenView(PlanIndexView): ...@@ -82,32 +190,61 @@ class PlanScreenView(PlanIndexView):
# Restrict AK slots to relevant ones # Restrict AK slots to relevant ones
# This will automatically filter all rooms not needed for the selected range in the orginal get_context method # 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) return super().get_queryset().filter(start__gt=self.start)
"""
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
# Find the earliest hour AKs start and end (handle 00:00 as 24:00)
self.earliest_start_hour = 23
self.latest_end_hour = 1
context = super().get_context_data(object_list=object_list, **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["end"] = self.event.end
context["earliest_start_hour"] = self.earliest_start_hour
context["latest_end_hour"] = self.latest_end_hour
return context return context
def _process_slot(self, akslot):
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)
class PlanRoomView(FilterByEventSlugMixin, DetailView): class PlanRoomView(FilterByEventSlugMixin, DetailView):
"""
Plan view for a single room
"""
template_name = "AKPlan/plan_room.html" template_name = "AKPlan/plan_room.html"
model = Room model = Room
context_object_name = "room" context_object_name = "room"
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **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') context["slots"] = AKSlot.objects.filter(room=context['room']).select_related('ak', 'ak__category', 'ak__track')
return context return context
class PlanTrackView(FilterByEventSlugMixin, DetailView): class PlanTrackView(FilterByEventSlugMixin, DetailView):
"""
Plan view for a single track
"""
template_name = "AKPlan/plan_track.html" template_name = "AKPlan/plan_track.html"
model = AKTrack model = AKTrack
context_object_name = "track" context_object_name = "track"
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs) context = super().get_context_data(object_list=object_list, **kwargs)
context["slots"] = AKSlot.objects.filter(event=self.event, ak__track=context['track']).select_related('ak', 'room', 'ak__category') # 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 return context
...@@ -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: 2021-10-29 13:36+0000\n" "POT-Creation-Date: 2023-08-16 16:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -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:144 #: AKPlanning/settings.py:148
msgid "German" msgid "German"
msgstr "Deutsch" msgstr "Deutsch"
#: AKPlanning/settings.py:145 #: AKPlanning/settings.py:149
msgid "English" msgid "English"
msgstr "Englisch" msgstr "Englisch"
...@@ -10,6 +10,7 @@ For the full list of settings and their values, see ...@@ -10,6 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/ https://docs.djangoproject.com/en/2.2/ref/settings/
""" """
import decimal
import os import os
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
...@@ -38,6 +39,8 @@ INSTALLED_APPS = [ ...@@ -38,6 +39,8 @@ INSTALLED_APPS = [
'AKScheduling.apps.AkschedulingConfig', 'AKScheduling.apps.AkschedulingConfig',
'AKPlan.apps.AkplanConfig', 'AKPlan.apps.AkplanConfig',
'AKOnline.apps.AkonlineConfig', 'AKOnline.apps.AkonlineConfig',
'AKPreference.apps.AkpreferenceConfig',
'AKSolverInterface.apps.AksolverinterfaceConfig',
'AKModel.apps.AKAdminConfig', 'AKModel.apps.AKAdminConfig',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
...@@ -45,17 +48,20 @@ INSTALLED_APPS = [ ...@@ -45,17 +48,20 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'debug_toolbar', 'debug_toolbar',
'bootstrap4', 'django_bootstrap5',
'fontawesome_5', 'fontawesomefree',
'fontawesome_6',
'timezone_field', 'timezone_field',
'rest_framework', 'rest_framework',
'simple_history', 'simple_history',
'registration', 'registration',
'bootstrap_datepicker_plus',
'django_tex', 'django_tex',
'compressor',
'docs',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.gzip.GZipMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
...@@ -138,8 +144,6 @@ TIME_ZONE = 'UTC' ...@@ -138,8 +144,6 @@ TIME_ZONE = 'UTC'
USE_I18N = True USE_I18N = True
USE_L10N = True
USE_TZ = True USE_TZ = True
LANGUAGES = [ LANGUAGES = [
...@@ -161,29 +165,38 @@ STATICFILES_DIRS = ( ...@@ -161,29 +165,38 @@ STATICFILES_DIRS = (
'static_common', 'static_common',
) )
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
)
# Settings for Bootstrap # Settings for Bootstrap
BOOTSTRAP4 = { BOOTSTRAP5 = {
# Use custom CSS
"css_url": {
"href": STATIC_URL + "common/css/bootstrap.css",
},
"javascript_url": { "javascript_url": {
"url": STATIC_URL + "common/vendor/bootstrap/bootstrap-4.3.1.min.js", "url": STATIC_URL + "common/vendor/bootstrap/bootstrap-5.0.2.bundle.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",
}, },
} }
# Settings for FontAwesome # Settings for FontAwesome
FONTAWESOME_5_CSS_URL = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.css" FONTAWESOME_6_CSS_URL = STATIC_URL + "fontawesomefree/css/all.min.css"
FONTAWESOME_5_PREFIX = "fa" 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? # Treat wishes as seperate category in submission views?
WISHES_AS_CATEGORY = True WISHES_AS_CATEGORY = True
...@@ -207,6 +220,15 @@ PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS = 2 * 60 * 60 ...@@ -207,6 +220,15 @@ PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS = 2 * 60 * 60
DASHBOARD_SHOW_RECENT = True DASHBOARD_SHOW_RECENT = True
# How many entries max? # How many entries max?
DASHBOARD_RECENT_MAX = 25 DASHBOARD_RECENT_MAX = 25
# How many events should be featured in the dashboard
# (active events will always be featured, even if their number is higher than this threshold)
DASHBOARD_MAX_FEATURED_EVENTS = 3
# In the export to the solver we need to calculate the integer number
# of discrete time slots covered by an AK. This is done by rounding
# the 'exact' number up. To avoid 'overshooting' by 1
# due to FLOP inaccuracies, we subtract this small epsilon before rounding.
EXPORT_CEIL_OFFSET_EPS = decimal.Decimal(1e-4)
# Registration/login behavior # Registration/login behavior
SIMPLE_BACKEND_REDIRECT_URL = "/user/" SIMPLE_BACKEND_REDIRECT_URL = "/user/"
...@@ -214,7 +236,7 @@ LOGIN_REDIRECT_URL = SIMPLE_BACKEND_REDIRECT_URL ...@@ -214,7 +236,7 @@ LOGIN_REDIRECT_URL = SIMPLE_BACKEND_REDIRECT_URL
# Content Security Policy # Content Security Policy
CSP_DEFAULT_SRC = ("'self'",) 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_STYLE_SRC = ("'self'", "'unsafe-inline'", "fonts.googleapis.com")
CSP_IMG_SRC = ("'self'", "data:") CSP_IMG_SRC = ("'self'", "data:")
CSP_FRAME_SRC = ("'self'", ) CSP_FRAME_SRC = ("'self'", )
...@@ -224,4 +246,9 @@ CSP_FONT_SRC = ("'self'", "data:", "fonts.gstatic.com") ...@@ -224,4 +246,9 @@ CSP_FONT_SRC = ("'self'", "data:", "fonts.gstatic.com")
SEND_MAILS = True SEND_MAILS = True
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 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")) include(optional("settings/*.py"))