diff --git a/AKPlan/locale/de_DE/LC_MESSAGES/django.po b/AKPlan/locale/de_DE/LC_MESSAGES/django.po new file mode 100644 index 0000000000000000000000000000000000000000..fbb174f50bca5dd2ea9236685ba72f39e014704c --- /dev/null +++ b/AKPlan/locale/de_DE/LC_MESSAGES/django.po @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-03-02 01:08+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: templates/AKPlan/plan_index.html:47 +msgid "Day" +msgstr "Tag" + +#: templates/AKPlan/plan_index.html:57 +msgid "Event" +msgstr "Veranstaltung" + +#: templates/AKPlan/plan_index.html:66 templates/AKPlan/plan_screen.html:64 +msgid "Room" +msgstr "Raum" + +#: templates/AKPlan/plan_index.html:106 +msgid "AK Plan" +msgstr "AK-Plan" + +#: templates/AKPlan/plan_index.html:112 +msgid "screen view" +msgstr "Anzeigeansicht" + +#: templates/AKPlan/plan_index.html:120 templates/AKPlan/plan_screen.html:99 +msgid "Current AKs" +msgstr "Aktuelle AKs" + +#: templates/AKPlan/plan_index.html:127 templates/AKPlan/plan_screen.html:104 +msgid "Next AKs" +msgstr "Nächste AKs" + +#: templates/AKPlan/plan_index.html:144 +msgid "Write to organizers of this event for questions and comments" +msgstr "Fragen oder Kommentare? Schreib den Orgas dieses Events eine Mail" + +#: templates/AKPlan/slots_table.html:12 +msgid "No AKs" +msgstr "Keine AKs" diff --git a/AKPlan/templates/AKPlan/plan_akslot.html b/AKPlan/templates/AKPlan/plan_akslot.html index f8702036bc7b9bf5b6f636323d3ba6a102ba9b68..70514a63ce7665e09dfac113934abf306e88c103 100644 --- a/AKPlan/templates/AKPlan/plan_akslot.html +++ b/AKPlan/templates/AKPlan/plan_akslot.html @@ -25,7 +25,7 @@ document.addEventListener('DOMContentLoaded', function() { // Adapt to timezone of the connected event timeZone: '{{ ak.event.timezone }}', defaultView: 'timeGrid', - // Adapt to user selected loclae + // Adapt to user selected locale locale: '{{ LANGUAGE_CODE }}', // No header, not buttons header: { diff --git a/AKPlan/templates/AKPlan/plan_index.html b/AKPlan/templates/AKPlan/plan_index.html new file mode 100644 index 0000000000000000000000000000000000000000..0559a096e6db43775b0d6a51a023b9389fbf5422 --- /dev/null +++ b/AKPlan/templates/AKPlan/plan_index.html @@ -0,0 +1,147 @@ +{% extends "base.html" %} + +{% load fontawesome %} +{% load i18n %} +{% load tags_AKModel %} +{% load static %} +{% load tz %} + + +{% block imports %} + {% get_current_language as LANGUAGE_CODE %} + + <link href='{% static 'AKPlan/fullcalendar/core/main.css' %}' rel='stylesheet' /> + <link href='{% static 'AKPlan/fullcalendar/timeline/main.css' %}' rel='stylesheet' /> + <link href='{% static 'AKPlan/fullcalendar/resource-timeline/main.css' %}' rel='stylesheet' /> + <link href='{% static 'AKPlan/fullcalendar/resource-timeline/main.min.css' %}' rel='stylesheet' /> + + <script src='{% static 'AKPlan/fullcalendar/core/main.js' %}'></script> + {% with 'AKPlan/fullcalendar/core/locales/'|add:LANGUAGE_CODE|add:'.js' as locale_file %} + <script src="{% static locale_file %}"></script> + {% endwith %} + <script src='{% static 'AKPlan/fullcalendar/timeline/main.js' %}'></script> + <script src='{% static 'AKPlan/fullcalendar/resource-common/main.js' %}'></script> + <script src='{% static 'AKPlan/fullcalendar/resource-timeline/main.js' %}'></script> + <script src='{% static 'AKPlan/fullcalendar/bootstrap/main.js' %}'></script> + + <script> + document.addEventListener('DOMContentLoaded', function() { + var planEl = document.getElementById('planCalendar'); + + var plan = new FullCalendar.Calendar(planEl, { + plugins: ['resourceTimeline', 'bootstrap'], + timeZone: '{{ event.timezone }}', + header: { + left: 'today prev,next', + center: 'title', + right: 'resourceTimelineDay,resourceTimelineEvent' + }, + aspectRatio: 2, + themeSystem: 'bootstrap', + // Adapt to user selected locale + locale: '{{ LANGUAGE_CODE }}', + defaultView: 'resourceTimelineEvent', + views: { + resourceTimelineDay: { + type: 'resourceTimeline', + buttonText: '{% trans "Day" %}', + slotDuration: '01:00', + scrollTime: '08:00', + }, + resourceTimelineEvent: { + type: 'resourceTimeline', + visibleRange: { + start: '{{ event.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}', + end: '{{ event.end | timezone:event.timezone | date:"Y-m-d H:i:s"}}', + }, + buttonText: '{% trans "Event" %}', + } + }, + editable: false, + allDaySlot: false, + nowIndicator: true, + eventTextColor: '#fff', + eventColor: '#127ba3', + resourceAreaWidth: '15%', + resourceLabelText: '{% trans "Room" %}', + resources: [ + {% for room in rooms %} + { + 'id': '{{ room.title }}', + 'title': '{{ room.title }}' + }, + {% endfor %} + ], + events: [ + {% for slot in akslots %} + {% if slot.room and slot.start %} + { + 'title': '{{ slot.ak.name }}', + '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" }}', + 'resourceId': '{{ slot.room.title }}', + }, + {% endif %} + {% endfor %} + ], + schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source', + }); + + plan.render(); + }); + </script> +{% endblock imports %} + + +{% block breadcrumbs %} + <li class="breadcrumb-item"> + {% if 'AKDashboard'|check_app_installed %} + <a href="{% url 'dashboard:dashboard' %}">AKPlanning</a> + {% else %} + AKPlanning + {% endif %} +</li> +<li class="breadcrumb-item">{{ event.slug }}</li> + <li class="breadcrumb-item"><a + href="{% url 'plan:plan_overview' event_slug=event.slug %}">{% trans "AK Plan" %}</a></li> +{% endblock %} + + +{% block content %} + <div class="float-right"> + <a href="{% url 'plan:plan_screen' event_slug=event.slug %}" class="btn btn-success">{% fontawesome_icon 'desktop' %} {% trans "screen view" %}</a> + </div> + + <h1>Plan: {{ event }}</h1> + + {% timezone event.timezone %} + <div class="row" style="margin-top:30px;"> + <div class="col-md-6"> + <h2><a name="currentAKs">{% trans "Current AKs" %}:</a></h2> + {% with akslots_now as slots %} + {% include "AKPlan/slots_table.html" %} + {% endwith %} + </div> + + <div class="col-md-6"> + <h2><a name="currentAKs">{% trans "Next AKs" %}:</a></h2> + {% with akslots_next as slots %} + {% include "AKPlan/slots_table.html" %} + {% endwith %} + </div> + + <div class="col-md-12"> + <div id="planCalendar" style="margin-top:30px;"></div> + </div> + </div> + {% endtimezone %} +{% endblock %} + + +{% block footer_custom %} + {% if event.contact_email %} + <h4> + <a href="mailto:{{ event.contact_email }}">{% fontawesome_icon "envelope" %} {% trans "Write to organizers of this event for questions and comments" %}</a> + </h4> + {% endif %} +{% endblock %} diff --git a/AKPlan/templates/AKPlan/plan_screen.html b/AKPlan/templates/AKPlan/plan_screen.html new file mode 100644 index 0000000000000000000000000000000000000000..8d21d59b24ee0d36281873feb5b6712e9ea34a43 --- /dev/null +++ b/AKPlan/templates/AKPlan/plan_screen.html @@ -0,0 +1,116 @@ +{% load static %} +{% load i18n %} +{% load bootstrap4 %} +{% load fontawesome %} +{% load tags_AKModel %} +{% load tz %} + + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>{% block title %}AK Planning{% endblock %}</title> + + {# Load Bootstrap CSS and JavaScript as well as font awesome #} + {% bootstrap_css %} + {% bootstrap_javascript jquery='slim' %} + {% fontawesome_stylesheet %} + + <link rel="stylesheet" href="{% static 'common/css/custom.css' %}"> + + {% get_current_language as LANGUAGE_CODE %} + + <link href='{% static 'AKPlan/fullcalendar/core/main.css' %}' rel='stylesheet' /> + <link href='{% static 'AKPlan/fullcalendar/timeline/main.css' %}' rel='stylesheet' /> + <link href='{% static 'AKPlan/fullcalendar/resource-timeline/main.css' %}' rel='stylesheet' /> + <link href='{% static 'AKPlan/fullcalendar/resource-timeline/main.min.css' %}' rel='stylesheet' /> + + <script src='{% static 'AKPlan/fullcalendar/core/main.js' %}'></script> + {% with 'AKPlan/fullcalendar/core/locales/'|add:LANGUAGE_CODE|add:'.js' as locale_file %} + <script src="{% static locale_file %}"></script> + {% endwith %} + <script src='{% static 'AKPlan/fullcalendar/timeline/main.js' %}'></script> + <script src='{% static 'AKPlan/fullcalendar/resource-common/main.js' %}'></script> + <script src='{% static 'AKPlan/fullcalendar/resource-timeline/main.js' %}'></script> + <script src='{% static 'AKPlan/fullcalendar/bootstrap/main.js' %}'></script> + + <script> + document.addEventListener('DOMContentLoaded', function() { + var planEl = document.getElementById('planCalendar'); + + var plan = new FullCalendar.Calendar(planEl, { + plugins: [ 'resourceTimeline', 'bootstrap'], + timeZone: '{{ event.timezone }}', + header: false, + themeSystem: 'bootstrap', + // Adapt to user selected locale + locale: '{{ LANGUAGE_CODE }}', + type: 'resourceTimeline', + slotDuration: '01:00', + defaultView: 'resourceTimeline', + visibleRange: { + start: '{{ start | timezone:event.timezone | date:"Y-m-d H:i:s" }}', + end: '{{ end | timezone:event.timezone | date:"Y-m-d H:i:s"}}', + }, + scrollTime: '{{ start | timezone:event.timezone | date:"H:i:s" }}', + editable: false, + allDaySlot: false, + nowIndicator: true, + eventTextColor: '#fff', + eventColor: '#127ba3', + height: 'parent', + resourceAreaWidth: '15%', + resourceLabelText: '{% trans "Room" %}', + resources: [ + {% for room in rooms %} + { + 'id': '{{ room.title }}', + 'title': '{{ room.title }}' + }, + {% endfor %} + ], + events: [ + {% for slot in akslots %} + {% if slot.room and slot.start %} + { + 'title': '{{ slot.ak.name }}', + '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" }}', + 'resourceId': '{{ slot.room.title }}', + }, + {% endif %} + {% endfor %} + ], + schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source', + }); + + plan.render(); + }); + </script> + +</head> +<body> + {% timezone event.timezone %} + <div class="row" style="height:100vh;margin:0;padding:1vh;"> + <div class="col-md-3"> + <h1>Plan: {{ event }}</h1> + + <h2><a name="currentAKs">{% trans "Current AKs" %}:</a></h2> + {% with akslots_now as slots %} + {% include "AKPlan/slots_table.html" %} + {% endwith %} + + <h2><a name="currentAKs">{% trans "Next AKs" %}:</a></h2> + {% with akslots_next as slots %} + {% include "AKPlan/slots_table.html" %} + {% endwith %} + </div> + <div class="col-md-9" style="height:98vh;"> + <div id="planCalendar"></div> + </div> + </div> + {% endtimezone %} + +</body> +</html> diff --git a/AKPlan/templates/AKPlan/slots_table.html b/AKPlan/templates/AKPlan/slots_table.html new file mode 100644 index 0000000000000000000000000000000000000000..cb246dbd7c57c5e99fb20c0858b6df8299f7a2ed --- /dev/null +++ b/AKPlan/templates/AKPlan/slots_table.html @@ -0,0 +1,14 @@ +{% load i18n %} + + +<table class="table table-striped"> + {% for akslot in slots %} + <tr> + <td><b><a href="{% url 'submit:ak_detail' event_slug=event.slug pk=akslot.ak.pk %}">{{ akslot.ak.name }}</a></b></td> + <td>{{ akslot.start | time:"H:i" }} - {{ akslot.end | time:"H:i" }}</td> + <td>{{ akslot.room }}</td> + </tr> + {% empty %} + {% trans "No AKs" %} + {% endfor %} +</table> diff --git a/AKPlan/urls.py b/AKPlan/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..dacc519e74d282d546a10c4562ad41dcc5f07954 --- /dev/null +++ b/AKPlan/urls.py @@ -0,0 +1,14 @@ +from django.urls import path, include +from . import views + +app_name = "plan" + +urlpatterns = [ + path( + '<slug:event_slug>/plan/', + include([ + path('', views.PlanIndexView.as_view(), name='plan_overview'), + path('screen/', views.PlanScreenView.as_view(), name='plan_screen'), + ]) + ), +] diff --git a/AKPlan/views.py b/AKPlan/views.py index 60f00ef0ef347811e7b0c0921b7fda097acd9fcc..d82ffe43679a5e18b72609ca6fc2a21c80e8ee43 100644 --- a/AKPlan/views.py +++ b/AKPlan/views.py @@ -1 +1,68 @@ -# Create your views here. +from datetime import timedelta + +from django.conf import settings +from django.utils.datetime_safe import datetime +from django.views.generic import ListView + +from AKModel.models import AKSlot +from AKModel.views import FilterByEventSlugMixin + + +class PlanIndexView(FilterByEventSlugMixin, ListView): + model = AKSlot + template_name = "AKPlan/plan_index.html" + context_object_name = "akslots" + ordering = "start" + + def get_queryset(self): + # Ignore slots not scheduled yet + return super().get_queryset().filter(start__isnull=False) + + def get_context_data(self, *, object_list=None, **kwargs): + context = super().get_context_data(object_list=object_list, **kwargs) + + context["event"] = self.event + + current_timestamp = datetime.now().astimezone(self.event.timezone) + + context["akslots_now"] = [] + context["akslots_next"] = [] + rooms = set() + + # Get list of current and next slots + for akslot in context["akslots"]: + # Construct a list of all rooms used by these slots on the fly + if akslot.room is not None: + rooms.add(akslot.room) + + # Recent AKs: Started but not ended yet + if akslot.start <= current_timestamp <= akslot.end: + context["akslots_now"].append(akslot) + # Next AKs: Not started yet, list will be filled in order until threshold is reached + elif akslot.start > current_timestamp: + if len(context["akslots_next"]) < settings.PLAN_MAX_NEXT_AKS: + context["akslots_next"].append(akslot) + + # Sort list of rooms by title + context["rooms"] = sorted(rooms, key=lambda x: x.title) + + return context + + +class PlanScreenView(PlanIndexView): + template_name = "AKPlan/plan_screen.html" + + def get_queryset(self): + # Determine interesting range (some hours ago until some hours in the future as specified in the settings) + self.start = datetime.now().astimezone(self.event.timezone) - timedelta(hours=settings.PLAN_BEAMER_HOURS_RETROSPECT) + self.end = self.start + timedelta(hours=(settings.PLAN_BEAMER_HOURS_RETROSPECT + settings.PLAN_BEAMER_HOURS_FUTURE)) + + # 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, start__lt=self.end) + + def get_context_data(self, *, object_list=None, **kwargs): + context = super().get_context_data(object_list=object_list, **kwargs) + context["start"] = self.start + context["end"] = self.end + return context diff --git a/AKPlanning/settings.py b/AKPlanning/settings.py index 17370e86fd4c5f8be722fcc6a728f4ed6e6b0319..40120d428f678318ac8de11afeb8b2a99beb38f3 100644 --- a/AKPlanning/settings.py +++ b/AKPlanning/settings.py @@ -36,7 +36,7 @@ INSTALLED_APPS = [ 'AKDashboard.apps.AkdashboardConfig', 'AKSubmission.apps.AksubmissionConfig', 'AKScheduling.apps.AkschedulingConfig', - # 'AKPlan.apps.AkplanConfig', + 'AKPlan.apps.AkplanConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -46,6 +46,7 @@ INSTALLED_APPS = [ 'bootstrap4', 'fontawesome', 'timezone_field', + 'rest_framework', ] MIDDLEWARE = [ @@ -156,4 +157,10 @@ FOOTER_INFO = { "impress_url": "" } +# How many AKs should be visible as next AKs +PLAN_MAX_NEXT_AKS = 10 +# Specify range of plan for screen/projector view +PLAN_SCREEN_HOURS_RETROSPECT = 3 +PLAN_SCREEN_HOURS_FUTURE = 18 + include(optional("settings/*.py"))