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/contentpanel/insertion/news \
/var/www/html/intranet_repo/projects/developerpanel/work \ /var/www/html/intranet_repo/projects/developerpanel/work \
/var/www/html/intranet_repo/projects/seelsorge/data \ /var/www/html/intranet_repo/projects/seelsorge/data \
/var/www/html/intranet_repo/projects/dashboard/cache \
/var/www/html/intranet_repo/projects/dashboard/month \ /var/www/html/intranet_repo/projects/dashboard/month \
&& chown -R www-data:www-data \ && chown -R www-data:www-data \
/data \ /data \
@@ -59,7 +58,6 @@ RUN mkdir -p \
/var/www/html/intranet_repo/projects/contentpanel/insertion/news \ /var/www/html/intranet_repo/projects/contentpanel/insertion/news \
/var/www/html/intranet_repo/projects/developerpanel/work \ /var/www/html/intranet_repo/projects/developerpanel/work \
/var/www/html/intranet_repo/projects/seelsorge/data \ /var/www/html/intranet_repo/projects/seelsorge/data \
/var/www/html/intranet_repo/projects/dashboard/cache \
/var/www/html/intranet_repo/projects/dashboard/month \ /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 && 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); clearTimeout(this.timer);
if (!this.rotate) return; if (!this.rotate) return;
const duration = this.items.length ? config.POSTER_MIN_DWELL_TIME_S * 1000 : config.EMPTY_REFRESH_MS; 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.progressAnimation?.cancel();
this.progress.toggleAttribute('hidden', !changing); this.progress.toggleAttribute('hidden', !this.items.length);
if (changing) { if (this.items.length) {
this.progressAnimation = this.progress.querySelector('.poster-progress-value').animate( this.progressAnimation = this.progress.querySelector('.poster-progress-value').animate(
[{ strokeDashoffset: 100 }, { strokeDashoffset: 0 }], [{ strokeDashoffset: 100 }, { strokeDashoffset: 0 }],
{ duration, easing: 'linear', fill: 'forwards' }); { duration, easing: 'linear', fill: 'forwards' });
@@ -35,7 +35,7 @@ body.dashboard {
padding: var(--space-sm); padding: var(--space-sm);
} }
#month-panel { grid-area: month; --panel-accent: var(--nak-purple); } #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); } #announcements-panel { --panel-accent: var(--nak-teal); }
.dashboard-column { display: grid; gap: var(--space-sm); min-height: 0; min-width: 0; } .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); } .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 { 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 { 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 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-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-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); } .text-page .announcement-meta { font-size: var(--announcement-meta-font); color: var(--dashboard-meta); }
+4 -34
View File
@@ -53,37 +53,10 @@ def _set_text(paragraph, text):
node.appendChild(document.createTextNode(part)) node.appendChild(document.createTextNode(part))
anchor.parentNode.insertBefore(node, anchor) anchor.parentNode.insertBefore(node, anchor)
anchor.parentNode.removeChild(anchor) anchor.parentNode.removeChild(anchor)
# Preserve verse/signature lines without stretching them to the full width.
if '\n' in text: if '\n' in text:
paragraph.getElementsByTagNameNS(W, 'jc')[0].setAttributeNS(W, 'w:val', 'left') for alignment in paragraph.getElementsByTagNameNS(W, 'jc'):
alignment.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: def fill_template(template: Path, output: Path, title: str, date_label: str, paragraphs: list[str]) -> None:
@@ -99,15 +72,12 @@ def fill_template(template: Path, output: Path, title: str, date_label: str, par
used = {p.getAttributeNS(W14, 'paraId') for p in document.getElementsByTagNameNS(W, 'p')} used = {p.getAttributeNS(W14, 'paraId') for p in document.getElementsByTagNameNS(W, 'p')}
index = 1 index = 1
for text in paragraphs: for text in paragraphs:
for value, compact in _items(text):
paragraph = body.cloneNode(deep=True) paragraph = body.cloneNode(deep=True)
while f'{index:08X}' in used: while f'{index:08X}' in used:
index += 1 index += 1
paragraph.setAttributeNS(W14, 'w14:paraId', f'{index:08X}') paragraph.setAttributeNS(W14, 'w14:paraId', f'{index:08X}')
used.add(f'{index:08X}') used.add(f'{index:08X}')
_set_text(paragraph, f'•\t{value}' if compact is not None else value) _set_text(paragraph, text)
if compact is not None:
_format_bullet(paragraph, compact)
body.parentNode.insertBefore(paragraph, body) body.parentNode.insertBefore(paragraph, body)
body.parentNode.removeChild(body) body.parentNode.removeChild(body)
output.parent.mkdir(parents=True, exist_ok=True) output.parent.mkdir(parents=True, exist_ok=True)
@@ -151,30 +151,6 @@ class PipelineTests(unittest.TestCase):
self.assertEqual([t.text for t in paragraph.findall('.//w:t', ns)], ['Von Herzen', 'Eure Gastgeber']) 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') 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): def test_template_fill_creates_multiple_paragraphs(self):
with tempfile.TemporaryDirectory() as temp: with tempfile.TemporaryDirectory() as temp:
target = Path(temp) / "filled.docx" target = Path(temp) / "filled.docx"
@@ -35,9 +35,7 @@ class RenderingTests(unittest.TestCase):
paragraphs = [ paragraphs = [
("Dies ist ein langer Testabsatz zur Prüfung von Schriftgröße, Zeilenumbruch und Seitenaufteilung. " * 12).strip() ("Dies ist ein langer Testabsatz zur Prüfung von Schriftgröße, Zeilenumbruch und Seitenaufteilung. " * 12).strip()
for _ in range(3) for _ in range(3)
] + ["• Ein längerer Stichpunkt, dessen zweite Zeile unter dem Text und nicht unter dem Aufzählungszeichen beginnt.\n" ] + ["Erste Gedichtzeile\nZweite Gedichtzeile\n\nVon Herzen\nEure Gastgeber",
"• 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."] "Abschlussmarker: Dieser letzte Absatz muss vollständig auf der letzten Seite erscheinen."]
fill_template(TEMPLATE, cls.document, fill_template(TEMPLATE, cls.document,
"Verabschiedung von Diakon Georg aus der Gemeinde Reinickendorf", "Verabschiedung von Diakon Georg aus der Gemeinde Reinickendorf",