Skip to content

Commit

Permalink
App Toolbox: Integration LaTeX-Projekt
Browse files Browse the repository at this point in the history
  • Loading branch information
gdmhrogut committed Sep 29, 2023
1 parent 26fd5f5 commit f288fa6
Show file tree
Hide file tree
Showing 21 changed files with 701 additions and 42 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"enableMapLocate": true,
"featureGeometry": true,
"fetchGeoJsonFeatureCollection": true,
"fetchPdf": true,
"filterApplication": true,
"filterReset": true,
"getFeatureCenter": true,
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ db.sqlite3

# Datenwerft.HRO
datenwerft/secrets.py
datenmanagement/migrations
hilfe/build
/static
/uploads
Expand Down
28 changes: 28 additions & 0 deletions datenmanagement/static/datenmanagement/js/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,34 @@ function formatData(data, brReplacement) {
return data.trim(); // remove any remaining whitespaces from both sides
}

/**
* @function
* @name fetchPdf
*
* fetches PDF file
*
* @param {string} url - URL
* @param {string} csrfToken - CSRF token
* @param {string} host - host
*/
function fetchPdf(url, csrfToken, host){
const response = fetch(
url, {
method: 'POST',
headers: {
contentType: 'application/json',
'X-CSRFToken': csrfToken
},
redirect: 'follow',
origin: host,
referrerPolicy: 'no-referrer',
body: JSON.stringify(window.renderParams)
}
);
response.then(response => response.blob())
.then(myblob => window.open(URL.createObjectURL(myblob)));
}

