snapshot: preserve handbook, bilingual phase 2, and logo updates

This commit is contained in:
Md Bayazid Bostame
2026-03-24 12:25:43 +01:00
parent 0f285aa2cf
commit 4d3c7bdf6e
26 changed files with 625 additions and 66 deletions

View File

@@ -28,9 +28,9 @@ class OffboardingRequestAdmin(admin.ModelAdmin):
@admin.register(FormOption)
class FormOptionAdmin(admin.ModelAdmin):
list_display = ('category', 'label', 'value', 'sort_order', 'is_active')
list_display = ('category', 'label', 'label_en', 'value', 'sort_order', 'is_active')
list_filter = ('category', 'is_active')
search_fields = ('label', 'value')
search_fields = ('label', 'label_en', 'value')
ordering = ('category', 'sort_order', 'label')
@@ -38,16 +38,16 @@ class FormOptionAdmin(admin.ModelAdmin):
class FormFieldConfigAdmin(admin.ModelAdmin):
list_display = ('form_type', 'field_name', 'page_key', 'sort_order', 'is_visible', 'is_required')
list_filter = ('form_type', 'page_key', 'is_visible', 'is_required')
search_fields = ('field_name', 'label_override', 'help_text_override')
search_fields = ('field_name', 'label_override', 'label_override_en', 'help_text_override', 'help_text_override_en')
ordering = ('form_type', 'sort_order', 'field_name')
list_editable = ('page_key', 'sort_order', 'is_visible', 'is_required')
@admin.register(IntroChecklistItem)
class IntroChecklistItemAdmin(admin.ModelAdmin):
list_display = ('section', 'label', 'condition_field', 'condition_operator', 'condition_value', 'sort_order', 'is_active')
list_display = ('section', 'label', 'label_en', 'condition_field', 'condition_operator', 'condition_value', 'sort_order', 'is_active')
list_filter = ('section', 'condition_operator', 'is_active')
search_fields = ('label', 'condition_field', 'condition_value')
search_fields = ('label', 'label_en', 'condition_field', 'condition_value')
ordering = ('section', 'sort_order', 'label')
list_editable = ('sort_order', 'is_active')

View File

@@ -1,4 +1,5 @@
from collections import OrderedDict
from django.utils.translation import get_language
from .models import FormFieldConfig
@@ -154,17 +155,20 @@ def apply_form_field_config(form_type: str, form) -> None:
field_names = list(form.fields.keys())
configs = _ensure_configs(form_type, field_names)
locked = LOCKED_FIELD_RULES.get(form_type, set())
language_code = get_language()
for field_name, field in list(form.fields.items()):
cfg = configs.get(field_name)
if not cfg:
continue
if cfg.label_override.strip():
field.label = cfg.label_override.strip()
translated_label = cfg.translated_label_override(language_code)
if translated_label:
field.label = translated_label
if cfg.help_text_override.strip():
field.help_text = cfg.help_text_override.strip()
translated_help_text = cfg.translated_help_text_override(language_code)
if translated_help_text:
field.help_text = translated_help_text
if field_name not in locked and cfg.is_required is not None:
field.required = cfg.is_required

View File

@@ -1,5 +1,6 @@
from django import forms
from pathlib import Path
from django.utils.translation import get_language
from .form_builder import apply_form_field_config
from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest
@@ -214,7 +215,8 @@ class OnboardingRequestForm(forms.ModelForm):
options = FormOption.objects.filter(category=category, is_active=True).order_by('sort_order', 'label')
if not options.exists():
return fallback
return [(o.value or o.label, o.label) for o in options]
language_code = get_language()
return [(o.value or o.label, o.translated_label(language_code)) for o in options]
def __init__(self, *args, **kwargs):
self.requester_email = (kwargs.pop('requester_email', '') or '').strip().lower()

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.1.5 on 2026-03-24 10:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('workflows', '0028_onboardingintroductionsession_exported_pdf_path'),
]
operations = [
migrations.AddField(
model_name='formfieldconfig',
name='help_text_override_en',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='formfieldconfig',
name='label_override_en',
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name='formoption',
name='label_en',
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name='introchecklistitem',
name='label_en',
field=models.CharField(blank=True, max_length=255),
),
]

View File

