diff --git a/AKModel/admin.py b/AKModel/admin.py index 9427710f3894d17dcc22163efcd003886f771dc7..16fe017f6a083ce54d658b30842d9ef0558fb497 100644 --- a/AKModel/admin.py +++ b/AKModel/admin.py @@ -1,8 +1,8 @@ +from django import forms from django.apps import apps from django.contrib import admin from django.contrib.admin import SimpleListFilter from django.db.models import Count, F -from django import forms from django.shortcuts import render, redirect from django.urls import path, reverse_lazy from django.utils import timezone @@ -16,9 +16,11 @@ from AKModel.availability.forms import AvailabilitiesFormMixin from AKModel.availability.models import Availability from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKTag, AKRequirement, AK, AKSlot, Room, AKOrgaMessage, \ ConstraintViolation -from AKModel.views import EventStatusView, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, AKRequirementOverview, \ +from AKModel.views import EventStatusView, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, \ + AKRequirementOverview, \ NewEventWizardStartView, NewEventWizardSettingsView, NewEventWizardPrepareImportView, NewEventWizardFinishView, \ NewEventWizardImportView, NewEventWizardActivateView +from AKModel.views import export_slides @admin.register(Event) @@ -56,6 +58,7 @@ class EventAdmin(admin.ModelAdmin): path('<slug:event_slug>/requirements/', self.admin_site.admin_view(AKRequirementOverview.as_view()), name="event_requirement_overview"), path('<slug:event_slug>/ak-csv-export/', self.admin_site.admin_view(AKCSVExportView.as_view()), name="ak_csv_export"), path('<slug:event_slug>/ak-wiki-export/', self.admin_site.admin_view(AKWikiExportView.as_view()), name="ak_wiki_export"), + path('<slug:event_slug>/ak-slide-export/', export_slides, name="ak_slide_export"), path('<slug:slug>/delete-orga-messages/', self.admin_site.admin_view(AKMessageDeleteView.as_view()), name="ak_delete_orga_messages"), ] diff --git a/AKModel/environment.py b/AKModel/environment.py new file mode 100644 index 0000000000000000000000000000000000000000..5883361b0d8a80f647e131185dabca8aaa4d4bd7 --- /dev/null +++ b/AKModel/environment.py @@ -0,0 +1,26 @@ +# environment.py +import re + +from django_tex.environment import environment + +# Used to filter all very special UTF-8 chars that are probably not contained in the LaTeX fonts +# and would hence cause compilation errors +utf8_replace_pattern = re.compile(u'[^\u0000-\u206F]', re.UNICODE) + +def latex_escape_utf8(value): + """ + Escape latex special chars and remove invalid utf-8 values + + :param value: string to escape + :type value: str + :return: escaped string + :rtype: str + """ + return utf8_replace_pattern.sub('', value).replace('&', '\&').replace('_', '\_').replace('#', '\#').replace('$', '\$').replace('%', '\%').replace('{', '\{').replace('}', '\}') + +def improved_tex_environment(**options): + env = environment(**options) + env.filters.update({ + 'latex_escape_utf8': latex_escape_utf8, + }) + return env diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po index 0f5209a90da627faff8fb5580cca31585a64b972..22e42f3091df8c92591259eeca9fb388b53b7fb0 100644 --- a/AKModel/locale/de_DE/LC_MESSAGES/django.po +++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po @@ -2,7 +2,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: 2021-05-08 18:07+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" @@ -11,7 +11,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: AKModel/admin.py:66 AKModel/admin.py:67 +#: AKModel/admin.py:69 AKModel/admin.py:70 #: AKModel/templates/admin/AKModel/event_wizard/activate.html:32 #: AKModel/templates/admin/AKModel/event_wizard/created_prepare_import.html:48 #: AKModel/templates/admin/AKModel/event_wizard/finish.html:21 @@ -21,23 +21,23 @@ msgstr "" msgid "Status" msgstr "Status" -#: AKModel/admin.py:153 +#: AKModel/admin.py:156 msgid "Wish" msgstr "AK-Wunsch" -#: AKModel/admin.py:159 +#: AKModel/admin.py:162 msgid "Is wish" msgstr "Ist ein Wunsch" -#: AKModel/admin.py:160 +#: AKModel/admin.py:163 msgid "Is not a wish" msgstr "Ist kein Wunsch" -#: AKModel/admin.py:187 +#: AKModel/admin.py:209 msgid "Export to wiki syntax" msgstr "In Wiki-Syntax exportieren" -#: AKModel/admin.py:283 +#: AKModel/admin.py:317 msgid "AK Details" msgstr "AK-Details" @@ -170,7 +170,7 @@ msgstr "Zeitzone" msgid "Time Zone where this event takes place in" msgstr "Zeitzone in der das Event stattfindet" -#: AKModel/models.py:25 AKModel/views.py:206 +#: AKModel/models.py:25 AKModel/views.py:209 msgid "Start" msgstr "Start" @@ -430,7 +430,7 @@ msgstr "AK präsentieren" msgid "Present results of this AK" msgstr "Die Ergebnisse dieses AKs vorstellen" -#: AKModel/models.py:218 AKModel/templates/admin/AKModel/status.html:85 +#: AKModel/models.py:218 AKModel/templates/admin/AKModel/status.html:87 msgid "Requirements" msgstr "Anforderungen" @@ -485,7 +485,7 @@ msgstr "Anzahl Personen, die online Interesse bekundet haben" #: AKModel/models.py:240 AKModel/models.py:440 #: AKModel/templates/admin/AKModel/status.html:49 -#: AKModel/templates/admin/AKModel/status.html:56 +#: AKModel/templates/admin/AKModel/status.html:56 AKModel/views.py:310 msgid "AKs" msgstr "AKs" @@ -761,7 +761,7 @@ msgid "Successfully imported.<br><br>Do you want to activate your event now?" msgstr "Erfolgreich importiert.<br><br>Soll das Event jetzt aktiviert werden?" #: AKModel/templates/admin/AKModel/event_wizard/activate.html:27 -#: AKModel/views.py:211 +#: AKModel/views.py:214 msgid "Finish" msgstr "Abschluss" @@ -845,7 +845,7 @@ msgid "No AKs with this requirement" msgstr "Kein AK mit dieser Anforderung" #: AKModel/templates/admin/AKModel/requirements_overview.html:45 -#: AKModel/templates/admin/AKModel/status.html:101 +#: AKModel/templates/admin/AKModel/status.html:103 msgid "Add Requirement" msgstr "Anforderung hinzufügen" @@ -898,19 +898,23 @@ msgstr "AKs als CSV exportieren" msgid "Export AKs for Wiki" msgstr "AKs im Wiki-Format exportieren" -#: AKModel/templates/admin/AKModel/status.html:87 +#: AKModel/templates/admin/AKModel/status.html:84 +msgid "Export AK Slides" +msgstr "AK-Folien exportieren" + +#: AKModel/templates/admin/AKModel/status.html:89 msgid "No requirements yet" msgstr "Bisher keine Anforderungen" -#: AKModel/templates/admin/AKModel/status.html:100 +#: AKModel/templates/admin/AKModel/status.html:102 msgid "Show AKs for requirements" msgstr "Zu Anforderungen gehörige AKs anzeigen" -#: AKModel/templates/admin/AKModel/status.html:104 +#: AKModel/templates/admin/AKModel/status.html:106 msgid "Messages" msgstr "Nachrichten" -#: AKModel/templates/admin/AKModel/status.html:106 +#: AKModel/templates/admin/AKModel/status.html:108 msgid "Delete all messages" msgstr "Alle Nachrichten löschen" @@ -918,54 +922,78 @@ msgstr "Alle Nachrichten löschen" msgid "Active Events" msgstr "Aktive Events" -#: AKModel/views.py:136 +#: AKModel/views.py:139 msgid "Event Status" msgstr "Eventstatus" -#: AKModel/views.py:149 +#: AKModel/views.py:152 msgid "Requirements for Event" msgstr "Anforderungen für das Event" -#: AKModel/views.py:163 +#: AKModel/views.py:166 msgid "AK CSV Export" msgstr "AK-CSV-Export" -#: AKModel/views.py:177 +#: AKModel/views.py:180 msgid "AK Wiki Export" msgstr "AK-Wiki-Export" -#: AKModel/views.py:197 +#: AKModel/views.py:200 msgid "AK Orga Messages successfully deleted" msgstr "AK-Organachrichten erfolgreich gelöscht" -#: AKModel/views.py:207 +#: AKModel/views.py:210 msgid "Settings" msgstr "Einstellungen" -#: AKModel/views.py:208 +#: AKModel/views.py:211 msgid "Event created, Prepare Import" msgstr "Event angelegt, Import vorbereiten" -#: AKModel/views.py:209 +#: AKModel/views.py:212 msgid "Import categories & requirements" msgstr "Kategorien & Anforderungen kopieren" -#: AKModel/views.py:210 +#: AKModel/views.py:213 #, fuzzy #| msgid "Active State" msgid "Activate?" msgstr "Aktivieren?" -#: AKModel/views.py:270 +#: AKModel/views.py:271 #, python-format msgid "Copied '%(obj)s'" msgstr "'%(obj)s' kopiert" -#: AKModel/views.py:272 +#: AKModel/views.py:273 #, python-format msgid "Could not copy '%(obj)s' (%(error)s)" msgstr "'%(obj)s' konnte nicht kopiert werden (%(error)s)" +#: AKModel/views.py:300 +msgid "Symbols" +msgstr "Symbole" + +#: AKModel/views.py:301 +msgid "Who?" +msgstr "Wer?" + +#: AKModel/views.py:302 +msgid "Duration(s)" +msgstr "Dauer(n)" + +#: AKModel/views.py:303 +msgid "Reso intention?" +msgstr "Resolutionsabsicht?" + +#: AKModel/views.py:304 +msgid "Category (for Wishes)" +msgstr "Kategorie (für Wünsche)" + +#: AKModel/views.py:311 +msgid "Wishes" +msgstr "Wünsche" + #~ msgid "Confirm" #~ msgstr "Bestätigen" diff --git a/AKModel/models.py b/AKModel/models.py index b371953669e9e5d594132dbe351d44d4c6f28d7d..dcb1d45775e341c91553578885f32801042f3081 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -264,7 +264,7 @@ class AK(models.Model): @property def durations_list(self): - return ", ".join(str(slot.duration) for slot in self.akslot_set.all()) + return ", ".join(str(slot.duration_simplified) for slot in self.akslot_set.all()) @property def tags_list(self): diff --git a/AKModel/templates/admin/AKModel/export/slides.tex b/AKModel/templates/admin/AKModel/export/slides.tex new file mode 100644 index 0000000000000000000000000000000000000000..d30f70e54c4357055c8a46ddba4378d21cd7ce5e --- /dev/null +++ b/AKModel/templates/admin/AKModel/export/slides.tex @@ -0,0 +1,105 @@ +\documentclass[aspectratio=169]{beamer} +\usetheme[numbering=fraction, progressbar=foot]{metropolis} + +\usepackage[utf8]{inputenc} +\usepackage{fontawesome5} + +\title{ {{- title -}} } +\subtitle{ {{- subtitle -}} } +\date{\today} + +\begin{document} + +\begin{frame} +\maketitle +\end{frame} + +\begin{frame} + \frametitle{ {{- translations.symbols -}} } + + \faUser~ {{ translations.who }} + + \faClock~ {{ translations.duration }} + + \faScroll~{{ translations.reso }} + + \faFilter~ {{ translations.category }} + +\end{frame} + + +{%for category, ak_list in categories_with_aks %} + + \section{ {{- category.name | latex_escape_utf8 -}} } + + {% for ak, next_aks in ak_list %} + + {% if not ak.wish %} + + %\setbeamertemplate{frame footer}{} + + \begin{frame}[shrink=15] + \frametitle{ {{- ak.name | latex_escape_utf8 -}} } + + \vspace{1em} + + \faUser~ {{ ak.owners_list | latex_escape_utf8 }} + + \faClock~ {{ak.durations_list}} + + {% if ak.reso %} + \faScroll + {% endif %} + + {{ ak.description | truncatechars(400) | latex_escape_utf8 }} + + \vspace{2em} + + \begin{scriptsize} + {% for n_ak in next_aks %} + {% if n_ak %}\hfill \faAngleDoubleRight~ {{- n_ak.name | latex_escape_utf8 -}}{% endif %} + {% endfor %} + \end{scriptsize} + + \end{frame} + + {% endif %} + + {% endfor %} + +{% endfor %} + +{% if not result_presentation_mode %} + + \section{ {{- translations.wishes -}} } + + {% for ak, next_aks in wishes %} + + %\setbeamertemplate{frame footer}{} + + \begin{frame}[shrink=15] + \frametitle{ {{- ak.name | latex_escape_utf8 -}} } + + \vspace{1em} + + \faFilter~ {{ ak.category.name | latex_escape_utf8 }} + + \faUser~ + + \faClock~ + + {{ ak.description | truncatechars(400) | latex_escape_utf8 }} + + \vspace{2em} + + \begin{scriptsize} + {% for n_ak in next_aks %} + {% if n_ak %}\hfill \faAngleDoubleRight~ {{- n_ak.name | latex_escape_utf8 -}}{% endif %} + {% endfor %} + \end{scriptsize} + + \end{frame} + {% endfor %} +{% endif %} + +\end{document} diff --git a/AKModel/templates/admin/AKModel/status.html b/AKModel/templates/admin/AKModel/status.html index ee84a94ac5aeb0d9d7de0c08db3811bb84d64809..dd269c83db03f118bb5f2c72447e437d8c856042 100644 --- a/AKModel/templates/admin/AKModel/status.html +++ b/AKModel/templates/admin/AKModel/status.html @@ -80,6 +80,8 @@ href="{% url 'admin:ak_csv_export' event_slug=event.slug %}">{% trans "Export AKs as CSV" %}</a> <a class="btn btn-success" href="{% url 'admin:ak_wiki_export' event_slug=event.slug %}">{% trans "Export AKs for Wiki" %}</a> + <a class="btn btn-success" + href="{% url 'admin:ak_slide_export' event_slug=event.slug %}">{% trans "Export AK Slides" %}</a> {% endif %} <h3 class="block-header">{% trans "Requirements" %}</h3> diff --git a/AKModel/views.py b/AKModel/views.py index cc78a0c5a2bece33be49d4c495746722b112ed49..c1926d7de07f97678e3051c6b80d82d01f5c05e5 100644 --- a/AKModel/views.py +++ b/AKModel/views.py @@ -1,10 +1,15 @@ +from itertools import zip_longest + from django.contrib import admin, messages +from django.contrib.admin.views.decorators import staff_member_required from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views.generic import TemplateView, DetailView, ListView, DeleteView, CreateView, FormView, UpdateView from rest_framework import viewsets, permissions, mixins +from django_tex.shortcuts import render_to_pdf + from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \ NewEventWizardImportForm, NewEventWizardActivateForm @@ -242,8 +247,6 @@ class NewEventWizardPrepareImportView(WizardViewMixin, EventSlugMixin, FormView) template_name = "admin/AKModel/event_wizard/created_prepare_import.html" wizard_step = 3 - - def form_valid(self, form): # Selected a valid event to import from? Use this to go to next step of wizard return redirect("admin:new_event_wizard_import", event_slug=self.event.slug, import_slug=form.cleaned_data["import_event"].slug) @@ -287,3 +290,50 @@ class NewEventWizardFinishView(WizardViewMixin, DetailView): model = Event template_name = "admin/AKModel/event_wizard/finish.html" wizard_step = 6 + + +@staff_member_required +def export_slides(request, event_slug): + template_name = 'admin/AKModel/export/slides.tex' + + event = get_object_or_404(Event, slug=event_slug) + + NEXT_AK_LIST_LENGTH = int(request.GET["num_next"]) if "num_next" in request.GET else 3 + RESULT_PRESENTATION_MODE = True if "presentation_mode" in request.GET else False + + translations = { + 'symbols': _("Symbols"), + 'who': _("Who?"), + 'duration': _("Duration(s)"), + 'reso': _("Reso intention?"), + 'category': _("Category (for Wishes)"), + 'wishes': _("Wishes"), + } + + def build_ak_list_with_next_aks(ak_list): + next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None) + return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=list())] + + categories = event.akcategory_set.all() + categories_with_aks = [] + ak_wishes = [] + for category in categories: + ak_list = [] + for ak in category.ak_set.all(): # order_by("owners").distinct(): + if ak.wish: + ak_wishes.append(ak) + else: + if not RESULT_PRESENTATION_MODE or ak.present: + ak_list.append(ak) + categories_with_aks.append((category, build_ak_list_with_next_aks(ak_list))) + + context = { + 'title': event.name, + 'categories_with_aks': categories_with_aks, + 'subtitle': _("AKs"), + "wishes": build_ak_list_with_next_aks(ak_wishes), + "translations": translations, + "result_presentation_mode": RESULT_PRESENTATION_MODE, + } + + return render_to_pdf(request, template_name, context, filename='slides.pdf') diff --git a/AKPlanning/settings.py b/AKPlanning/settings.py index 24346632836f229713f10ed41ab862355d9d845b..a7e3c286868cfcf899d8d74c1dfc7f3fe28a750a 100644 --- a/AKPlanning/settings.py +++ b/AKPlanning/settings.py @@ -52,6 +52,7 @@ INSTALLED_APPS = [ 'simple_history', 'registration', 'bootstrap_datepicker_plus', + 'django_tex', ] MIDDLEWARE = [ @@ -86,6 +87,14 @@ TEMPLATES = [ ], }, }, + { + 'NAME': 'tex', + 'BACKEND': 'django_tex.engine.TeXEngine', + 'APP_DIRS': True, + 'OPTIONS': { + 'environment': 'AKModel.environment.improved_tex_environment', + }, + }, ] WSGI_APPLICATION = 'AKPlanning.wsgi.application' @@ -138,6 +147,9 @@ LANGUAGES = [ INTERNAL_IPS = ['127.0.0.1', '::1'] +LATEX_INTERPRETER = 'pdflatex' +LATEX_RUN_COUNT = 2 + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ diff --git a/INSTALL.md b/INSTALL.md index 040a5aa8e59a7fe6ffdb9e531b00977468af993c..54a002e3d63a8730bdadab29334c13447f72b8ff 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -12,6 +12,7 @@ AKPlanning has two types of requirements: System requirements are dependent on o * Python 3.7 incl. development tools * Virtualenv +* pdflatex & beamer class (`texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-extra`) * for production using uwsgi: * C compiler e.g. gcc * uwsgi diff --git a/requirements.txt b/requirements.txt index 2d21e5a5cdf1e4161852a84c8ab11db7eb9b3f2e..89e362f134fd970578b7aafbe1c32006ce7bf318 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ django-simple-history==3.0.0 django-registration-redux==2.9 django-debug-toolbar==3.2.1 django-bootstrap-datepicker-plus==3.0.5 +django-tex @ git+https://github.com/bhaettasch/django-tex.git@91db2dc814a35c6e1d4a4b758a1a7b56822305b5 django-csp==3.7 mysqlclient==2.0.3 # for production deployment pytz==2021.1