Author SHA1 Message Date
samuelbias 91d5f3d422 Merge pull request 'redesign - new dashboard, new contentpanel template functionality, monatsplan fix' (#29) from dev into main
Reviewed-on: #29
Reviewed-by: Leonie Fink <leoniefink12@gmail.com>
2026-09-04 14:31:09 +02:00
samuelbias 8ba41eb71e Merge pull request 'contentpanel - test fix' (#28) from feature/dashboard-rewrite-v2 into dev
Reviewed-on: #28
2026-09-04 14:23:39 +02:00
samuelbias 5c7401f3bc Merge pull request 'Feature/dashboard rewrite v2' (#27) from feature/dashboard-rewrite-v2 into dev
Reviewed-on: #27
2026-09-04 13:27:47 +02:00
samuelbias 9abf97a922 Merge pull request 'contentpanel - Aushangworkflow um docx Vorlage erweitern' (#25) from feature/contentpanel/aushangsvorlage into dev
Reviewed-on: #25
2026-09-02 21:14:36 +02:00
samuelbias b5233bb6eb Merge pull request 'feature/developerpanel - linked dev instance and sso' (#21) from feature/developerpanel into main
Reviewed-on: #21
2026-08-29 22:58:45 +02:00
samuelbias 9c34e504ca Merge pull request 'feature/developerpanel - linked dev instance and sso' (#22) from feature/developerpanel into dev
Reviewed-on: #22
2026-08-29 22:56:58 +02:00
samuelbias ab58e13b88 Merge pull request 'bugfix/docker-compose - strato sync switch implemented' (#19) from bugfix/docker-compose into main
Reviewed-on: #19
2026-08-29 22:12:31 +02:00
samuelbias 9c3aef4a96 Merge pull request 'bugfix/docker-compose - strato sync switch implemented' (#20) from bugfix/docker-compose into dev
Reviewed-on: #20
2026-08-29 22:11:52 +02:00
samuelbias a238ba1a55 Merge pull request 'bugfix/docker-compose - strato sync switch implemented' (#17) from bugfix/docker-compose into main
Reviewed-on: #17
Reviewed-by: Leonie Fink <leoniefink12@gmail.com>
2026-08-29 21:57:11 +02:00
samuelbias e8305771f8 Merge pull request 'bugfix/docker-compose - strato sync switch implemented' (#18) from bugfix/docker-compose into dev
Reviewed-on: #18
2026-08-29 21:56:34 +02:00
samuelbias 6988aee875 Merge pull request 'bugfix/docker-compose - strato sync switch implemented' (#16) from bugfix/docker-compose into main
Reviewed-on: #16
2026-08-29 21:51:55 +02:00
samuelbias 2e33f2da38 Merge pull request 'bugfix/docker-compose - strato sync switch implemented' (#15) from bugfix/docker-compose into dev
Reviewed-on: #15
2026-08-29 21:51:03 +02:00
samuelbias 494bfa8884 Merge pull request 'ACHTUNG: Refactoring - Rewrite des Intranet Quellcodes' (#14) from dev into main
Reviewed-on: #14
Reviewed-by: Leonie Fink <leoniefink12@gmail.com>
2026-08-29 21:37:33 +02:00
samuelbias 37e7e4e367 Merge pull request 'monatsplan - ckalender read fixed, developerpanel - rss feed implemented' (#13) from bugfix/monatsplan into dev
Reviewed-on: #13
2026-08-29 21:28:07 +02:00
samuelbias 0314bced40 Merge pull request 'web - refactor core modules, harden authorization, simplify endpoints' (#11) from refactor/whole-web-application into dev
Reviewed-on: #11
2026-08-29 11:27:24 +02:00
markusvonhobe 3c56459cb8 Merge pull request 'project/developerpanel - all necessary websites linked' (#7) from feature/new-developerpanel into main
Reviewed-on: #7
Reviewed-by: Markus von Hobe <markus.vonhobe@gmail.com>
2026-08-28 23:23:43 +02:00
samuelbias 643d1a49d1 Merge pull request 'Feature/new developerpanel' (#6) from feature/new-developerpanel into dev
Reviewed-on: #6
2026-08-28 16:03:29 +02:00
7 changed files with 34 additions and 94 deletions
-2
View File
@@ -51,7 +51,6 @@ RUN mkdir -p \
/var/www/html/intranet_repo/projects/contentpanel/insertion/news \
/var/www/html/intranet_repo/projects/developerpanel/work \
/var/www/html/intranet_repo/projects/seelsorge/data \
/var/www/html/intranet_repo/projects/dashboard/cache \
/var/www/html/intranet_repo/projects/dashboard/month \
&& chown -R www-data:www-data \
/data \
@@ -59,7 +58,6 @@ RUN mkdir -p \
/var/www/html/intranet_repo/projects/contentpanel/insertion/news \
/var/www/html/intranet_repo/projects/developerpanel/work \
/var/www/html/intranet_repo/projects/seelsorge/data \
/var/www/html/intranet_repo/projects/dashboard/cache \
/var/www/html/intranet_repo/projects/dashboard/month \
&& chmod +x /opt/intranet/docker/scheduler.py /opt/intranet/worker/pdf_python/pdf_to_png.sh /opt/intranet/worker/contentpanel_aushang/process.py /opt/intranet/worker/contentpanel_aushang/betteroffice_render.py
@@ -203,10 +203,9 @@
clearTimeout(this.timer);
if (!this.rotate) return;
const duration = this.items.length ? config.POSTER_MIN_DWELL_TIME_S * 1000 : config.EMPTY_REFRESH_MS;
const changing = this.items.length && this.layout().groups.length > 1;
this.progressAnimation?.cancel();
this.progress.toggleAttribute('hidden', !changing);
if (changing) {
this.progress.toggleAttribute('hidden', !this.items.length);
if (this.items.length) {
this.progressAnimation = this.progress.querySelector('.poster-progress-value').animate(
[{ strokeDashoffset: 100 }, { strokeDashoffset: 0 }],
{ duration, easing: 'linear', fill: 'forwards' });
@@ -35,7 +35,7 @@ body.dashboard {
padding: var(--space-sm);
}
#month-panel { grid-area: month; --panel-accent: var(--nak-purple); }
#poster-panel { grid-area: posters; --panel-accent: var(--nak-orange); --poster-frame-width: 1px; }
#poster-panel { grid-area: posters; --panel-accent: var(--nak-orange); --poster-frame-width: 2px; }
#announcements-panel { --panel-accent: var(--nak-teal); }
.dashboard-column { display: grid; gap: var(--space-sm); min-height: 0; min-width: 0; }
.announcements-column { grid-area: announcements; grid-template-rows: auto minmax(0, 1fr); }
@@ -74,7 +74,6 @@ body.dashboard {
.text-page { color: var(--dashboard-text); }
.text-page p { margin: 0 0 var(--space-xs); white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--announcement-font); line-height: var(--announcement-leading); }
.text-page p:last-child { margin-bottom: 0; }
.text-page .announcement-title { font-size: var(--announcement-detail-font); }
.text-page .announcement-title, .text-page .bible-reference { font-weight: 500; }
.text-page .announcement-subtitle { font-size: var(--announcement-detail-font); color: var(--dashboard-meta); }
.text-page .announcement-meta { font-size: var(--announcement-meta-font); color: var(--dashboard-meta); }
+21 -51
View File
@@ -35,7 +35,7 @@ def validate_template(template: Path) -> None:
_placeholders(minidom.parseString(archive.read('word/document.xml')))
def _set_text(paragraph, text):
def _set_text(paragraph, text):
if any(ord(char) < 32 and char not in '\t\n\r' for char in text):
raise TemplateError('Text enthält ungültige Steuerzeichen.')
nodes = paragraph.getElementsByTagNameNS(W, 't')
@@ -44,49 +44,22 @@ def _set_text(paragraph, text):
node.removeChild(node.firstChild)
document, anchor = paragraph.ownerDocument, nodes[0]
text = text.replace('\r\n', '\n').replace('\r', '\n')
for part in re.split(r'(\n|\t)', text):
for part in re.split(r'(\n|\t)', text):
if part in ('\n', '\t'):
node = document.createElementNS(W, 'w:br' if part == '\n' else 'w:tab')
else:
node = document.createElementNS(W, 'w:t')
node.setAttribute('xml:space', 'preserve')
node.appendChild(document.createTextNode(part))
anchor.parentNode.insertBefore(node, anchor)
anchor.parentNode.removeChild(anchor)
if '\n' in text:
paragraph.getElementsByTagNameNS(W, 'jc')[0].setAttributeNS(W, 'w:val', 'left')
def _items(text):
lines = [line.strip() for line in text.replace('\r\n', '\n').replace('\r', '\n').split('\n') if line.strip()]
bullets = [line[1:].lstrip() if line.startswith('•') and line[1:].strip() else None for line in lines]
if not any(bullets):
return [(text, None)]
return [(bullet, i + 1 < len(lines) and bullets[i + 1] is not None) if bullet else (line, None)
for i, (line, bullet) in enumerate(zip(lines, bullets))]
def _format_bullet(paragraph, compact):
properties = paragraph.getElementsByTagNameNS(W, 'pPr')[0]
anchor = properties.getElementsByTagNameNS(W, 'jc')[0]
document = paragraph.ownerDocument
tabs = document.createElementNS(W, 'w:tabs')
tab = document.createElementNS(W, 'w:tab')
tab.setAttributeNS(W, 'w:val', 'left')
tab.setAttributeNS(W, 'w:pos', '480')
tabs.appendChild(tab)
properties.insertBefore(tabs, anchor)
if compact:
spacing = document.createElementNS(W, 'w:spacing')
spacing.setAttributeNS(W, 'w:after', '0')
properties.insertBefore(spacing, anchor)
indent = document.createElementNS(W, 'w:ind')
indent.setAttributeNS(W, 'w:left', '480')
indent.setAttributeNS(W, 'w:hanging', '360')
properties.insertBefore(indent, anchor)
def fill_template(template: Path, output: Path, title: str, date_label: str, paragraphs: list[str]) -> None:
anchor.parentNode.insertBefore(node, anchor)
anchor.parentNode.removeChild(anchor)
# Preserve verse/signature lines without stretching them to the full width.
if '\n' in text:
for alignment in paragraph.getElementsByTagNameNS(W, 'jc'):
alignment.setAttributeNS(W, 'w:val', 'left')
def fill_template(template: Path, output: Path, title: str, date_label: str, paragraphs: list[str]) -> None:
paragraphs = [p.strip() for p in paragraphs if p.strip()]
if not title.strip() or not date_label.strip() or not paragraphs:
raise TemplateError('Titel, Datum und Inhalt dürfen nicht leer sein.')
@@ -96,19 +69,16 @@ def fill_template(template: Path, output: Path, title: str, date_label: str, par
_set_text(placeholders[TITLE_TOKEN], title.strip())
_set_text(placeholders[DATE_TOKEN], date_label.strip())
body = placeholders[BODY_TOKEN]
used = {p.getAttributeNS(W14, 'paraId') for p in document.getElementsByTagNameNS(W, 'p')}
index = 1
for text in paragraphs:
for value, compact in _items(text):
paragraph = body.cloneNode(deep=True)
while f'{index:08X}' in used:
index += 1
paragraph.setAttributeNS(W14, 'w14:paraId', f'{index:08X}')
used.add(f'{index:08X}')
_set_text(paragraph, f'•\t{value}' if compact is not None else value)
if compact is not None:
_format_bullet(paragraph, compact)
body.parentNode.insertBefore(paragraph, body)
used = {p.getAttributeNS(W14, 'paraId') for p in document.getElementsByTagNameNS(W, 'p')}
index = 1
for text in paragraphs:
paragraph = body.cloneNode(deep=True)
while f'{index:08X}' in used:
index += 1
paragraph.setAttributeNS(W14, 'w14:paraId', f'{index:08X}')
used.add(f'{index:08X}')
_set_text(paragraph, text)
body.parentNode.insertBefore(paragraph, body)
body.parentNode.removeChild(body)
output.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output, 'w') as target:
@@ -139,7 +139,7 @@ class PipelineTests(unittest.TestCase):
self.assertEqual(xml.count(DATE_TOKEN), 1)
self.assertEqual(xml.count(BODY_TOKEN), 1)
def test_original_line_breaks_use_word_elements(self):
def test_original_line_breaks_use_word_elements(self):
with tempfile.TemporaryDirectory() as temp:
target = Path(temp) / 'lines.docx'
fill_template(TEMPLATE, target, 'Gruß', _date_label('2026-09-02'), ['Von Herzen\nEure Gastgeber'])
@@ -148,34 +148,10 @@ class PipelineTests(unittest.TestCase):
ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
paragraph = next(p for p in document.findall('.//w:p', ns) if p.find('.//w:br', ns) is not None)
self.assertEqual(len(paragraph.findall('.//w:br', ns)), 1)
self.assertEqual([t.text for t in paragraph.findall('.//w:t', ns)], ['Von Herzen', 'Eure Gastgeber'])
self.assertEqual(paragraph.find('w:pPr/w:jc', ns).get('{' + ns['w'] + '}val'), 'left')
def test_bullet_lines_become_hanging_list_paragraphs(self):
with tempfile.TemporaryDirectory() as temp:
target = Path(temp) / 'bullets.docx'
fill_template(TEMPLATE, target, 'Einladung', _date_label('2026-09-04'), [
'Einleitung.',
'SEPTEMBER\n•05.09.2026: Erster Stichpunkt.\n• Zweiter Stichpunkt.\nOKTOBER\n• Dritter Stichpunkt.',
'Abschluss.',
])
with zipfile.ZipFile(target) as archive:
document = ET.fromstring(archive.read('word/document.xml'))
ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
q = lambda name: '{' + ns['w'] + '}' + name
bullets = [paragraph for paragraph in document.findall('.//w:p', ns)
if ''.join(paragraph.itertext()).startswith('•')]
self.assertEqual(len(bullets), 3)
self.assertTrue({'SEPTEMBER', 'OKTOBER'} <= {''.join(p.itertext()) for p in document.findall('.//w:p', ns)})
for paragraph in bullets:
self.assertIsNotNone(paragraph.find('w:r/w:tab', ns))
self.assertEqual(paragraph.find('w:pPr/w:tabs/w:tab', ns).get(q('pos')), '480')
indent = paragraph.find('w:pPr/w:ind', ns)
self.assertEqual((indent.get(q('left')), indent.get(q('hanging'))), ('480', '360'))
self.assertEqual(bullets[0].find('w:pPr/w:spacing', ns).get(q('after')), '0')
self.assertIsNone(bullets[1].find('w:pPr/w:spacing', ns))
def test_template_fill_creates_multiple_paragraphs(self):
self.assertEqual([t.text for t in paragraph.findall('.//w:t', ns)], ['Von Herzen', 'Eure Gastgeber'])
self.assertEqual(paragraph.find('w:pPr/w:jc', ns).get('{' + ns['w'] + '}val'), 'left')
def test_template_fill_creates_multiple_paragraphs(self):
with tempfile.TemporaryDirectory() as temp:
target = Path(temp) / "filled.docx"
paragraphs = [f"Absatz {i}: Inhalt für den Aushang." for i in range(1, 8)]
@@ -32,13 +32,11 @@ class RenderingTests(unittest.TestCase):
cls.document = cls.output / "verified.docx"
with zipfile.ZipFile(TEMPLATE) as archive:
(cls.output / "original-logo.png").write_bytes(archive.read("word/media/image1.png"))
paragraphs = [
("Dies ist ein langer Testabsatz zur Prüfung von Schriftgröße, Zeilenumbruch und Seitenaufteilung. " * 12).strip()
for _ in range(3)
] + ["• Ein längerer Stichpunkt, dessen zweite Zeile unter dem Text und nicht unter dem Aufzählungszeichen beginnt.\n"
"• Ein zweiter Stichpunkt bestätigt den gleichmäßigen Abstand der Liste.",
"Erste Gedichtzeile\nZweite Gedichtzeile\n\nVon Herzen\nEure Gastgeber",
"Abschlussmarker: Dieser letzte Absatz muss vollständig auf der letzten Seite erscheinen."]
paragraphs = [
("Dies ist ein langer Testabsatz zur Prüfung von Schriftgröße, Zeilenumbruch und Seitenaufteilung. " * 12).strip()
for _ in range(3)
] + ["Erste Gedichtzeile\nZweite Gedichtzeile\n\nVon Herzen\nEure Gastgeber",
"Abschlussmarker: Dieser letzte Absatz muss vollständig auf der letzten Seite erscheinen."]
fill_template(TEMPLATE, cls.document,
"Verabschiedung von Diakon Georg aus der Gemeinde Reinickendorf",
_date_label("2026-09-02"), paragraphs)