@@ -1,4 +1,5 @@
from django.db import models
from django.utils.translation import get_language
class EmployeeProfile(models.Model):
@@ -98,6 +99,7 @@ class FormOption(models.Model):
category = models.CharField(max_length=40, choices=CATEGORY_CHOICES)
label = models.CharField(max_length=255)
label_en = models.CharField(max_length=255, blank=True)
value = models.CharField(max_length=255, blank=True)
sort_order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
@@ -109,6 +111,12 @@ class FormOption(models.Model):
def __str__(self) -> str:
return f"{self.get_category_display()}: {self.label}"
def translated_label(self, language_code: str | None = None) -> str:
lang = (language_code or get_language() or 'de').split('-')[0]
if lang == 'en' and self.label_en.strip():
return self.label_en.strip()
return self.label.strip()
class FormFieldConfig(models.Model):
PAGE_CHOICES = [
@@ -130,7 +138,9 @@ class FormFieldConfig(models.Model):
is_required = models.BooleanField(null=True, blank=True, default=None)
page_key = models.CharField(max_length=20, blank=True, default='', choices=PAGE_CHOICES)
label_override = models.CharField(max_length=255, blank=True)
label_override_en = models.CharField(max_length=255, blank=True)
help_text_override = models.TextField(blank=True)
help_text_override_en = models.TextField(blank=True)
class Meta:
ordering = ['form_type', 'sort_order', 'field_name']
@@ -141,6 +151,18 @@ class FormFieldConfig(models.Model):
def __str__(self) -> str:
return f'{self.get_form_type_display()}: {self.field_name}'
def translated_label_override(self, language_code: str | None = None) -> str:
lang = (language_code or get_language() or 'de').split('-')[0]
if lang == 'en' and self.label_override_en.strip():
return self.label_override_en.strip()
return self.label_override.strip()
def translated_help_text_override(self, language_code: str | None = None) -> str:
lang = (language_code or get_language() or 'de').split('-')[0]
if lang == 'en' and self.help_text_override_en.strip():
return self.help_text_override_en.strip()
return self.help_text_override.strip()
class NotificationTemplate(models.Model):
TEMPLATE_CHOICES = [
@@ -249,6 +271,7 @@ class IntroChecklistItem(models.Model):
section = models.CharField(max_length=30, choices=SECTION_CHOICES)
label = models.CharField(max_length=255)
label_en = models.CharField(max_length=255, blank=True)
sort_order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
condition_field = models.CharField(max_length=80, blank=True)
@@ -261,6 +284,12 @@ class IntroChecklistItem(models.Model):
def __str__(self) -> str:
return f'{self.get_section_display()}: {self.label}'
def translated_label(self, language_code: str | None = None) -> str:
lang = (language_code or get_language() or 'de').split('-')[0]
if lang == 'en' and self.label_en.strip():
return self.label_en.strip()
return self.label.strip()
class OnboardingIntroductionSession(models.Model):
STATUS_CHOICES = [

View File

@@ -235,7 +235,7 @@ body {
.add-option-form {
display: grid;
grid-template-columns: minmax(220px, 1fr) minmax(220px, 1fr) auto;
grid-template-columns: minmax(180px, 1fr) minmax(180px, 1fr) minmax(180px, 1fr) auto;
gap: 8px;
margin-bottom: 10px;
}
@@ -260,6 +260,7 @@ body {
border: 1px solid #e2e8f0;
padding: 7px 8px;
text-align: left;
vertical-align: top;
}
.option-table th {
@@ -300,6 +301,8 @@ body {
.options-actions {
margin-top: 10px;
display: flex;
justify-content: flex-end;
}
@media (max-width: 1120px) {

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="420" height="110" viewBox="0 0 420 110" role="img" aria-labelledby="title desc">
<title id="title">TUBCO</title>
<desc id="desc">TUBCO wordmark</desc>
<rect width="420" height="110" fill="white"/>
<g transform="translate(8 12)">
<text x="0" y="46" font-family="Arial, Helvetica, sans-serif" font-size="58" font-weight="700" letter-spacing="-2">
<tspan fill="#000078">TUB</tspan><tspan fill="#c31924">CO</tspan>
</text>
<text x="2" y="82" font-family="Georgia, 'Times New Roman', serif" font-size="24" fill="#000078" letter-spacing="0.8">
TU BERLIN CORPORATE
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 663 B

View File

@@ -8,6 +8,7 @@ from celery import shared_task
from django.contrib.auth import get_user_model
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext as _, get_language
from jinja2 import Template
from pypdf import PageObject, PdfReader, PdfWriter
from xhtml2pdf import pisa
@@ -294,7 +295,7 @@ def _matches_intro_condition(request_obj: OnboardingRequest, item: IntroChecklis
return True
def _build_intro_sections_from_admin(request_obj: OnboardingRequest) -> dict[str, list[str]]:
def _build_intro_sections_from_admin(request_obj: OnboardingRequest, language_code: str | None = None) -> dict[str, list[str]]:
items = list(IntroChecklistItem.objects.filter(is_active=True).order_by('section', 'sort_order', 'label'))
if not items:
return {}
@@ -304,11 +305,26 @@ def _build_intro_sections_from_admin(request_obj: OnboardingRequest) -> dict[str
if item.section not in section_map:
continue
if _matches_intro_condition(request_obj, item):
section_map[item.section].append(item.label)
section_map[item.section].append(item.translated_label(language_code))
return {key: values for key, values in section_map.items() if values}
def build_intro_sections_for_request(request_obj: OnboardingRequest) -> list[dict]:
def build_intro_sections_for_request(request_obj: OnboardingRequest, language_code: str | None = None) -> list[dict]:
lang = (language_code or get_language() or 'de').split('-')[0]
section_titles = {
'de': {
'workplace': 'Geräte und Arbeitsplatz',
'accounts': 'Konten und Berechtigungen',
'software': 'Software und Tools',
'process': 'Prozesse und Hinweise',
},
'en': {
'workplace': 'Devices and workplace',
'accounts': 'Accounts and permissions',
'software': 'Software and tools',
'process': 'Processes and notes',
},
}
devices = _split_multiline(request_obj.needed_devices)
software = _split_multiline(request_obj.needed_software)
accesses = _split_multiline(request_obj.needed_accesses)
@@ -354,12 +370,12 @@ def build_intro_sections_for_request(request_obj: OnboardingRequest) -> list[dic
if request_obj.successor_name:
process_items.append(f'Übergabe-/Nachfolgekontext besprochen: {request_obj.successor_name}')
custom_intro_items = _build_intro_sections_from_admin(request_obj)
custom_intro_items = _build_intro_sections_from_admin(request_obj, lang)
intro_sections_raw = [
('workplace', 'Geräte und Arbeitsplatz', workplace_items),
('accounts', 'Konten und Berechtigungen', account_items),
('software', 'Software und Tools', software_items),
('process', 'Prozesse und Hinweise', process_items),
('workplace', section_titles.get(lang, section_titles['de'])['workplace'], workplace_items),
('accounts', section_titles.get(lang, section_titles['de'])['accounts'], account_items),
('software', section_titles.get(lang, section_titles['de'])['software'], software_items),
('process', section_titles.get(lang, section_titles['de'])['process'], process_items),
]
sections = []
@@ -714,7 +730,7 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path:
return output_pdf
def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest) -> Path:
def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest, language_code: str | None = None) -> Path:
safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'onboarding_intro_{request_obj.id}')
output_pdf = settings.PDF_OUTPUT_DIR / f'onboarding_intro_{safe_name}.pdf'
temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_intro_{safe_name}.pdf'
@@ -729,7 +745,7 @@ def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest) -> Path:
'title': section['title'],
'rows': _chunk_list([item['label'] for item in section['items']], chunk_size=2),
}
for section in build_intro_sections_for_request(request_obj)
for section in build_intro_sections_for_request(request_obj, language_code=language_code)
]
requester_email = request_obj.onboarded_by_email or '-'
@@ -755,7 +771,11 @@ def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest) -> Path:
return output_pdf
def _generate_onboarding_intro_session_pdf(session: OnboardingIntroductionSession, admin_signature_name: str = '-') -> Path:
def _generate_onboarding_intro_session_pdf(
session: OnboardingIntroductionSession,
admin_signature_name: str = '-',
language_code: str | None = None,
) -> Path:
request_obj = session.onboarding_request
safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'onboarding_intro_session_{request_obj.id}')
version = timezone.now().strftime('%Y%m%d%H%M%S')
@@ -768,7 +788,7 @@ def _generate_onboarding_intro_session_pdf(session: OnboardingIntroductionSessio
salutation = (request_obj.get_gender_display() or '').strip()
display_name = f"{salutation} {request_obj.full_name}".strip() if salutation else request_obj.full_name
raw_sections = build_intro_sections_for_request(request_obj)
raw_sections = build_intro_sections_for_request(request_obj, language_code=language_code)
checked_map = session.checklist_state or {}
exported_sections = []
checked_count = 0

View File

@@ -1,7 +1,6 @@
{% load static i18n %}
{% get_current_language as CURRENT_LANGUAGE %}
<!doctype html>
<html lang="{{ CURRENT_LANGUAGE }}">
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -18,22 +17,13 @@
input { width: 100%; padding: 10px; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; }
.btn { width: 100%; }
.errorlist { color: #b91c1c; margin: 6px 0; }
.card-head { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; }
.lang-switch { display:flex; gap:6px; }
.lang-btn { border:1px solid #d9e3f0; background:#f8fbff; color:#1f3a5f; border-radius:999px; padding:6px 10px; font-size:12px; font-weight:700; cursor:pointer; }
.lang-btn.active { background:#000078; border-color:#000078; color:#fff; }
.card-head { display:block; }
</style>
</head>
<body>
<div class="card">
<div class="card-head">
<img class="logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<form method="post" action="{% url 'set_language' %}" class="lang-switch">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}" />
<button class="lang-btn {% if CURRENT_LANGUAGE == 'de' %}active{% endif %}" type="submit" name="language" value="de">DE</button>
<button class="lang-btn {% if CURRENT_LANGUAGE == 'en' %}active{% endif %}" type="submit" name="language" value="en">EN</button>
</form>
<img class="logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
</div>
<h1>{% trans "Anmeldung" %}</h1>
<p>{% trans "Bitte melden Sie sich mit Ihrem Benutzerkonto an." %}</p>

View File

@@ -0,0 +1,223 @@
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Developer Handbook</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style>
body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #1b2b43; padding: 20px; }
.shell { max-width: 1120px; margin: 0 auto; background: #fff; border: 1px solid #d7e0ea; border-radius: 14px; padding: 18px; }
.brand-logo { width: 190px; max-width: 100%; height: auto; margin: 0 0 10px; display: block; }
.top { display: flex; justify-content: space-between; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
h1 { margin: 0; color: #000078; font-size: 30px; }
.sub { margin: 8px 0 16px; color: #5f6f85; }
.toc { border: 1px solid #d7e0ea; border-radius: 10px; padding: 10px; background: #f7fbff; margin-bottom: 16px; }
.toc a { color: #0b4da2; text-decoration: none; margin-right: 10px; white-space: nowrap; }
h2 { margin: 20px 0 8px; color: #113a74; border-bottom: 1px solid #e1e8f2; padding-bottom: 4px; }
h3 { margin: 14px 0 6px; color: #183f77; }
ul { margin: 8px 0 12px 20px; }
li { margin: 4px 0; }
code { background: #f1f5fb; border: 1px solid #dce6f3; border-radius: 6px; padding: 2px 6px; }
pre { background: #f7fbff; border: 1px solid #dce6f3; border-radius: 10px; padding: 10px; overflow-x: auto; }
.box { border: 1px solid #d7e0ea; border-radius: 10px; padding: 10px; background: #fcfdff; margin: 8px 0 12px; }
.note { border-left: 4px solid #000078; padding: 8px 10px; background: #f4f8ff; margin: 10px 0; }
</style>
</head>
<body>
<div class="shell">
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<div class="top">
<h1>Developer Handbook</h1>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<a class="btn btn-secondary" href="/admin-tools/wiki/">Project Wiki</a>
<a class="btn btn-secondary" href="/">Back to Home</a>
</div>
</div>
<p class="sub">Engineering runbook for development, deployment, maintenance, and extension of the TUBCO Onboarding & Offboarding Portal.</p>
<div class="toc">
<a href="#overview">Overview</a>
<a href="#structure">Structure</a>
<a href="#local">Local Dev</a>
<a href="#docker">Docker</a>
<a href="#db">Database</a>
<a href="#translations">Translations</a>
<a href="#pdf">PDF Pipeline</a>
<a href="#email">Email Pipeline</a>
<a href="#nextcloud">Nextcloud</a>
<a href="#builders">Builders</a>
<a href="#testing">Testing</a>
<a href="#deploy">Deployment</a>
<a href="#troubleshooting">Troubleshooting</a>
<a href="#security">Security</a>
</div>
<h2 id="overview">1) Overview</h2>
<div class="box">
<p>This handbook is for developers and maintainers. It documents the actual engineering workflow of the standalone product repository.</p>
<ul>
<li>Repository: <code>tubco-onboarding-offboarding-portal</code></li>
<li>Main stack: Django + Celery + PostgreSQL + Redis + MailHog</li>
<li>Runtime mode: Docker Compose for local development and staging-style operation</li>
</ul>
</div>
<h2 id="structure">2) Repository Structure</h2>
<ul>
<li><code>/backend/config/</code>: Django settings, WSGI, URL config</li>
<li><code>/backend/workflows/</code>: application logic, views, models, tasks, templates, static assets</li>
<li><code>/backend/media/templates/</code>: PDF HTML templates and letterhead source files</li>
<li><code>/backend/media/pdfs/</code>: generated PDF outputs on host volume</li>
<li><code>/backend/locale/</code>: translation catalogs</li>
<li><code>/docker-compose.yml</code>: local runtime orchestration</li>
<li><code>/Makefile</code>: repeatable translation commands</li>
<li><code>/.github/workflows/i18n.yml</code>: translation compile validation in CI</li>
</ul>
<h2 id="local">3) Local Development Workflow</h2>
<h3>Start</h3>
<pre><code>cd /Users/bostame/Documents/tubco-onboarding-offboarding-portal
docker compose up -d --build</code></pre>
<h3>Main URLs</h3>
<ul>
<li>App: <code>http://127.0.0.1:8088/</code></li>
<li>MailHog: <code>http://127.0.0.1:8025/</code></li>
<li>Health check: <code>http://127.0.0.1:8088/healthz/</code></li>
</ul>
<h3>Bootstrap users</h3>
<ul>
<li>Admin: <code>admin_test</code> / <code>admin12345</code></li>
<li>User: <code>user_test</code> / <code>user12345</code></li>
</ul>
<h2 id="docker">4) Docker Operations</h2>
<pre><code>docker compose up -d --build
docker compose restart web
docker compose restart worker
docker compose logs --no-color --tail=120 web
docker compose logs --no-color --tail=120 worker
docker compose down
docker compose down -v</code></pre>
<div class="note">
The source code is bind-mounted into the container. Most template/view/static changes only require a web restart, not a full rebuild. Image changes such as system packages require <code>docker compose up -d --build</code>.
</div>
<h2 id="db">5) Database and Migrations</h2>
<pre><code>docker compose exec -T web python manage.py makemigrations
docker compose exec -T web python manage.py migrate
docker compose exec -T web python manage.py check</code></pre>
<ul>
<li>Never edit or remove historical migrations casually.</li>
<li>When adding fields to builder-driven models, preserve fallback behavior for existing rows.</li>
<li>Fresh boot sequence runs migrations automatically in <code>entrypoint-web.sh</code>.</li>
</ul>
<h2 id="translations">6) Translation Workflow</h2>
<h3>Standard Django i18n path</h3>
<pre><code>make i18n-update-en
make i18n-compile</code></pre>
<p>Equivalent raw commands:</p>
<pre><code>docker compose exec -T web django-admin makemessages -l en
docker compose exec -T web django-admin compilemessages</code></pre>
<ul>
<li><code>gettext</code> is installed in the Docker image.</li>
<li>Do not use custom ad hoc <code>.mo</code> compilation anymore.</li>
<li>Phase 1 bilingual support covers fixed UI.</li>
<li>Phase 2 covers builder-driven labels and checklist items with explicit German and English fields.</li>
</ul>
<h2 id="pdf">7) PDF Pipeline</h2>
<ul>
<li>PDF generation is HTML-to-PDF using <code>xhtml2pdf</code>.</li>
<li>Letterhead overlay is applied from <code>templates.pdf</code>.</li>
<li>Main logic lives in <code>backend/workflows/tasks.py</code>.</li>
<li>Key templates:
<ul>
<li><code>onboarding_template.html</code></li>
<li><code>offboarding_template.html</code></li>
<li><code>onboarding_intro_template.html</code></li>
<li><code>onboarding_intro_session_pdf.html</code></li>
</ul>
</li>
</ul>
<div class="note">
xhtml2pdf is sensitive to layout complexity. Keep print templates conservative and verify every structural change with a real generated PDF.
</div>
<h2 id="email">8) Email Pipeline</h2>
<ul>
<li>Notification defaults are defined in <code>DEFAULT_NOTIFICATION_TEMPLATES</code> in <code>tasks.py</code>.</li>
<li>Admin-configured overrides live in <code>NotificationTemplate</code> and <code>NotificationRule</code>.</li>
<li>Mail sending uses Celery tasks and supports test mode redirection.</li>
<li>MailHog is the local verification path when test mode is active.</li>
</ul>
<h2 id="nextcloud">9) Nextcloud Integration</h2>
<ul>
<li>Configured from Admin Apps → Integrations.</li>
<li>Upload logic lives in <code>backend/workflows/services.py</code>.</li>
<li>Feature can be globally toggled without changing environment variables.</li>
<li>Failures should degrade gracefully and not block request persistence.</li>
</ul>
<h2 id="builders">10) Builder Architecture</h2>
<h3>Form Builder</h3>
<ul>
<li>Model: <code>FormFieldConfig</code> + <code>FormOption</code></li>
<li>Controls field order, visibility, required flags, option sets, and bilingual label/help-text overrides</li>
</ul>
<h3>Intro Builder</h3>
<ul>
<li>Model: <code>IntroChecklistItem</code></li>
<li>Controls additional checklist items used by intro PDF and live intro checklist</li>
<li>Now supports bilingual DE/EN labels</li>
</ul>
<div class="note">
Dynamic content should use explicit DE/EN fields with German fallback, not machine translation at runtime.
</div>
<h2 id="testing">11) Testing and Validation</h2>
<pre><code>docker compose exec -T web python manage.py check
docker compose exec -T web python manage.py test
docker compose exec -T web python manage.py run_staging_e2e_check</code></pre>
<ul>
<li>Use <code>check</code> after model/view/template changes.</li>
<li>Use targeted shell checks for render validation when changing templates or routes.</li>
<li>Use real PDF generation tests when changing PDF templates or intro/offboarding document logic.</li>
</ul>
<h2 id="deploy">12) Deployment and Release Checklist</h2>
<ol>
<li>Run <code>manage.py check</code></li>
<li>Run tests or targeted verification</li>
<li>Run translation compile step</li>
<li>Generate at least one onboarding/offboarding PDF if PDF templates changed</li>
<li>Verify MailHog or SMTP path if email behavior changed</li>
<li>Verify Nextcloud upload if integration behavior changed</li>
<li>Update Project Wiki and Developer Handbook if architecture/workflow changed</li>
<li>Take a snapshot commit before major next-phase work</li>
</ol>
<h2 id="troubleshooting">13) Troubleshooting</h2>
<ul>
<li><strong>Page looks stale:</strong> restart <code>web</code> and hard-refresh browser</li>
<li><strong>Second request hangs:</strong> inspect web logs and verify health endpoint</li>
<li><strong>PDF looks unchanged:</strong> regenerate the PDF; browser may cache old file names unless the path changes</li>
<li><strong>Language switch not visible:</strong> verify translation catalog compiled and restart web</li>
<li><strong>Mail not visible:</strong> check MailHog on port <code>8025</code> and test/production mode toggle</li>
<li><strong>Nextcloud sync unclear:</strong> verify config in Integrations page and inspect service logs</li>
</ul>
<h2 id="security">14) Security and Maintenance Notes</h2>
<ul>
<li>Containers run as non-root <code>app</code> user.</li>
<li>Keep secrets in <code>.env</code>, not in tracked files.</li>
<li>Avoid destructive git operations in a dirty repo.</li>
<li>Prefer standard framework workflows over custom one-off maintenance scripts.</li>
<li>When adding new features, document them in both the Project Wiki and this handbook if they change engineering workflow.</li>
</ul>
</div>
</body>
</html>

View File

@@ -11,7 +11,7 @@
<body>
<div class="shell">
<div class="topbar">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<a class="btn btn-secondary" href="/">Zur Startseite</a>
</div>
@@ -81,7 +81,8 @@
{% csrf_token %}
<input type="hidden" name="builder_action" value="add_option" />
<input type="hidden" name="category" value="{{ selected_option_category }}" />
<input type="text" name="label" placeholder="Neuer Optionsname" required />
<input type="text" name="label" placeholder="Label (DE)" required />
<input type="text" name="label_en" placeholder="Label (EN, optional)" />
<input type="text" name="value" placeholder="Technischer Wert (optional)" />
<button class="btn btn-primary" type="submit">Option hinzufügen</button>
</form>
@@ -93,7 +94,8 @@
<thead>
<tr>
<th>Sortierung</th>
<th>Label</th>
<th>Label (DE)</th>
<th>Label (EN)</th>
<th>Value</th>
<th>Aktiv</th>
<th>Löschen</th>
@@ -107,6 +109,7 @@
<span class="drag-handle" title="Ziehen zum Sortieren">⋮⋮</span>
</td>
<td><input type="text" name="label_{{ item.id }}" value="{{ item.label }}" required /></td>
<td><input type="text" name="label_en_{{ item.id }}" value="{{ item.label_en }}" /></td>
<td><input type="text" name="value_{{ item.id }}" value="{{ item.value }}" /></td>
<td><input type="checkbox" name="active_{{ item.id }}" {% if item.is_active %}checked{% endif %} /></td>
<td>
@@ -114,7 +117,7 @@
</td>
</tr>
{% empty %}
<tr><td colspan="5">Keine Optionen in dieser Kategorie.</td></tr>
<tr><td colspan="6">Keine Optionen in dieser Kategorie.</td></tr>
{% endfor %}
</tbody>
</table>
@@ -124,6 +127,47 @@
</div>
</form>
</section>
<section class="options-panel">
<div class="options-head">
<h2>Feldtexte verwalten</h2>
</div>
<form method="post" action="/admin-tools/form-builder/?form_type={{ form_type }}&option_category={{ selected_option_category }}">
{% csrf_token %}
<div class="option-table-wrap">
<table class="option-table">
<thead>
<tr>
<th>Feld</th>
<th>Label (DE)</th>
<th>Label (EN)</th>
<th>Hilfetext (DE)</th>
<th>Hilfetext (EN)</th>
</tr>
</thead>
<tbody>
{% for item in field_text_items %}
<tr>
<td>
<input type="hidden" name="field_ids" value="{{ item.id }}" />
<strong>{{ item.field_name }}</strong>
</td>
<td><input type="text" name="label_override_{{ item.id }}" value="{{ item.label_override }}" placeholder="Fallback: Standardlabel" /></td>
<td><input type="text" name="label_override_en_{{ item.id }}" value="{{ item.label_override_en }}" placeholder="English label" /></td>
<td><input type="text" name="help_text_override_{{ item.id }}" value="{{ item.help_text_override }}" placeholder="Optionaler Hilfetext" /></td>
<td><input type="text" name="help_text_override_en_{{ item.id }}" value="{{ item.help_text_override_en }}" placeholder="Optional English help text" /></td>
</tr>
{% empty %}
<tr><td colspan="5">Keine Feldkonfigurationen verfügbar.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="options-actions">
<button class="btn btn-primary" type="submit" name="builder_action" value="save_field_texts">Feldtexte speichern</button>
</div>
</form>
</section>
</div>
<script src="{% static 'workflows/js/form_builder.js' %}"></script>

View File

@@ -0,0 +1,69 @@
{% load static i18n %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Handbook</title>
<link rel="stylesheet" href="{% static 'workflows/css/buttons.css' %}" />
<style>
body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #1b2b43; padding: 20px; }
.shell { max-width: 1120px; margin: 0 auto; background: #fff; border: 1px solid #d7e0ea; border-radius: 14px; padding: 18px; }
.brand-logo { width: 190px; max-width: 100%; height: auto; margin: 0 0 10px; display: block; }
.top { display: flex; justify-content: space-between; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
h1 { margin: 0; color: #000078; font-size: 30px; }
.sub { margin: 8px 0 18px; color: #5f6f85; max-width: 760px; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(280px, 1fr)); gap: 14px; }
.card { border: 1px solid #d7e0ea; border-radius: 14px; background: #fcfdff; padding: 16px; }
.eyebrow { display: inline-block; padding: 5px 10px; border-radius: 999px; background: #eef4ff; color: #244a8f; border: 1px solid #d5e2f9; font-size: 12px; font-weight: 700; margin-bottom: 10px; }
h2 { margin: 0 0 8px; color: #113a74; }
p { margin: 0 0 14px; color: #5f6f85; }
ul { margin: 0 0 14px 18px; color: #334155; }
li { margin: 4px 0; }
.actions { display: flex; gap: 8px; flex-wrap: wrap; }
@media (max-width: 760px) { .grid { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="shell">
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<div class="top">
<h1>Handbook</h1>
<a class="btn btn-secondary" href="/">Back to Home</a>
</div>
<p class="sub">Single documentation entry point for both operational knowledge and long-term engineering knowledge.</p>
<div class="grid">
<section class="card">
<div class="eyebrow">Operations</div>
<h2>Project Wiki</h2>
<p>Operational and product-level documentation for onboarding, offboarding, PDFs, integrations, admin tools, and system behavior.</p>
<ul>
<li>workflow overview</li>
<li>admin tools and system behavior</li>
<li>integrations and operations</li>
<li>runbook and troubleshooting</li>
</ul>
<div class="actions">
<a class="btn btn-secondary" href="/admin-tools/wiki/">Open Project Wiki</a>
</div>
</section>
<section class="card">
<div class="eyebrow">Engineering</div>
<h2>Developer Handbook</h2>
<p>Engineering documentation for architecture, local setup, Docker, migrations, translations, deployment, testing, and long-term maintenance.</p>
<ul>
<li>repository and service structure</li>
<li>Docker and migration workflow</li>
<li>translation and builder architecture</li>
<li>deployment, security, and maintenance notes</li>
</ul>
<div class="actions">
<a class="btn btn-secondary" href="/admin-tools/developer-handbook/">Open Developer Handbook</a>
</div>
</section>
</div>
</div>
</body>
</html>

View File

@@ -56,12 +56,21 @@
background: #fff;
}
.brand-wrap {
display: flex;
flex-direction: column;
gap: 8px;
flex: 0 0 auto;
min-width: 0;
}
.brand-logo {
width: 210px;
max-width: 100%;
height: auto;
display: block;
margin: 0;
flex: 0 0 auto;
}
.quick-actions {
@@ -436,7 +445,9 @@
<body>
<div class="shell">
<div class="topbar">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<div class="brand-wrap">
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
</div>
<div class="quick-actions">
<form method="post" action="{% url 'set_language' %}" class="lang-switch">
{% csrf_token %}
@@ -549,9 +560,9 @@
<a class="btn btn-secondary" href="/admin-tools/intro-builder/">{% trans "Öffnen" %}</a>
</section>
<section class="admin-card">
<h3>{% trans "Projekt Wiki" %}</h3>
<p>{% trans "Dokumentation, Architektur und Runbook." %}</p>
<a class="btn btn-secondary" href="/admin-tools/wiki/">{% trans "Öffnen" %}</a>
<h3>{% trans "Handbook" %}</h3>
<p>{% trans "Project wiki and developer documentation in one place." %}</p>
<a class="btn btn-secondary" href="/admin-tools/handbook/">{% trans "Öffnen" %}</a>
</section>
<section class="admin-card">
<h3>{% trans "Integrationen" %}</h3>

View File

@@ -73,7 +73,7 @@
<body>
<div class="shell">
<div class="topbar">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<a class="btn btn-secondary" href="/">Zur Startseite</a>
</div>
<h1>Integrationen Setup</h1>

View File

@@ -35,7 +35,7 @@
<body>
<div class="shell">
<div class="topbar">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<a class="btn btn-secondary" href="/">Zur Startseite</a>
</div>
@@ -66,9 +66,13 @@
</select>
</div>
<div class="field">
<label for="label">Checklistenpunkt</label>
<label for="label">Checklistenpunkt (DE)</label>
<input id="label" name="label" placeholder="z. B. Nextcloud Ordnerstruktur erklärt" required />
</div>
<div class="field">
<label for="label_en">Checklist item (EN)</label>
<input id="label_en" name="label_en" placeholder="e.g. Nextcloud folder structure explained" />
</div>
<div class="actions">
<button class="btn btn-primary" type="submit">Punkt hinzufügen</button>
</div>
@@ -85,7 +89,8 @@
<tr>
<th>Sortierung</th>
<th>Abschnitt</th>
<th>Checklistenpunkt</th>
<th>Checklistenpunkt (DE)</th>
<th>Checklistenpunkt (EN)</th>
<th>Feld-Bedingung</th>
<th>Operator</th>
<th>Wert</th>
@@ -108,6 +113,7 @@
</select>
</td>
<td><input type="text" name="label_{{ item.id }}" value="{{ item.label }}" required /></td>
<td><input type="text" name="label_en_{{ item.id }}" value="{{ item.label_en }}" /></td>
<td>
<select name="field_{{ item.id }}">
{% for value, label in condition_field_choices %}
@@ -129,7 +135,7 @@
</td>
</tr>
{% empty %}
<tr><td colspan="8">Noch keine benutzerdefinierten Checklistenpunkte angelegt. Solange die Liste leer ist, nutzt das System die integrierten Standardpunkte.</td></tr>
<tr><td colspan="9">Noch keine benutzerdefinierten Checklistenpunkte angelegt. Solange die Liste leer ist, nutzt das System die integrierten Standardpunkte.</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -19,7 +19,7 @@
</div>
<div class="wrap">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<div class="top-link" style="display:flex; gap:8px; align-items:center;">
<form method="post" action="{% url 'set_language' %}" class="lang-switch" style="display:flex; gap:6px;">
{% csrf_token %}

View File

@@ -19,7 +19,7 @@
</div>
<div class="top-wrap">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<div class="top-link" style="display:flex; gap:8px; align-items:center;">
<form method="post" action="{% url 'set_language' %}" class="lang-switch" style="display:flex; gap:6px;">
{% csrf_token %}

View File

@@ -61,7 +61,7 @@
<body>
<div class="shell">
<div class="topbar">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<div class="top-actions">
<a class="btn btn-secondary" href="/requests/">Zum Dashboard</a>
<a class="btn btn-secondary" href="/">Zur Startseite</a>

View File

@@ -26,7 +26,7 @@
</head>
<body>
<div class="shell">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<div class="top">
<h1>Project Wiki</h1>
<a class="btn btn-secondary" href="/">Back to Home</a>
@@ -165,7 +165,9 @@
<ul>
<li><strong>Current scope:</strong> the core user interface supports German and English switching for the main fixed UI pages.</li>
<li><strong>Covered now:</strong> login, home, requests dashboard, onboarding form shell, offboarding form shell, and common status/messages in views.</li>
<li><strong>Not fully bilingual yet:</strong> dynamic Form Builder content, intro-builder item labels, admin-configured email templates, and most generated PDF/business text remain primarily single-language.</li>
<li><strong>Phase 2 added:</strong> dynamic Form Builder option labels, field label/help-text overrides, and intro-builder checklist item labels now support German and English values.</li>
<li><strong>Editing path:</strong> these DE/EN values are maintained directly in the frontend builder pages, not only in Django admin.</li>
<li><strong>Not fully bilingual yet:</strong> admin-configured email templates and several generated PDF/business text blocks still remain primarily single-language.</li>
<li><strong>Implementation:</strong> Django i18n with locale middleware, translation catalogs, and a DE/EN language switch in the main UI.</li>
</ul>
@@ -174,6 +176,8 @@
<li>The long-term translation path uses Django's standard <code>makemessages</code> and <code>compilemessages</code> workflow.</li>
<li><code>gettext</code> is installed in the Docker image so translations can be compiled inside the running container.</li>
<li>Translation catalogs live under <code>/backend/locale/</code>.</li>
<li>A root <code>Makefile</code> provides repeatable local commands for updating and compiling translations.</li>
<li>CI validates translation compilation on push and pull request via <code>.github/workflows/i18n.yml</code>.</li>
<li>The earlier ad hoc Python-based <code>.mo</code> compilation path should no longer be used for ongoing maintenance.</li>
</ul>

View File

@@ -877,7 +877,7 @@
<div class="shell">
<div class="topbar">
<div class="brand-wrap">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
</div>
<div class="quick-actions">
<form method="post" action="{% url 'set_language' %}" class="lang-switch">

View File

@@ -44,7 +44,7 @@
<body>
<div class="shell">
<div class="topbar">
<img class="brand-logo" src="https://tub.co/media/site/0856bfc615-1750234287/tubco-wortbildmarke.svg" alt="TUB/CO Logo" />
<img class="brand-logo" src="{% static 'workflows/img/tubco-logo.svg' %}" alt="TUB/CO Logo" />
<a class="btn btn-secondary" href="/">Zur Startseite</a>
</div>
<h1>Geplante Welcome E-Mails</h1>

View File

@@ -27,7 +27,9 @@ urlpatterns = [
path('admin-tools/welcome-emails/<int:schedule_id>/pause/', views.pause_welcome_email, name='pause_welcome_email'),
path('admin-tools/welcome-emails/<int:schedule_id>/resume/', views.resume_welcome_email, name='resume_welcome_email'),
path('admin-tools/welcome-emails/<int:schedule_id>/cancel/', views.cancel_welcome_email, name='cancel_welcome_email'),
path('admin-tools/handbook/', views.handbook_page, name='handbook_page'),
path('admin-tools/wiki/', views.project_wiki_page, name='project_wiki_page'),
path('admin-tools/developer-handbook/', views.developer_handbook_page, name='developer_handbook_page'),
path('admin-tools/form-builder/', views.form_builder_page, name='form_builder_page'),
path('admin-tools/form-builder/save-order/', views.form_builder_save_order, name='form_builder_save_order'),
path('admin-tools/intro-builder/', views.intro_builder_page, name='intro_builder_page'),

View File

@@ -16,6 +16,7 @@ from django.views.decorators.http import require_POST
from django.views.decorators.csrf import ensure_csrf_cookie
from django.utils import timezone
from django.utils.translation import gettext as _, gettext_lazy
from django.utils.translation import get_language
from .forms import OffboardingRequestForm, OnboardingRequestForm
from .form_builder import (
@@ -124,6 +125,10 @@ def _form_field_labels(form_type: str) -> dict[str, str]:
return {}
def _translate_choice_list(choices):
return [(value, str(label)) for value, label in choices]
def _build_onboarding_layout(form) -> list[dict]:
ordered_names = list(form.fields.keys())
group_by_field = {}
@@ -209,12 +214,24 @@ def home(request):
)
@login_required
@user_passes_test(_is_staff)
def handbook_page(request):
return render(request, 'workflows/handbook.html')
@login_required
@user_passes_test(_is_staff)
def project_wiki_page(request):
return render(request, 'workflows/project_wiki.html')
@login_required
@user_passes_test(_is_staff)
def developer_handbook_page(request):
return render(request, 'workflows/developer_handbook.html')
@login_required
def requests_dashboard(request):
if request.method == 'POST':
@@ -416,7 +433,7 @@ def onboarding_success(request, request_id: int):
@require_POST
def generate_onboarding_intro_pdf(request, request_id: int):
obj = get_object_or_404(OnboardingRequest, id=request_id)
pdf_path = _generate_onboarding_intro_pdf(obj)
pdf_path = _generate_onboarding_intro_pdf(obj, language_code=get_language())
obj.intro_pdf_path = str(pdf_path)
obj.save(update_fields=['intro_pdf_path'])
messages.success(request, _('Einweisungs- und Übergabeprotokoll wurde erzeugt.'))
@@ -429,7 +446,11 @@ def generate_onboarding_intro_pdf(request, request_id: int):
def generate_onboarding_intro_session_pdf(request, request_id: int):
onboarding = get_object_or_404(OnboardingRequest, id=request_id)
session, _created = OnboardingIntroductionSession.objects.get_or_create(onboarding_request=onboarding)
pdf_path = _generate_onboarding_intro_session_pdf(session, admin_signature_name=_display_user_name(request.user))
pdf_path = _generate_onboarding_intro_session_pdf(
session,
admin_signature_name=_display_user_name(request.user),
language_code=get_language(),
)
session.exported_pdf_path = str(pdf_path)
session.save(update_fields=['exported_pdf_path'])
messages.success(request, _('Einweisungsprotokoll aus Live-Status wurde erzeugt.'))
@@ -441,7 +462,7 @@ def generate_onboarding_intro_session_pdf(request, request_id: int):
def onboarding_intro_session_page(request, request_id: int):
onboarding = get_object_or_404(OnboardingRequest, id=request_id)
session, _created = OnboardingIntroductionSession.objects.get_or_create(onboarding_request=onboarding)
sections = build_intro_sections_for_request(onboarding)
sections = build_intro_sections_for_request(onboarding, language_code=get_language())
if request.method == 'POST':
checked_ids = set(request.POST.getlist('checked_items'))
@@ -576,6 +597,7 @@ def offboarding_success(request, request_id: int):
@login_required
@user_passes_test(_is_staff)
def form_builder_page(request):
language_code = get_language()
form_type = request.GET.get('form_type', 'onboarding')
if form_type not in DEFAULT_FIELD_ORDER:
form_type = 'onboarding'
@@ -600,6 +622,7 @@ def form_builder_page(request):
if action == 'add_option':
category = request.POST.get('category', '').strip()
label = request.POST.get('label', '').strip()
label_en = request.POST.get('label_en', '').strip()
value = request.POST.get('value', '').strip()
if category not in option_categories:
messages.error(request, 'Ungültige Kategorie.')
@@ -612,6 +635,7 @@ def form_builder_page(request):
FormOption.objects.create(
category=category,
label=label,
label_en=label_en,
value=value or label,
sort_order=(next_sort + 1) if next_sort is not None else 0,
is_active=True,
@@ -627,17 +651,31 @@ def form_builder_page(request):
continue
next_label = request.POST.get(f'label_{option.id}', '').strip() or option.label
option.label = next_label
option.label_en = request.POST.get(f'label_en_{option.id}', '').strip()
option.value = request.POST.get(f'value_{option.id}', '').strip() or next_label
option.is_active = request.POST.get(f'active_{option.id}') == 'on'
option.sort_order = pos
try:
option.save(update_fields=['label', 'value', 'is_active', 'sort_order'])
option.save(update_fields=['label', 'label_en', 'value', 'is_active', 'sort_order'])
except IntegrityError:
messages.error(request, f'Doppelte Bezeichnung in Kategorie: {next_label}')
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option.category}")
option_category = option.category
messages.success(request, 'Optionen wurden gespeichert.')
elif action == 'save_field_texts':
field_ids = request.POST.getlist('field_ids')
for raw_id in field_ids:
cfg = FormFieldConfig.objects.filter(id=raw_id, form_type=form_type).first()
if not cfg:
continue
cfg.label_override = (request.POST.get(f'label_override_{cfg.id}') or '').strip()
cfg.label_override_en = (request.POST.get(f'label_override_en_{cfg.id}') or '').strip()
cfg.help_text_override = (request.POST.get(f'help_text_override_{cfg.id}') or '').strip()
cfg.help_text_override_en = (request.POST.get(f'help_text_override_en_{cfg.id}') or '').strip()
cfg.save(update_fields=['label_override', 'label_override_en', 'help_text_override', 'help_text_override_en'])
messages.success(request, 'Feldtexte wurden gespeichert.')
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}")
default_names = list(DEFAULT_FIELD_ORDER.get(form_type, []))
@@ -676,7 +714,9 @@ def form_builder_page(request):
column_by_key[page_key]['items'].append(
{
'field_name': cfg.field_name,
'label': labels.get(cfg.field_name, cfg.field_name),
'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name),
'label_de': cfg.label_override or labels.get(cfg.field_name, cfg.field_name),
'label_en': cfg.label_override_en,
'is_visible': cfg.is_visible,
'is_required': cfg.is_required,
'locked': cfg.field_name in locked,
@@ -690,7 +730,9 @@ def form_builder_page(request):
'items': [
{
'field_name': cfg.field_name,
'label': labels.get(cfg.field_name, cfg.field_name),
'label': cfg.translated_label_override(language_code) or labels.get(cfg.field_name, cfg.field_name),
'label_de': cfg.label_override or labels.get(cfg.field_name, cfg.field_name),
'label_en': cfg.label_override_en,
'is_visible': cfg.is_visible,
'is_required': cfg.is_required,
'locked': cfg.field_name in locked,
@@ -707,9 +749,10 @@ def form_builder_page(request):
'form_type': form_type,
'columns': columns,
'form_types': [('onboarding', 'Onboarding'), ('offboarding', 'Offboarding')],
'option_categories': FormOption.CATEGORY_CHOICES,
'option_categories': _translate_choice_list(FormOption.CATEGORY_CHOICES),
'selected_option_category': option_category,
'option_items': FormOption.objects.filter(category=option_category).order_by('sort_order', 'label'),
'field_text_items': configs,
},
)
@@ -732,6 +775,7 @@ def intro_builder_page(request):
if action == 'add_item':
section = (request.POST.get('section') or '').strip()
label = (request.POST.get('label') or '').strip()
label_en = (request.POST.get('label_en') or '').strip()
if section not in {k for k, _ in IntroChecklistItem.SECTION_CHOICES}:
messages.error(request, 'Ungültiger Abschnitt.')
return redirect('intro_builder_page')
@@ -744,6 +788,7 @@ def intro_builder_page(request):
IntroChecklistItem.objects.create(
section=section,
label=label,
label_en=label_en,
sort_order=(next_sort + 1) if next_sort is not None else 0,
is_active=True,
condition_operator='always',
@@ -767,6 +812,7 @@ def intro_builder_page(request):
operator = 'always'
item.section = section
item.label = (request.POST.get(f'label_{item.id}') or item.label).strip() or item.label
item.label_en = (request.POST.get(f'label_en_{item.id}') or '').strip()
item.is_active = request.POST.get(f'active_{item.id}') == 'on'
item.condition_field = (request.POST.get(f'field_{item.id}') or '').strip()
item.condition_operator = operator
@@ -776,6 +822,7 @@ def intro_builder_page(request):
update_fields=[
'section',
'label',
'label_en',
'is_active',
'condition_field',
'condition_operator',
@@ -809,8 +856,8 @@ def intro_builder_page(request):
'workflows/intro_builder.html',
{
'items': items,
'section_choices': IntroChecklistItem.SECTION_CHOICES,
'operator_choices': IntroChecklistItem.OPERATOR_CHOICES,
'section_choices': _translate_choice_list(IntroChecklistItem.SECTION_CHOICES),
'operator_choices': _translate_choice_list(IntroChecklistItem.OPERATOR_CHOICES),
'condition_field_choices': condition_field_choices,
},
)