/**
* @function
* @name initDataTable
Expand Down
41 changes: 40 additions & 1 deletion datenmanagement/templates/datenmanagement/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ <h4 class="mt-3">
window.dataUrl = '{{ url_model_tabledata }}';
{% endif %}
window.languageUrl = '{% static 'datatables/datatables.german.lang' %}';
window.checkAllTarget = true;
</script>
<script>
/**
Expand Down Expand Up @@ -126,9 +127,46 @@ <h4 class="mt-3">
'Bei der Übernahme der aktuellen Filtermenge auf die Karte ist ein Serverfehler aufgetreten.'
);
});

// fetch PDF file on clicking the "OK" button in the PDF export confirmation modal...
$('#confirm-export-modal-ok').on('click', () => fetchPdf(
'{% url "toolbox:renderpdf" %}', '{{ csrf_token }}', '{{ request.get_host }}'
));

// fetch PDF file on clicking the PDF export button...
$('#template-button').on('click', function() {
let checkedBoxes = $('.action-checkbox').filter(':checked');
let tplSelector = $('#template-selector');
let desiredTplId = tplSelector.value;
window.renderParams= {
'pks': [],
'templateid': desiredTplId,
'datenthema': '{{ model_name }}',
'onlyactive': !$('#onlyactive').prop('checked'),
};
if (checkedBoxes.length > 0) {
for (let i = 0; i < checkedBoxes.length; i++)
window.renderParams.pks.push(checkedBoxes[i].value);
fetchPdf(
'{% url "toolbox:renderpdf" %}', '{{ csrf_token }}', '{{ request.get_host }}'
);
}
else {
$('#confirm-export-modal').modal('toggle');
}
});

// select or unselect all rows (= records)
$('#check-all').on('click', function() {
let target = window.checkAllTarget;
$('.action-checkbox').each(function() {
$(this).prop('checked', target);
});
window.checkAllTarget = !target;
});
});

// as soon as a row (= record) has been selected or deselected...
// as soon as a row (= record) has been selected or unselected...
$('body').on('change', '.action-checkbox', function() {
let actionCheckboxes = $('.action-checkbox').filter(':checked');
// adjust information texts accordingly
Expand Down Expand Up @@ -157,5 +195,6 @@ <h4 class="mt-3">
</div>
{% endif %}
{% include "modal-error.html" %}
{% include "modal-confirm-export.html" with objcount=objects_count %}
{% endif %}
{% endblock %}
5 changes: 5 additions & 0 deletions datenmanagement/views/views_list_map.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import date, datetime, timezone
from decimal import Decimal
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.serializers import serialize
from django.urls import reverse
from django.utils.html import escape
Expand All @@ -13,6 +14,7 @@
from zoneinfo import ZoneInfo

from datenmanagement.utils import get_data, get_thumb_url, localize_number
from toolbox.models import SuitableFor
from toolbox.utils import optimize_datatable_filter
from .functions import add_basic_model_context_elements, add_user_agent_context_elements, \
get_model_objects
Expand Down Expand Up @@ -277,6 +279,9 @@ def get_context_data(self, **kwargs):
else:
context['objects_count'] = get_model_objects(self.model, None, True)
context['url_back'] = reverse('datenmanagement:' + model_name + '_start')
content_type = ContentType.objects.get_for_model(self.model)
suitable_templates = SuitableFor.objects.filter(datenthema=content_type)
context['suitables'] = suitable_templates
return context


Expand Down
17 changes: 17 additions & 0 deletions datenwerft/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,23 @@
}


# Toolbox app:
# PDF export

PDF_ESCAPE = [
('&', r'\&'),
(chr(8211), '--')
]
PDF_JINJASTRINGS = {
'block_start': r'\JINJA{',
'block_end': '}',
'variable_start': r'\VAR{',
'variable_end': '}',
'comment_start': r'\JCMNT{',
'comment_end': '}'
}


# configuration file with additional parameters
# which must not fall under Git version control

Expand Down
19 changes: 19 additions & 0 deletions datenwerft/templates/modal-confirm-export.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<div id="confirm-export-modal" class="modal" tabindex="-1"> <!-- aria-labelledby?-->
<div class="modal-dialog">
<div class="modal-content">
<div id="confirm-export-modal-header" class="modal-header">
<h5 id="confirmexport-modal-title" class="modal-title">alle Datensätze exportieren?</h5>
</div>
<div id="confirm-export-modal-body" class="modal-body">
<div class="text-center">
<p>Sie haben keinen Eintrag ausgewählt. Es werden also alle vorhandenen Datensätze dieses Datenthemas exportiert. Dies kann lange dauern und ungewollt große Dokumente erzeugen.</p>
<p><strong>{{ objcount }} Datensätze werden exportiert.</strong></p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="confirm-export-modal-ok" data-bs-dismiss="modal">Export durchführen</button>
<button type="button" class="btn btn-warning" id="confirm-export-modal-cancel" data-bs-dismiss="modal">abbrechen</button>
</div>
</div>
</div>
</div>
2 changes: 1 addition & 1 deletion datenwerft/templates/modal-error.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div id="error-modal-header" class="modal-header">
<h5 id="error-modal-title" class="modal-title"></h5>
<h5 id="error-modal-title" class="modal-title"></h5>
</div>
<div id="error-modal-body" class="modal-body"></div>
<div class="modal-footer">
Expand Down
2 changes: 1 addition & 1 deletion datenwerft/templates/modal-loading.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div id="loading-modal-header" class="modal-header">
<h5 id="loading-modal-title" class="modal-title"></h5>
<h5 id="loading-modal-title" class="modal-title"></h5>
</div>
<div id="loading-modal-body" class="modal-body">
<span id="loading-modal-body-text"></span>
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ django-jsonview
django-leaflet
django-user-agents
djangorestframework
Jinja2
Pillow
psycopg2-binary
PyYAML
Expand Down
9 changes: 9 additions & 0 deletions toolbox/405.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<body>
<head>
</head>
<html>
<h1>405 Falsche Methode</h1>
Sie haben eine GET-Anfrage gesendet (vermutlich, indem sie diese URL direkt mit dem Browser angewählt haben). Vorgesehen ist, dass in der App Datenmanagement aus ihren Eingaben in den Datenthemen-Ansichten ein POST-Request zusammengestellt wird, der hierhergesandt und mit einem PDF beantwortet wird.
</html>
</body>
61 changes: 29 additions & 32 deletions toolbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,51 +31,48 @@ Perspektivisch soll der Pfad `toolbox/mkpdf` zu einer Konfigurationsdirektive we

## Gebrauch

Die Verwaltung der Templates erfolgt im Django-Admin. Es gibt zwei Datenmodelle: `PdfTemplate` und `SuitableFor`.
Die Verwaltung der Templates erfolgt im *Django*-Admin. Es gibt zwei Datenmodelle: `PdfTemplate` und `SuitableFor`.

### PdfTemplate
### `PdfTemplate`

* werden hochgeladen, und müssen einen sprechenden Namen bekommen. Dieser wird dem User bei geeigneten Datenthemen zur Auswahl angezeigt.
* Sind LaTeX-Dateien mit Jinja2-TemplateTags.
- werden hochgeladen und müssen einen sprechenden Namen erhalten; dieser wird dem Benutzer bei geeigneten Datenthemen zur Auswahl angezeigt
- sind *LaTeX*-Dateien mit *Jinja2*-Template-Tags

### SuitableFor
### `SuitableFor`

* Sind eine Verknüpfung zwischen Datenthemen (mittels `ContentTypes`) und PdfTemplates.
* Können direkt im Admin für die Templates erzeugt werden.
* Pflichtfelder sind:
+ `Datenthema`: für welches Thema das Template als geeignet bezeichnet werden soll.
+ `Template`: von welchem Template das `SuitableFor` handelt. Bei Verwendung des Inline-Admin im PdfTemplate-Admin ist klar, dass es um das gerade bearbeitete Template geht.

* Das Feld `ins Template zu speisende Attribute` ist kein Pflichtfeld, eventuelle Einträge müssen das Format `[[feld, breite]]` haben.
+ `feld` muss ein Attributname des gewählten Datenthemas sein
+ `breite` muss eine von LaTeX verarbeitbare Längenangabe sein (zB `12mm`, `10pt`, `2in`). Dies wird (noch) nicht geprüft. Auch noch nicht geprüft wird, ob die Summe der Einträge die Breite einer Seite überschreitet.
+ Diese Angaben stehen im Templatekontext *zur Verfügung*, nur die angegebenen Spalten zu nutzen, ist Aufgabe des Templateautors.

* Das Feld `Sortierung der Einträge` verlangt das Format `[Sortierschlüssel]`, wobei Sortierschlüssel ein Feld des gewählten Datenthemas sein muss. Es darf ein "minus", - , vorangestellt werden. Nach diesem Feld wird dann aufsteigend sortiert. Es können mehrere Sortierschlüssel angegeben werden, dann wird von links nach rechts geschachtelt sortiert. Also in der Art `["baujahr", "-nutzfläche"]`.

* Existiert für ein Datenthema ein `SuitableFor`-Objekt, dann wird in der Listenansicht zu diesem Thema das verknüpfte Template zur Auswahl stehen.
- sind Verknüpfungen zwischen Datenthemen (mittels `ContentTypes`) und `PdfTemplate`
- können direkt im *Django*-Admin für die Templates erzeugt werden
- Pflichtfelder sind:
- *Datenthema:* für welches Datenthema das Template als geeignet bezeichnet werden soll
- *Template:* von welchem Template das `SuitableFor` handelt; bei Verwendung des Inline-Admin im `PdfTemplate`-Admin ist klar, dass es um das gerade bearbeitete Template geht
- Das Feld *ins Template zu speisende Attribute* ist kein Pflichtfeld, eventuelle Einträge müssen das Format `[[feld, breite]]` haben:
- `feld` muss ein Attributname des gewählten Datenthemas sein
- `breite` muss eine von *LaTeX* verarbeitbare Längenangabe sein (zB `12mm`, `10pt`, `2in`). Dies wird (noch) nicht geprüft. Auch noch nicht geprüft wird, ob die Summe der Einträge die Breite einer Seite überschreitet.
- Das Feld *Sortierung der Einträge* verlangt das Format `[Sortierschlüssel]`, wobei der Sortierschlüssel ein Feld des gewählten Datenthemas sein muss. Es darf ein „-“ vorangestellt werden. Nach diesem Feld wird dann aufsteigend sortiert. Es können mehrere Sortierschlüssel angegeben werden, dann wird von links nach rechts geschachtelt sortiert – also in der Art `["baujahr", "-nutzfläche"]`.
- Existiert für ein Datenthema ein `SuitableFor`-Objekt, dann wird in der Listenansicht zu diesem Thema das verknüpfte Template zur Auswahl stehen.

## Templates

Grundsätzlich sind die Templates LaTeX-Dokumente, in welchen Jinja-Template-Tags genutzt werden können. Die jinja-Marker {% STATEMENT %} und {{ DATA }} sind ersetzt durch \JINJA{STATEMENT} und \VAR{DATA}.
Grundsätzlich sind die Templates *LaTeX*-Dokumente, in denen *Jinja2*-Template-Tags genutzt werden können. Die *Jinja2*-Marker `{% STATEMENT %}` und `{{ DATA }}` sind ersetzt durch `\JINJA{STATEMENT}` und `\VAR{DATA}`.

Im Kontext-Dictionary stehen die folgenden Daten zur Verfügung:

* `jetzt` - Ein deutsch formatierter Datumsstempel (dd.mm.jjjj).
* `datenthema` - Der Name des gerade zu verarbeitenden Datenthemas (für den Titel des Dokuments).
* `usedkeys` - Die im Abschnitt [SuitableFor](.SuitableFor) beschriebenen zu verarbeitenden Tupel aus Schlüssel und wie breit die Tabellenspalte für diesen Schlüssel sein soll.
* `display_names` - Ein dictionary `{attrnamen: displaynamen}`. `attrnamen` sind die internen Namen der Felder des Datenthemas, wie sie auch in `usedkeys` auftauchen, `displaynamen` sind die anzuzeigenden Kurzbeschreibungen/Darstellnamen der Felder.
* `records` - Ein Django-Queryset, sortiert wie im `SuitableFor` eingestellt.
- `jetzt` deutsch formatierter Datumsstempel (dd.mm.jjjj)
- `datenthema` Name des gerade zu verarbeitenden Datenthemas (für den Titel des Dokuments)
- `usedkeys` – die im Abschnitt [SuitableFor](#suitablefor) beschriebenen, zu verarbeitenden Tupel aus Schlüssel und wie breit die Tabellenspalte für diesen Schlüssel sein soll
- `display_names` – ein Dictionary `{attrnamen: displaynamen}`: `attrnamen` sind die internen Namen der Felder des Datenthemas, wie sie auch in `usedkeys` auftauchen, `displaynamen` sind die anzuzeigenden Kurzbeschreibungen/Darstellnamen der Felder
- `records` – ein *Django*-Queryset, sortiert wie im `SuitableFor` eingestellt

* Sollte das Erzeugen des PDFs fehlschlagen, so wird `stdout` von `pdflatex` als Antwort zurückgegeben. Es empfiehlt sich trotzdem, die Templates vorerst auf dem eigenen Rechner zu schreiben und zu testen.
Sollte das Erzeugen der PDF-Datei fehlschlagen, so wird `stdout` von `pdflatex` als Antwort zurückgegeben. Es empfiehlt sich trotzdem, die Templates vorerst auf dem eigenen Rechner zu schreiben und zu testen.

* es können mithilfe der Jinja-Sprache nun die verschiedenen Daten ins Template gepumpt werden. Besonders hervorgehoben seien hier
+ der LaTeX-Befehl `\begin{longtable}`
+ jinjas for-Schleife, bei uns mit `\JINJA{for}, \JINJA{endfor}`
+ die Möglichkeit, im Template Variablen zu setzen, so kann über eine Liste von Schlüsseln iteriert werden, welche im Schleifenkörper nochmals als Schlüssel verwendet werden (zB für die Verarbeitung baumartiger Strukturen. Sie `pdfs.py: sortforbaudenkmale()` und das Template `Custom-Baudenkmale`
Es können mit Hilfe der *Jinja2*-Sprache nun die verschiedenen Daten ins Template gepumpt werden. Besonders hervorgehoben seien hier:

Für eine Einführung in Jinja sei auf die die Jinja-Dokumentation, [Abschnitt Template-Designer](https://jinja.palletsprojects.com/en/3.0.x/templates/), verwiesen und für LaTeX auf die große Menge an brauchbarer Literatur und Handbüchern, insbesondere [diesen Kurzeinstieg](https://www.ctan.org/pkg/lshort-german). Für gewöhnlich gibt es zu jeder erdenklichen Frage, die einem beim \LaTeX-Schreiben einfallen könnte, einen Thread in einem Forum, oder einen Artikel in den [Hilfeseiten von Overleaf](https://overleaf.com/learn/).
- der *LaTeX*-Befehl `\begin{longtable}`,
- die `for`-Schleife von *Jinja2,* bei uns mit `\JINJA{for}, \JINJA{endfor}` und
- die Möglichkeit, im Template Variablen zu setzen; so kann über eine Liste von Schlüsseln iteriert werden, die im Schleifenkörper nochmals als Schlüssel verwendet werden (z.B. für die Verarbeitung baumartiger Strukturen, siehe `pdfs.py: sortforbaudenkmale()` und das Template `Custom-Baudenkmale`)

Für eine Einführung in *Jinja2* sei auf die die *Jinja2*-Dokumentation, [Abschnitt Template-Designer](https://jinja.palletsprojects.com/en/3.0.x/templates/), verwiesen und für *LaTeX* auf die große Menge an brauchbarer Literatur und Handbüchern, insbesondere [diesen Kurzeinstieg](https://www.ctan.org/pkg/lshort-german). Für gewöhnlich gibt es zu jeder erdenklichen Frage, die einem beim *LaTeX*-Schreiben einfallen könnte, einen Thread in einem Forum oder einen Artikel in den [Hilfeseiten von Overleaf](https://overleaf.com/learn/).

## TODO
In der Klasse *SuitableFor* existiert ein Feld *Bemerkungen*, sodass bei mehreren Verknüpfungen desselben Datenthemas mit demselben Template jeweils im Auswahlmenü ein eigener Text angezeigt werden kann. Im Moment wird der Template-Name angezeigt. Dies gilt es noch zu ändern. Außerdem zur Diskussion stand, die Objekte auch in Karten einzutragen und diese Karten in die Exporte einzubetten.

In der Klasse `SuitableFor` existiert ein Feld *Bemerkungen,* sodass bei mehreren Verknüpfungen desselben Datenthemas mit demselben Template jeweils im Auswahlmenü ein eigener Text angezeigt werden kann. Im Moment wird der Template-Name angezeigt. Dies gilt es noch zu ändern. Außerdem zur Diskussion stand die Objekte auch in Karten einzutragen und diese Karten in die Exporte einzubetten.
51 changes: 51 additions & 0 deletions toolbox/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from django.contrib import admin
from .models import PdfTemplate, SuitableFor
from django.db import models
from django.forms import TextInput, ModelForm
from django.core.exceptions import ValidationError


class SuitableForInline(admin.StackedInline):
model = SuitableFor
fieldsets = [(None, {'fields': ['id', 'datenthema', 'bemerkungen', 'usedkeys', 'sortby']})]


@admin.register(PdfTemplate)
class PdfTemplateAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'created_at', 'description')
inlines = [SuitableForInline]
formfield_overrides = {
models.CharField: {'widget': TextInput(attrs={'size': '20'})}
}


class SuitableForm(ModelForm):

def get_f_names(self):
fields = self.cleaned_data['datenthema'].model_class()._meta.get_fields()
return [f.name for f in fields]

def clean_sortby(self):
keylist = self.cleaned_data['sortby']
fieldnames = self.get_f_names()
if keylist is not None:
for key in keylist:
if key not in fieldnames and not (key[0] == '-' and key[1:] in fieldnames):
raise ValidationError(f"{key} ist kein Feld von {self.cleaned_data['datenthema']}!")
return self.cleaned_data['sortby']

def clean_usedkeys(self):
keylist = self.cleaned_data['usedkeys']
fieldnames = self.get_f_names()
if keylist is not None:
for key in keylist:
if key[0] not in fieldnames:
raise ValidationError(f"{key[0]} ist kein Feld von {self.cleaned_data['datenthema']}!")
return keylist


@admin.register(SuitableFor)
class SuitableForAdmin(admin.ModelAdmin):
ordering = ['datenthema']
list_display = ('id', 'datenthema', 'template__name', 'bemerkungen')
form = SuitableForm
Empty file added toolbox/management/__init__.py
Empty file.
Empty file.
28 changes: 28 additions & 0 deletions toolbox/management/commands/cleanpdfdir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import os
from django.conf import settings
from django.core.management.base import BaseCommand


class Command(BaseCommand):
def handle(self, *args, **options):

verbose = False
if options.get('verbosity') > 1:
verbose = True

rmroot = os.path.join(settings.BASE_DIR, 'toolbox', 'mkpdf')
os.chdir(rmroot)
fs = os.listdir()
deld = 0
skpd = 0
for f in fs:
if f[:4] == 'tmp_':
os.remove(f)
if verbose:
print(f'del {f}')
deld += 1
else:
if verbose:
print(f'skip {f}')
skpd += 1
print(f'{deld} Datei(en) gelöscht, {skpd} Datei(en) übersprungen in {rmroot}')
Loading

0 comments on commit f288fa6

Please sign in to comment.