From 51700cfa8bbb1ca5d60f8de2222c48a4c7d88bc1 Mon Sep 17 00:00:00 2001 From: Md Bayazid Bostame Date: Thu, 26 Mar 2026 11:43:54 +0100 Subject: [PATCH] snapshot: preserve branding foundation and platform owner split --- backend/locale/en/LC_MESSAGES/django.mo | Bin 31194 -> 31810 bytes backend/locale/en/LC_MESSAGES/django.po | 572 ++++++++++++------ backend/workflows/admin.py | 7 +- backend/workflows/branding.py | 106 ++++ backend/workflows/context_processors.py | 5 +- backend/workflows/forms.py | 64 +- .../commands/bootstrap_initial_users.py | 4 +- .../migrations/0036_portalbranding.py | 33 + ...lter_portalbranding_logo_image_and_more.py | 24 + backend/workflows/models.py | 35 ++ backend/workflows/roles.py | 50 +- .../workflows/static/workflows/css/home.css | 28 + backend/workflows/tasks.py | 11 +- .../workflows/branding_settings.html | 70 +++ .../workflows/developer_handbook.html | 15 +- .../workflows/templates/workflows/home.html | 26 +- .../workflows/includes/app_header.html | 4 +- .../templates/workflows/project_wiki.html | 4 +- .../workflows/requests_dashboard.html | 4 +- .../templates/workflows/user_management.html | 4 +- backend/workflows/urls.py | 2 + backend/workflows/views.py | 140 ++++- 22 files changed, 966 insertions(+), 242 deletions(-) create mode 100644 backend/workflows/branding.py create mode 100644 backend/workflows/migrations/0036_portalbranding.py create mode 100644 backend/workflows/migrations/0037_alter_portalbranding_logo_image_and_more.py create mode 100644 backend/workflows/templates/workflows/branding_settings.html diff --git a/backend/locale/en/LC_MESSAGES/django.mo b/backend/locale/en/LC_MESSAGES/django.mo index f69d1a631a88264e9e713cee9cf78c091bc88852..46923b302d4e4c50f173911bca2a1ce4cd9592bc 100644 GIT binary patch delta 9595 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 diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index af32b22..8758ccb 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: tubco-portal\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-26 09:25+0000\n" +"POT-Creation-Date: 2026-03-26 10:38+0000\n" "PO-Revision-Date: 2026-03-24 00:00+0000\n" "Language: en\n" "MIME-Version: 1.0\n" @@ -57,6 +57,7 @@ msgstr "" #: workflows/forms.py:103 workflows/forms.py:128 #: workflows/templates/workflows/user_management.html:72 +#: workflows/templates/workflows/user_management.html:170 msgid "Benutzername" msgstr "" @@ -89,118 +90,163 @@ msgstr "" #: workflows/forms.py:130 workflows/templates/workflows/user_management.html:74 #: workflows/templates/workflows/user_management.html:93 +#: workflows/templates/workflows/user_management.html:171 #, fuzzy #| msgid "Rolle:" msgid "Rolle" msgstr "Role:" -#: workflows/forms.py:143 +#: workflows/forms.py:144 msgid "Dieser Benutzername ist bereits vergeben." msgstr "This username is already taken." -#: workflows/forms.py:152 workflows/views.py:472 +#: workflows/forms.py:153 workflows/views.py:567 msgid "Ungültige Rolle." msgstr "Invalid role." -#: workflows/forms.py:408 +#: workflows/forms.py:155 workflows/views.py:570 +msgid "Nur Platform Owner dürfen diese Rolle vergeben." +msgstr "" + +#: workflows/forms.py:186 +msgid "Portal-Titel" +msgstr "Portal title" + +#: workflows/forms.py:187 +msgid "Firmenname" +msgstr "Company name" + +#: workflows/forms.py:188 +msgid "Support-E-Mail" +msgstr "Support email" + +#: workflows/forms.py:189 +msgid "Standardsprache" +msgstr "Default language" + +#: workflows/forms.py:190 +msgid "Logo" +msgstr "Logo" + +#: workflows/forms.py:191 +msgid "PDF-Briefkopf" +msgstr "PDF letterhead" + +#: workflows/forms.py:192 +msgid "Primärfarbe" +msgstr "Primary color" + +#: workflows/forms.py:193 +msgid "Sekundärfarbe" +msgstr "Secondary color" + +#: workflows/forms.py:207 +msgid "Das Logo darf maximal 5 MB groß sein." +msgstr "" + +#: workflows/forms.py:215 +msgid "Der PDF-Briefkopf darf maximal 10 MB groß sein." +msgstr "" + +#: workflows/forms.py:458 #, python-format msgid "" "Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen " "(frühestens %(date)s)." msgstr "" -#: workflows/models.py:55 workflows/views.py:199 +#: workflows/models.py:90 workflows/views.py:199 msgid "Eingereicht" msgstr "Submitted" -#: workflows/models.py:56 workflows/views.py:200 +#: workflows/models.py:91 workflows/views.py:200 msgid "In Bearbeitung" msgstr "Processing" -#: workflows/models.py:57 workflows/models.py:372 workflows/views.py:201 +#: workflows/models.py:92 workflows/models.py:407 workflows/views.py:201 msgid "Abgeschlossen" msgstr "Completed" -#: workflows/models.py:58 workflows/models.py:312 +#: workflows/models.py:93 workflows/models.py:347 #: workflows/templates/workflows/backup_recovery.html:70 #: workflows/templates/workflows/requests_dashboard.html:222 #: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:202 msgid "Fehlgeschlagen" msgstr "Failed" -#: workflows/models.py:65 +#: workflows/models.py:100 msgid "Herr" msgstr "" -#: workflows/models.py:65 +#: workflows/models.py:100 msgid "Frau" msgstr "" -#: workflows/models.py:65 +#: workflows/models.py:100 msgid "Divers" msgstr "" -#: workflows/models.py:75 +#: workflows/models.py:110 msgid "befristet" msgstr "" -#: workflows/models.py:75 +#: workflows/models.py:110 msgid "unbefristet" msgstr "" -#: workflows/models.py:138 +#: workflows/models.py:173 #: workflows/templates/workflows/onboarding_intro_session.html:28 #: workflows/templates/workflows/requests_dashboard.html:145 msgid "Abteilung" msgstr "Department" -#: workflows/models.py:139 +#: workflows/models.py:174 msgid "Geräte" msgstr "" -#: workflows/models.py:140 +#: workflows/models.py:175 msgid "Software" msgstr "" -#: workflows/models.py:141 +#: workflows/models.py:176 #, fuzzy #| msgid "Vorgänge" msgid "Zugänge" msgstr "Requests" -#: workflows/models.py:142 +#: workflows/models.py:177 msgid "Workspace-Gruppen" msgstr "" -#: workflows/models.py:143 +#: workflows/models.py:178 msgid "Ressourcen" msgstr "" -#: workflows/models.py:144 +#: workflows/models.py:179 msgid "Telefonnummern" msgstr "" -#: workflows/models.py:170 +#: workflows/models.py:205 msgid "Automatisch" msgstr "" -#: workflows/models.py:171 workflows/views.py:94 +#: workflows/models.py:206 workflows/views.py:94 msgid "Stammdaten" msgstr "Master data" -#: workflows/models.py:172 workflows/views.py:95 +#: workflows/models.py:207 workflows/views.py:95 msgid "Vertrag" msgstr "Contract" -#: workflows/models.py:173 workflows/views.py:96 +#: workflows/models.py:208 workflows/views.py:96 msgid "IT-Setup" msgstr "IT setup" -#: workflows/models.py:174 workflows/views.py:97 +#: workflows/models.py:209 workflows/views.py:97 msgid "Abschluss" msgstr "Finish" -#: workflows/models.py:177 workflows/models.py:258 +#: workflows/models.py:212 workflows/models.py:293 #: workflows/templates/workflows/home.html:62 #: workflows/templates/workflows/onboarding_form.html:25 #: workflows/templates/workflows/requests_dashboard.html:68 @@ -208,259 +254,263 @@ msgstr "Finish" msgid "Onboarding" msgstr "Onboarding" -#: workflows/models.py:178 workflows/models.py:259 +#: workflows/models.py:213 workflows/models.py:294 #: workflows/templates/workflows/home.html:78 #: workflows/templates/workflows/requests_dashboard.html:78 #: workflows/templates/workflows/requests_dashboard.html:132 msgid "Offboarding" msgstr "Offboarding" -#: workflows/models.py:216 +#: workflows/models.py:251 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: IT" msgstr "Onboarding" -#: workflows/models.py:217 +#: workflows/models.py:252 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Onboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:218 +#: workflows/models.py:253 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Visitenkarte" msgstr "Start onboarding" -#: workflows/models.py:219 +#: workflows/models.py:254 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: HR Works" msgstr "Onboarding" -#: workflows/models.py:220 +#: workflows/models.py:255 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Schlüssel" msgstr "Start onboarding" -#: workflows/models.py:221 +#: workflows/models.py:256 msgid "Onboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:222 +#: workflows/models.py:257 #, fuzzy #| msgid "Welcome E-Mails" msgid "Onboarding: Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/models.py:223 +#: workflows/models.py:258 #, fuzzy #| msgid "Offboarding" msgid "Offboarding: IT" msgstr "Offboarding" -#: workflows/models.py:224 +#: workflows/models.py:259 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Offboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:225 +#: workflows/models.py:260 #, fuzzy #| msgid "Offboarding starten" msgid "Offboarding: HR Works Deaktivierung" msgstr "Start offboarding" -#: workflows/models.py:226 +#: workflows/models.py:261 msgid "Offboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:262 +#: workflows/models.py:297 msgid "Immer" msgstr "" -#: workflows/models.py:263 workflows/models.py:341 +#: workflows/models.py:298 workflows/models.py:376 msgid "Enthält" msgstr "" -#: workflows/models.py:264 workflows/models.py:342 +#: workflows/models.py:299 workflows/models.py:377 msgid "Ist gleich" msgstr "" -#: workflows/models.py:265 +#: workflows/models.py:300 msgid "Ist aktiv/Ja" msgstr "" -#: workflows/models.py:266 +#: workflows/models.py:301 #, fuzzy #| msgid "inaktiv" msgid "Ist inaktiv/Nein" msgstr "inactive" -#: workflows/models.py:308 +#: workflows/models.py:343 #: workflows/templates/workflows/welcome_emails.html:100 msgid "Geplant" msgstr "Scheduled" -#: workflows/models.py:309 +#: workflows/models.py:344 #: workflows/templates/workflows/welcome_emails.html:102 msgid "Pausiert" msgstr "Paused" -#: workflows/models.py:310 +#: workflows/models.py:345 #: workflows/templates/workflows/welcome_emails.html:104 msgid "Abgebrochen" msgstr "Cancelled" -#: workflows/models.py:311 +#: workflows/models.py:346 #: workflows/templates/workflows/welcome_emails.html:106 msgid "Gesendet" msgstr "Sent" -#: workflows/models.py:334 workflows/tasks.py:575 +#: workflows/models.py:369 workflows/tasks.py:576 msgid "Geräte und Arbeitsplatz" msgstr "Devices and workplace" -#: workflows/models.py:335 workflows/tasks.py:576 +#: workflows/models.py:370 workflows/tasks.py:577 msgid "Konten und Berechtigungen" msgstr "Accounts and permissions" -#: workflows/models.py:336 workflows/tasks.py:577 +#: workflows/models.py:371 workflows/tasks.py:578 msgid "Software und Tools" msgstr "Software and tools" -#: workflows/models.py:337 workflows/tasks.py:578 +#: workflows/models.py:372 workflows/tasks.py:579 msgid "Prozesse und Hinweise" msgstr "Processes and notes" -#: workflows/models.py:340 +#: workflows/models.py:375 msgid "Immer anzeigen" msgstr "Always show" -#: workflows/models.py:343 +#: workflows/models.py:378 msgid "Ist Ja / aktiv" msgstr "Is yes / active" -#: workflows/models.py:344 +#: workflows/models.py:379 msgid "Ist Nein / inaktiv" msgstr "Is no / inactive" -#: workflows/models.py:371 +#: workflows/models.py:406 msgid "Entwurf" msgstr "Draft" -#: workflows/models.py:391 +#: workflows/models.py:426 #, fuzzy #| msgid "Nextcloud:" msgid "Nextcloud" msgstr "Nextcloud:" -#: workflows/models.py:392 +#: workflows/models.py:427 msgid "S3" msgstr "" -#: workflows/models.py:393 +#: workflows/models.py:428 msgid "NFS" msgstr "" -#: workflows/roles.py:20 +#: workflows/roles.py:22 +msgid "Platform Owner" +msgstr "" + +#: workflows/roles.py:23 msgid "Super Admin" msgstr "Super Admin" -#: workflows/roles.py:21 +#: workflows/roles.py:24 msgid "Admin" msgstr "Admin" -#: workflows/roles.py:22 +#: workflows/roles.py:25 msgid "IT Staff" msgstr "IT Staff" -#: workflows/roles.py:23 +#: workflows/roles.py:26 msgid "Mitarbeiter" msgstr "Staff" -#: workflows/tasks.py:591 +#: workflows/tasks.py:592 #, python-format msgid "%(item)s übergeben und Grundfunktionen erklärt" msgstr "%(item)s handed over and basic functions explained" -#: workflows/tasks.py:593 +#: workflows/tasks.py:594 #, python-format msgid "%(item)s gezeigt bzw. Nutzung erklärt" msgstr "%(item)s shown or usage explained" -#: workflows/tasks.py:595 +#: workflows/tasks.py:596 #, python-format msgid "Telefonnummer / Direktwahl erklärt: %(value)s" msgstr "Phone number / direct extension explained: %(value)s" -#: workflows/tasks.py:597 +#: workflows/tasks.py:598 msgid "Arbeitsplatz, Geräte und allgemeine Nutzung besprochen" msgstr "Workplace, devices, and general usage reviewed" -#: workflows/tasks.py:599 +#: workflows/tasks.py:600 #, python-format msgid "%(item)s Zugang erklärt" msgstr "%(item)s access explained" -#: workflows/tasks.py:600 +#: workflows/tasks.py:601 #, python-format msgid "%(item)s Gruppe / Berechtigung erläutert" msgstr "%(item)s group / permission explained" -#: workflows/tasks.py:602 +#: workflows/tasks.py:603 #, python-format msgid "Dienstliche E-Mail-Adresse erläutert: %(value)s" msgstr "Work email address explained: %(value)s" -#: workflows/tasks.py:604 +#: workflows/tasks.py:605 #, python-format msgid "Gruppenpostfach erklärt: %(item)s" msgstr "Group mailbox explained: %(item)s" -#: workflows/tasks.py:606 +#: workflows/tasks.py:607 msgid "Zugänge, Konten und Anmeldelogik besprochen" msgstr "Accesses, accounts, and login logic reviewed" -#: workflows/tasks.py:608 +#: workflows/tasks.py:609 #, python-format msgid "%(item)s Einführung durchgeführt" msgstr "%(item)s introduction completed" -#: workflows/tasks.py:609 +#: workflows/tasks.py:610 #, python-format msgid "%(item)s zusätzlich besprochen" msgstr "%(item)s discussed additionally" -#: workflows/tasks.py:611 +#: workflows/tasks.py:612 msgid "Benötigte Standardsoftware und tägliche Nutzung erklärt" msgstr "Required standard software and daily usage explained" -#: workflows/tasks.py:614 +#: workflows/tasks.py:615 msgid "Passwortregeln und sicherer Umgang besprochen" msgstr "Password rules and secure handling reviewed" -#: workflows/tasks.py:615 +#: workflows/tasks.py:616 msgid "Dateiablage, Nextcloud und Freigaben erklärt" msgstr "File storage, Nextcloud, and sharing explained" -#: workflows/tasks.py:616 +#: workflows/tasks.py:617 msgid "Kommunikationswege und Support-Prozess erklärt" msgstr "Communication channels and support process explained" -#: workflows/tasks.py:619 +#: workflows/tasks.py:620 #, python-format msgid "%(item)s als zusätzliche Ausstattung besprochen" msgstr "%(item)s discussed as additional equipment" -#: workflows/tasks.py:621 +#: workflows/tasks.py:622 #, python-format msgid "Zusätzlicher Zugang besprochen: %(item)s" msgstr "Additional access discussed: %(item)s" -#: workflows/tasks.py:623 +#: workflows/tasks.py:624 #, python-format msgid "Übergabe-/Nachfolgekontext besprochen: %(value)s" msgstr "Handover / successor context reviewed: %(value)s" @@ -507,13 +557,15 @@ msgstr "Password saved" msgid "" "Ihr Passwort wurde erfolgreich gesetzt. Sie können sich jetzt mit Ihrem " "Benutzerkonto anmelden." -msgstr "Your password has been set successfully. You can now sign in with your account." +msgstr "" +"Your password has been set successfully. You can now sign in with your " +"account." #: workflows/templates/registration/password_reset_complete.html:19 #: workflows/templates/registration/password_reset_confirm.html:41 #: workflows/templates/registration/password_reset_done.html:19 #: workflows/templates/workflows/auth/password_reset_complete.html:19 -#: workflows/templates/workflows/auth/password_reset_confirm.html:44 +#: workflows/templates/workflows/auth/password_reset_confirm.html:43 #: workflows/templates/workflows/auth/password_reset_done.html:19 msgid "Zur Anmeldung" msgstr "Back to sign in" @@ -541,17 +593,17 @@ msgid "Bitte prüfen Sie die beiden Passwortfelder und versuchen Sie es erneut." msgstr "Please check both password fields and try again." #: workflows/templates/registration/password_reset_confirm.html:36 -#: workflows/templates/workflows/auth/password_reset_confirm.html:39 +#: workflows/templates/workflows/auth/password_reset_confirm.html:38 msgid "Passwort speichern" msgstr "Save password" #: workflows/templates/registration/password_reset_confirm.html:39 -#: workflows/templates/workflows/auth/password_reset_confirm.html:42 +#: workflows/templates/workflows/auth/password_reset_confirm.html:41 msgid "Link ungültig" msgstr "Invalid link" #: workflows/templates/registration/password_reset_confirm.html:40 -#: workflows/templates/workflows/auth/password_reset_confirm.html:43 +#: workflows/templates/workflows/auth/password_reset_confirm.html:42 msgid "" "Dieser Link ist nicht mehr gültig. Bitte fordern Sie einen neuen Passwort-" "Link an." @@ -569,7 +621,9 @@ msgstr "Email sent" msgid "" "Wenn ein passendes Konto existiert, wurde ein Passwort-Link an die " "hinterlegte E-Mail-Adresse verschickt." -msgstr "If a matching account exists, a password link has been sent to the stored email address." +msgstr "" +"If a matching account exists, a password link has been sent to the stored " +"email address." #: workflows/templates/registration/password_reset_form.html:4 #: workflows/templates/registration/password_reset_form.html:17 @@ -583,7 +637,9 @@ msgstr "Reset password" msgid "" "Geben Sie Ihre E-Mail-Adresse ein. Wenn ein Konto vorhanden ist, erhalten " "Sie einen Passwort-Link." -msgstr "Enter your email address. If an account exists, you will receive a password link." +msgstr "" +"Enter your email address. If an account exists, you will receive a password " +"link." #: workflows/templates/registration/password_reset_form.html:24 #: workflows/templates/workflows/auth/password_reset_form.html:25 @@ -592,7 +648,7 @@ msgstr "Request link" #: workflows/templates/workflows/audit_log.html:4 #: workflows/templates/workflows/audit_log.html:15 -#: workflows/templates/workflows/home.html:132 +#: workflows/templates/workflows/home.html:148 msgid "Audit Log" msgstr "" @@ -604,6 +660,7 @@ msgstr "" #: workflows/templates/workflows/audit_log.html:54 #: workflows/templates/workflows/backup_recovery.html:43 #: workflows/templates/workflows/requests_dashboard.html:193 +#: workflows/templates/workflows/user_management.html:156 #: workflows/templates/workflows/welcome_emails.html:87 msgid "Aktion" msgstr "Action" @@ -644,6 +701,7 @@ msgid "Zurücksetzen" msgstr "Reset" #: workflows/templates/workflows/audit_log.html:52 +#: workflows/templates/workflows/user_management.html:155 msgid "Zeit" msgstr "" @@ -659,6 +717,7 @@ msgid "Ziel" msgstr "" #: workflows/templates/workflows/audit_log.html:57 +#: workflows/templates/workflows/user_management.html:159 msgid "Details" msgstr "" @@ -688,7 +747,7 @@ msgstr "No requests available yet." #: workflows/templates/workflows/backup_recovery.html:4 #: workflows/templates/workflows/backup_recovery.html:12 -#: workflows/templates/workflows/home.html:139 +#: workflows/templates/workflows/home.html:155 msgid "Backup & Recovery" msgstr "Backup & Recovery" @@ -855,9 +914,53 @@ msgstr "Action in progress" msgid "Die Aktion wird im aktuellen Tab ausgeführt." msgstr "The action is running in the current tab." +#: workflows/templates/workflows/branding_settings.html:4 +#: workflows/templates/workflows/branding_settings.html:12 +#: workflows/templates/workflows/home.html:118 +msgid "Branding" +msgstr "Branding" + +#: workflows/templates/workflows/branding_settings.html:13 +msgid "Portalname, Firmenauftritt, Logo und PDF-Briefkopf zentral verwalten." +msgstr "" +"Manage portal name, company branding, logo, and PDF letterhead centrally." + +#: workflows/templates/workflows/branding_settings.html:48 +msgid "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB." +msgstr "" + +#: workflows/templates/workflows/branding_settings.html:51 +msgid "Aktuelles Logo:" +msgstr "Current logo:" + +#: workflows/templates/workflows/branding_settings.html:51 +#: workflows/templates/workflows/branding_settings.html:60 +msgid "öffnen" +msgstr "open" + +#: workflows/templates/workflows/branding_settings.html:57 +msgid "Erlaubtes Format: PDF. Maximal 10 MB." +msgstr "" + +#: workflows/templates/workflows/branding_settings.html:60 +msgid "Aktueller Briefkopf:" +msgstr "Current letterhead:" + +#: workflows/templates/workflows/branding_settings.html:65 +msgid "" +"TUBCO bleibt als Standard erhalten, bis hier Werte geändert oder Dateien " +"hochgeladen werden." +msgstr "" +"TUBCO remains the default until values are changed or files are uploaded " +"here." + +#: workflows/templates/workflows/branding_settings.html:66 +msgid "Branding speichern" +msgstr "Save branding" + #: workflows/templates/workflows/form_builder.html:4 #: workflows/templates/workflows/form_builder.html:14 -#: workflows/templates/workflows/home.html:153 +#: workflows/templates/workflows/home.html:169 msgid "Form Builder" msgstr "Form Builder" @@ -982,7 +1085,7 @@ msgstr "Save field text" #: workflows/templates/workflows/handbook.html:4 #: workflows/templates/workflows/handbook.html:15 -#: workflows/templates/workflows/home.html:165 +#: workflows/templates/workflows/home.html:181 msgid "Handbook" msgstr "Handbook" @@ -1103,12 +1206,6 @@ msgstr "" msgid "Open Release Checklist" msgstr "" -#: workflows/templates/workflows/home.html:4 -#: workflows/templates/workflows/home.html:35 -#: workflows/templates/workflows/requests_dashboard.html:303 -msgid "TUBCO Onboarding & Offboarding Portal" -msgstr "TUBCO Onboarding & Offboarding Portal" - #: workflows/templates/workflows/home.html:26 msgid "Abmelden" msgstr "Log out" @@ -1244,88 +1341,103 @@ msgstr "PDF access" msgid "Dashboard öffnen" msgstr "Open dashboard" -#: workflows/templates/workflows/home.html:112 -msgid "Admin Apps" -msgstr "Admin Apps" - #: workflows/templates/workflows/home.html:113 -msgid "Konfiguration, Tests und Steuerung." +msgid "Platform Apps" +msgstr "" + +#: workflows/templates/workflows/home.html:114 +#, fuzzy +#| msgid "Konfiguration, Tests und Steuerung." +msgid "Produktweite Konfiguration und Produktsteuerung." msgstr "Configuration, tests, and controls." -#: workflows/templates/workflows/home.html:118 -msgid "Integrationen" -msgstr "Integrations" - #: workflows/templates/workflows/home.html:119 -msgid "Nextcloud- und E-Mail-Setup." -msgstr "Nextcloud and email setup." +msgid "Logo, Portalname, Farben und PDF-Briefkopf verwalten." +msgstr "Manage logo, portal name, colors, and PDF letterhead." #: workflows/templates/workflows/home.html:120 -#: workflows/templates/workflows/home.html:127 -#: workflows/templates/workflows/home.html:134 -#: workflows/templates/workflows/home.html:141 -#: workflows/templates/workflows/home.html:148 -#: workflows/templates/workflows/home.html:155 -#: workflows/templates/workflows/home.html:160 -#: workflows/templates/workflows/home.html:167 -#: workflows/templates/workflows/home.html:174 +#: workflows/templates/workflows/home.html:136 +#: workflows/templates/workflows/home.html:143 +#: workflows/templates/workflows/home.html:150 +#: workflows/templates/workflows/home.html:157 +#: workflows/templates/workflows/home.html:164 +#: workflows/templates/workflows/home.html:171 +#: workflows/templates/workflows/home.html:176 +#: workflows/templates/workflows/home.html:183 +#: workflows/templates/workflows/home.html:190 msgid "Öffnen" msgstr "Open" -#: workflows/templates/workflows/home.html:125 +#: workflows/templates/workflows/home.html:128 +msgid "Admin Apps" +msgstr "Admin Apps" + +#: workflows/templates/workflows/home.html:129 +msgid "Konfiguration, Tests und Steuerung." +msgstr "Configuration, tests, and controls." + +#: workflows/templates/workflows/home.html:134 +msgid "Integrationen" +msgstr "Integrations" + +#: workflows/templates/workflows/home.html:135 +msgid "Nextcloud- und E-Mail-Setup." +msgstr "Nextcloud and email setup." + +#: workflows/templates/workflows/home.html:141 #: workflows/templates/workflows/user_management.html:4 #: workflows/templates/workflows/user_management.html:14 msgid "Benutzer & Rollen" msgstr "Users & roles" -#: workflows/templates/workflows/home.html:126 +#: workflows/templates/workflows/home.html:142 msgid "Benutzer anlegen, Rollen zuweisen und Zugriffe steuern." msgstr "Create users, assign roles, and control access." -#: workflows/templates/workflows/home.html:133 +#: workflows/templates/workflows/home.html:149 msgid "Wichtige Admin-Aktionen nachvollziehen und prüfen." msgstr "" -#: workflows/templates/workflows/home.html:140 +#: workflows/templates/workflows/home.html:156 msgid "Backups erstellen und sicher verifizieren." msgstr "" -#: workflows/templates/workflows/home.html:146 +#: workflows/templates/workflows/home.html:162 #: workflows/templates/workflows/welcome_emails.html:4 msgid "Welcome E-Mails" msgstr "Welcome Emails" -#: workflows/templates/workflows/home.html:147 +#: workflows/templates/workflows/home.html:163 msgid "Geplante Welcome Mails verwalten." msgstr "Manage scheduled welcome emails." -#: workflows/templates/workflows/home.html:154 +#: workflows/templates/workflows/home.html:170 msgid "Felder, Schritte und Optionen verwalten." msgstr "Manage fields, steps, and options." -#: workflows/templates/workflows/home.html:158 +#: workflows/templates/workflows/home.html:174 #: workflows/templates/workflows/intro_builder.html:4 #: workflows/templates/workflows/intro_builder.html:17 msgid "Einweisungs-Builder" msgstr "Introduction Builder" -#: workflows/templates/workflows/home.html:159 +#: workflows/templates/workflows/home.html:175 msgid "Checklistenpunkte für das Einweisungsprotokoll konfigurieren." msgstr "Configure checklist items for the introduction protocol." -#: workflows/templates/workflows/home.html:166 +#: workflows/templates/workflows/home.html:182 msgid "Project wiki and developer documentation in one place." msgstr "Project wiki and developer documentation in one place." -#: workflows/templates/workflows/home.html:172 +#: workflows/templates/workflows/home.html:188 msgid "Django Admin" msgstr "Django Admin" -#: workflows/templates/workflows/home.html:173 +#: workflows/templates/workflows/home.html:189 msgid "Vollständige Datenverwaltung." msgstr "Full data management." -#: workflows/templates/workflows/home.html:181 +#: workflows/templates/workflows/home.html:197 msgid "Tipp: Die letzten Vorgänge sehen Sie jederzeit im Anfragen Dashboard." msgstr "Tip: You can always see the latest requests in the Requests Dashboard." @@ -1890,7 +2002,7 @@ msgid "Dienstliche E-Mail" msgstr "Work email" #: workflows/templates/workflows/onboarding_intro_session.html:31 -#: workflows/views.py:708 +#: workflows/views.py:820 msgid "Vertragsbeginn" msgstr "Contract start" @@ -2151,6 +2263,7 @@ msgstr "" #: workflows/templates/workflows/request_timeline.html:74 #: workflows/templates/workflows/requests_dashboard.html:190 #: workflows/templates/workflows/user_management.html:73 +#: workflows/templates/workflows/user_management.html:172 msgid "E-Mail" msgstr "Email" @@ -2330,7 +2443,12 @@ msgid "Noch keine Vorgänge vorhanden." msgstr "No requests available yet." #: workflows/templates/workflows/user_management.html:15 -msgid "Super Admins verwalten Benutzerkonten, Rollen und den aktiven Zugriff." +#, fuzzy +#| msgid "" +#| "Super Admins verwalten Benutzerkonten, Rollen und den aktiven Zugriff." +msgid "" +"Platform Owner und Super Admins verwalten Benutzerkonten, Rollen und den " +"aktiven Zugriff." msgstr "Super admins manage user accounts, roles, and active access." #: workflows/templates/workflows/user_management.html:22 @@ -2394,13 +2512,59 @@ msgid "Es sind noch keine Benutzer vorhanden." msgstr "No users exist yet." #: workflows/templates/workflows/user_management.html:140 +#, fuzzy +#| msgid "" +#| "Hinweis: Der aktuell angemeldete Super Admin kann sich hier nicht selbst " +#| "deaktivieren oder auf eine niedrigere Rolle setzen." msgid "" -"Hinweis: Der aktuell angemeldete Super Admin kann sich hier nicht selbst " -"deaktivieren oder auf eine niedrigere Rolle setzen." +"Hinweis: Der letzte aktive Platform Owner oder Super Admin kann sich hier " +"nicht selbst entfernen oder auf eine niedrigere Rolle setzen." msgstr "" "Note: The currently signed-in super admin cannot deactivate themselves or " "assign a lower role here." +#: workflows/templates/workflows/user_management.html:146 +#, fuzzy +#| msgid "Benutzer anlegen" +msgid "Letzte Benutzeraktionen" +msgstr "Create user" + +#: workflows/templates/workflows/user_management.html:147 +msgid "Die letzten Änderungen an Benutzerkonten und Rollen." +msgstr "" + +#: workflows/templates/workflows/user_management.html:149 +msgid "Zum Audit Log" +msgstr "" + +#: workflows/templates/workflows/user_management.html:157 +#, fuzzy +#| msgid "Vorgänge" +msgid "Betroffen" +msgstr "Requests" + +#: workflows/templates/workflows/user_management.html:158 +msgid "Durch" +msgstr "" + +#: workflows/templates/workflows/user_management.html:173 +#, fuzzy +#| msgid "E-Mail versendet" +msgid "Einladung versendet" +msgstr "Email sent" + +#: workflows/templates/workflows/user_management.html:174 +#, fuzzy +#| msgid "Passwort gespeichert" +msgid "Passwort geändert" +msgstr "Password saved" + +#: workflows/templates/workflows/user_management.html:180 +#, fuzzy +#| msgid "Es sind noch keine Benutzer vorhanden." +msgid "Noch keine Benutzeraktionen vorhanden." +msgstr "No users exist yet." + #: workflows/templates/workflows/welcome_emails.html:14 msgid "Geplante Welcome E-Mails" msgstr "Scheduled welcome emails" @@ -2503,7 +2667,7 @@ msgstr "Devices, software, and access" msgid "Notizen und Freigabe" msgstr "Notes and approval" -#: workflows/views.py:128 workflows/views.py:794 workflows/views.py:799 +#: workflows/views.py:128 workflows/views.py:906 workflows/views.py:911 msgid "Sie haben keine Berechtigung für diese Aktion." msgstr "You do not have permission for this action." @@ -2715,22 +2879,21 @@ msgstr "Request saved" msgid "Backup-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:400 +#: workflows/views.py:436 msgid "Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt." msgstr "" -#: workflows/views.py:408 +#: workflows/views.py:445 #, python-format msgid "Zugangseinladung für %(username)s" msgstr "" -#: workflows/views.py:410 +#: workflows/views.py:447 #, python-format msgid "" "Hallo %(name)s,\n" "\n" -"für Sie wurde ein Benutzerkonto im TUBCO Onboarding- und Offboarding-Portal " -"angelegt.\n" +"für Sie wurde ein Benutzerkonto im %(portal_title)s angelegt.\n" "Bitte öffnen Sie den folgenden Link, um Ihr Passwort zu setzen:\n" "%(url)s\n" "\n" @@ -2738,12 +2901,12 @@ msgid "" "Ihrem Administrator." msgstr "" -#: workflows/views.py:420 +#: workflows/views.py:458 #, python-format msgid "Passwort zurücksetzen für %(username)s" msgstr "" -#: workflows/views.py:422 +#: workflows/views.py:460 #, python-format msgid "" "Hallo %(name)s,\n" @@ -2756,24 +2919,60 @@ msgid "" "ignorieren." msgstr "" -#: workflows/views.py:445 +#: workflows/views.py:498 +#, fuzzy +#| msgid "" +#| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." +msgid "" +"Branding konnte nicht gespeichert werden. Bitte prüfen Sie die Eingaben." +msgstr "User could not be created. Please check the input." + +#: workflows/views.py:524 +#, fuzzy +#| msgid "Offboarding-Anfrage speichern" +msgid "Portal-Branding wurde gespeichert." +msgstr "Save offboarding request" + +#: workflows/views.py:540 msgid "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." msgstr "User could not be created. Please check the input." -#: workflows/views.py:458 +#: workflows/views.py:553 #, fuzzy, python-format #| msgid "Benutzer wurde erstellt: %(username)s" msgid "Benutzer wurde erstellt und eingeladen: %(username)s" msgstr "User created: %(username)s" -#: workflows/views.py:476 +#: workflows/views.py:575 +#, fuzzy +#| msgid "" +#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " +#| "oder herabstufen." +msgid "" +"Der aktuell angemeldete Platform Owner kann sich hier nicht selbst sperren " +"oder herabstufen." +msgstr "" +"The currently signed-in super admin cannot lock or downgrade themselves here." + +#: workflows/views.py:578 msgid "" "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren oder " "herabstufen." msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:479 +#: workflows/views.py:581 +#, fuzzy +#| msgid "" +#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " +#| "oder herabstufen." +msgid "" +"Der letzte aktive Platform Owner kann nicht deaktiviert oder herabgestuft " +"werden." +msgstr "" +"The currently signed-in super admin cannot lock or downgrade themselves here." + +#: workflows/views.py:584 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -2784,18 +2983,28 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:496 +#: workflows/views.py:601 #, python-format msgid "Benutzer wurde aktualisiert: %(username)s" msgstr "User updated: %(username)s" -#: workflows/views.py:518 +#: workflows/views.py:623 #, fuzzy, python-format #| msgid "Benutzer wurde erstellt: %(username)s" msgid "Passwort-Reset-Link wurde versendet: %(username)s" msgstr "User created: %(username)s" -#: workflows/views.py:529 +#: workflows/views.py:635 +#, fuzzy +#| msgid "" +#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " +#| "oder herabstufen." +msgid "" +"Der aktuell angemeldete Platform Owner kann sich hier nicht selbst löschen." +msgstr "" +"The currently signed-in super admin cannot lock or downgrade themselves here." + +#: workflows/views.py:638 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -2805,7 +3014,16 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:532 +#: workflows/views.py:641 +#, fuzzy +#| msgid "" +#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " +#| "oder herabstufen." +msgid "Der letzte aktive Platform Owner kann nicht gelöscht werden." +msgstr "" +"The currently signed-in super admin cannot lock or downgrade themselves here." + +#: workflows/views.py:644 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -2814,124 +3032,132 @@ msgid "Der letzte aktive Super Admin kann nicht gelöscht werden." msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:545 +#: workflows/views.py:657 #, fuzzy, python-format #| msgid "Benutzer wurde erstellt: %(username)s" msgid "Benutzer wurde gelöscht: %(username)s" msgstr "User created: %(username)s" -#: workflows/views.py:632 +#: workflows/views.py:744 #, python-format msgid "Backup wurde erstellt: %(name)s" msgstr "" -#: workflows/views.py:634 +#: workflows/views.py:746 #, python-format msgid "Backup konnte nicht erstellt werden: %(error)s" msgstr "" -#: workflows/views.py:650 +#: workflows/views.py:762 #, python-format msgid "Backup wurde verifiziert: %(name)s" msgstr "" -#: workflows/views.py:652 +#: workflows/views.py:764 #, python-format msgid "Backup-Verifikation fehlgeschlagen: %(error)s" msgstr "" -#: workflows/views.py:668 +#: workflows/views.py:780 #, python-format msgid "Backup wurde gelöscht: %(name)s" msgstr "" -#: workflows/views.py:670 +#: workflows/views.py:782 #, python-format msgid "Backup konnte nicht gelöscht werden: %(error)s" msgstr "" -#: workflows/views.py:696 +#: workflows/views.py:808 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Anfrage erstellt" msgstr "Request saved" -#: workflows/views.py:698 +#: workflows/views.py:810 #, fuzzy, python-format #| msgid "Sitzungsstatus" msgid "Status: %(status)s" msgstr "Session status" -#: workflows/views.py:710 +#: workflows/views.py:822 #, fuzzy #| msgid "Geplant für" msgid "Geplanter Start" msgstr "Scheduled for" -#: workflows/views.py:720 +#: workflows/views.py:832 msgid "Geräteübergabe / Hardware-Abholung" msgstr "" -#: workflows/views.py:722 +#: workflows/views.py:834 msgid "Geplanter Hardware-Termin" msgstr "" -#: workflows/views.py:731 +#: workflows/views.py:843 #, fuzzy #| msgid "Noch nicht verfügbar" msgid "PDF verfügbar" msgstr "Not available yet" -#: workflows/views.py:757 +#: workflows/views.py:869 #, fuzzy #| msgid "Einweisung" msgid "Einweisungssitzung" msgstr "Introduction" -#: workflows/views.py:769 +#: workflows/views.py:881 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/views.py:808 +#: workflows/views.py:920 msgid "Keine Einträge ausgewählt." msgstr "No entries selected." -#: workflows/views.py:851 +#: workflows/views.py:963 #, python-format msgid "%(count)s Eintrag/Einträge gelöscht." msgstr "%(count)s entry/entries deleted." -#: workflows/views.py:853 +#: workflows/views.py:965 #, python-format msgid "%(count)s Auswahl(en) konnten nicht verarbeitet werden." msgstr "%(count)s selection(s) could not be processed." -#: workflows/views.py:855 +#: workflows/views.py:967 msgid "Keine passenden Einträge gefunden." msgstr "No matching entries found." -#: workflows/views.py:1082 +#: workflows/views.py:1194 msgid "Einweisungs- und Übergabeprotokoll wurde erzeugt." msgstr "Introduction and handover protocol was generated." -#: workflows/views.py:1099 +#: workflows/views.py:1211 msgid "Einweisungsprotokoll aus Live-Status wurde erzeugt." msgstr "Introduction protocol from live status was generated." -#: workflows/views.py:1128 +#: workflows/views.py:1240 msgid "Einweisung wurde zurückgesetzt." msgstr "Introduction was reset." -#: workflows/views.py:1142 +#: workflows/views.py:1254 msgid "Einweisung wurde als abgeschlossen gespeichert." msgstr "Introduction was saved as completed." -#: workflows/views.py:1155 +#: workflows/views.py:1267 msgid "Einweisung wurde als Entwurf gespeichert." msgstr "Introduction was saved as draft." +#, fuzzy +#~| msgid "Produktion" +#~ msgid "Product Owner" +#~ msgstr "Production" + +#~ msgid "TUBCO Onboarding & Offboarding Portal" +#~ msgstr "TUBCO Onboarding & Offboarding Portal" + #~ msgid "Die Passwörter stimmen nicht überein." #~ msgstr "The passwords do not match." diff --git a/backend/workflows/admin.py b/backend/workflows/admin.py index 5895ee7..d142cb2 100644 --- a/backend/workflows/admin.py +++ b/backend/workflows/admin.py @@ -3,7 +3,7 @@ from django.conf import settings from django import forms from .emailing import send_system_email -from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig +from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig @admin.register(EmployeeProfile) @@ -20,6 +20,11 @@ class AdminAuditLogAdmin(admin.ModelAdmin): ordering = ('-created_at', '-id') +@admin.register(PortalBranding) +class PortalBrandingAdmin(admin.ModelAdmin): + list_display = ('name', 'portal_title', 'company_name', 'support_email', 'default_language', 'updated_at') + + @admin.register(OnboardingRequest) class OnboardingRequestAdmin(admin.ModelAdmin): list_display = ('id', 'full_name', 'work_email', 'department', 'contract_start', 'created_at') diff --git a/backend/workflows/branding.py b/backend/workflows/branding.py new file mode 100644 index 0000000..b25a984 --- /dev/null +++ b/backend/workflows/branding.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from pathlib import Path + +from django.conf import settings +from django.templatetags.static import static + +from .models import PortalBranding + + +def get_portal_branding() -> PortalBranding: + branding, _ = PortalBranding.objects.get_or_create( + name='Default', + defaults={ + 'portal_title': 'TUBCO Onboarding & Offboarding Portal', + 'company_name': 'TUBCO', + 'support_email': 'info@tub.co', + 'default_language': 'de', + 'primary_color': '#000078', + 'secondary_color': '#c0002b', + }, + ) + return branding + + +def get_portal_logo_url() -> str: + branding = get_portal_branding() + if branding.logo_image: + try: + return branding.logo_image.url + except ValueError: + pass + return static('workflows/img/tubco-logo.svg') + + +def get_portal_letterhead_path() -> Path: + branding = get_portal_branding() + if branding.pdf_letterhead: + try: + candidate = Path(branding.pdf_letterhead.path) + if candidate.exists(): + return candidate + except (ValueError, NotImplementedError): + pass + return settings.PDF_TEMPLATES_DIR / 'templates.pdf' + + +def get_branding_context() -> dict[str, object]: + branding = get_portal_branding() + return { + 'portal_branding': branding, + 'portal_title': branding.portal_title, + 'portal_company_name': branding.company_name, + 'portal_support_email': branding.support_email, + 'portal_default_language': branding.default_language, + 'portal_primary_color': branding.primary_color, + 'portal_secondary_color': branding.secondary_color, + 'portal_logo_url': get_portal_logo_url(), + 'portal_has_custom_logo': bool(branding.logo_image), + 'portal_has_custom_letterhead': bool(branding.pdf_letterhead), + } + + +def get_branding_email_copy() -> dict[str, str]: + branding = get_portal_branding() + company_name = (branding.company_name or 'TUBCO').strip() + portal_title = (branding.portal_title or f'{company_name} Portal').strip() + return { + 'company_name': company_name, + 'portal_title': portal_title, + 'support_email': (branding.support_email or '').strip(), + } + + +def get_default_notification_templates() -> dict[str, dict[str, str]]: + from copy import deepcopy + + from .tasks import DEFAULT_NOTIFICATION_TEMPLATES + + templates = deepcopy(DEFAULT_NOTIFICATION_TEMPLATES) + company_name = get_branding_email_copy()['company_name'] + welcome = templates.get('onboarding_welcome') + if welcome: + welcome['subject'] = f'Willkommen bei {company_name}, {{ VORNAME }}' + welcome['subject_en'] = f'Welcome to {company_name}, {{ VORNAME }}' + welcome['body'] = ( + 'Hallo {{ FULL_NAME }},\n\n' + f'herzlich willkommen bei {company_name}.\n' + 'Wir freuen uns sehr, dass du ab dem {{ CONTRACT_START }} unser Team in der Abteilung {{ DEPARTMENT }} verstärkst.\n\n' + 'Deine dienstliche E-Mail-Adresse lautet: {{ EMAIL }}.\n' + 'Im Anhang findest du deine Onboarding-Unterlagen als PDF.\n\n' + 'Wenn du Fragen hast, melde dich gerne jederzeit.\n\n' + 'Viele Grüße\n' + f'{company_name} IT' + ) + welcome['body_en'] = ( + 'Hello {{ FULL_NAME }},\n\n' + f'welcome to {company_name}.\n' + 'We are very happy that you will join our {{ DEPARTMENT }} team starting on {{ CONTRACT_START }}.\n\n' + 'Your work email address is: {{ EMAIL }}.\n' + 'You will find your onboarding documents attached as a PDF.\n\n' + 'If you have any questions, feel free to contact us anytime.\n\n' + 'Best regards,\n' + f'{company_name} IT' + ) + return templates diff --git a/backend/workflows/context_processors.py b/backend/workflows/context_processors.py index 64bad7f..162e55b 100644 --- a/backend/workflows/context_processors.py +++ b/backend/workflows/context_processors.py @@ -1,5 +1,8 @@ +from .branding import get_branding_context from .roles import template_role_context def role_context(request): - return template_role_context(getattr(request, 'user', None)) + context = template_role_context(getattr(request, 'user', None)) + context.update(get_branding_context()) + return context diff --git a/backend/workflows/forms.py b/backend/workflows/forms.py index 2853db5..d5ca11b 100644 --- a/backend/workflows/forms.py +++ b/backend/workflows/forms.py @@ -7,8 +7,8 @@ from django.utils import timezone from django.utils.translation import get_language, gettext as _, gettext_lazy from .form_builder import apply_form_field_config -from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, WorkflowConfig -from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role +from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, WorkflowConfig +from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role YES_NO_CHOICES = [('', '--'), ('ja', 'Ja'), ('nein', 'Nein')] @@ -129,12 +129,13 @@ class UserManagementCreateForm(forms.Form): email = forms.EmailField(label=_('E-Mail-Adresse')) role_key = forms.ChoiceField(label=_('Rolle')) - def __init__(self, *args, **kwargs): + def __init__(self, *args, include_product_owner: bool = False, **kwargs): super().__init__(*args, **kwargs) - self.fields['role_key'].choices = [ - (role_key, str(ROLE_LABELS[role_key])) - for role_key in (ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF) - ] + self.include_product_owner = include_product_owner + role_order = [ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF] + if include_product_owner: + role_order = [ROLE_PLATFORM_OWNER] + role_order + self.fields['role_key'].choices = [(role_key, str(ROLE_LABELS[role_key])) for role_key in role_order] def clean_username(self): username = (self.cleaned_data.get('username') or '').strip() @@ -150,6 +151,8 @@ class UserManagementCreateForm(forms.Form): role_key = (self.cleaned_data.get('role_key') or '').strip() if role_key not in ROLE_GROUP_NAMES: raise forms.ValidationError(_('Ungültige Rolle.')) + if role_key == ROLE_PLATFORM_OWNER and not self.include_product_owner: + raise forms.ValidationError(_('Nur Platform Owner dürfen diese Rolle vergeben.')) return role_key def save(self): @@ -166,6 +169,53 @@ class UserManagementCreateForm(forms.Form): return user +class PortalBrandingForm(forms.ModelForm): + class Meta: + model = PortalBranding + fields = [ + 'portal_title', + 'company_name', + 'support_email', + 'default_language', + 'logo_image', + 'pdf_letterhead', + 'primary_color', + 'secondary_color', + ] + labels = { + 'portal_title': gettext_lazy('Portal-Titel'), + 'company_name': gettext_lazy('Firmenname'), + 'support_email': gettext_lazy('Support-E-Mail'), + 'default_language': gettext_lazy('Standardsprache'), + 'logo_image': gettext_lazy('Logo'), + 'pdf_letterhead': gettext_lazy('PDF-Briefkopf'), + 'primary_color': gettext_lazy('Primärfarbe'), + 'secondary_color': gettext_lazy('Sekundärfarbe'), + } + widgets = { + 'primary_color': forms.TextInput(attrs={'type': 'color'}), + 'secondary_color': forms.TextInput(attrs={'type': 'color'}), + 'logo_image': forms.ClearableFileInput(attrs={'accept': '.svg,.png,.jpg,.jpeg,.webp'}), + 'pdf_letterhead': forms.ClearableFileInput(attrs={'accept': '.pdf'}), + } + + def clean_logo_image(self): + logo = self.cleaned_data.get('logo_image') + if not logo: + return logo + if getattr(logo, 'size', 0) > 5 * 1024 * 1024: + raise forms.ValidationError(_('Das Logo darf maximal 5 MB groß sein.')) + return logo + + def clean_pdf_letterhead(self): + letterhead = self.cleaned_data.get('pdf_letterhead') + if not letterhead: + return letterhead + if getattr(letterhead, 'size', 0) > 10 * 1024 * 1024: + raise forms.ValidationError(_('Der PDF-Briefkopf darf maximal 10 MB groß sein.')) + return letterhead + + class OnboardingRequestForm(forms.ModelForm): first_name = forms.CharField(label='Vorname', required=False) last_name = forms.CharField(label='Nachname', required=False) diff --git a/backend/workflows/management/commands/bootstrap_initial_users.py b/backend/workflows/management/commands/bootstrap_initial_users.py index 5adfb7f..aa35bce 100644 --- a/backend/workflows/management/commands/bootstrap_initial_users.py +++ b/backend/workflows/management/commands/bootstrap_initial_users.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand -from workflows.roles import ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role, ensure_role_groups +from workflows.roles import ROLE_PLATFORM_OWNER, ROLE_STAFF, assign_user_role, ensure_role_groups DEFAULT_USERS = [ { @@ -45,7 +45,7 @@ class Command(BaseCommand): is_superuser=item['is_superuser'], ) ensure_role_groups() - assign_user_role(user, ROLE_SUPER_ADMIN if item['username'] == 'admin_test' else ROLE_STAFF) + assign_user_role(user, ROLE_PLATFORM_OWNER if item['username'] == 'admin_test' else ROLE_STAFF) self.stdout.write(f'created {user.username}') self.stdout.write(self.style.SUCCESS('initial users created')) diff --git a/backend/workflows/migrations/0036_portalbranding.py b/backend/workflows/migrations/0036_portalbranding.py new file mode 100644 index 0000000..a06b44b --- /dev/null +++ b/backend/workflows/migrations/0036_portalbranding.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.5 on 2026-03-26 10:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0035_workflowconfig_remote_backup_enabled_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='PortalBranding', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='Default', max_length=80, unique=True)), + ('portal_title', models.CharField(default='TUBCO Onboarding & Offboarding Portal', max_length=255)), + ('company_name', models.CharField(default='TUBCO', max_length=255)), + ('support_email', models.EmailField(blank=True, default='info@tub.co', max_length=254)), + ('default_language', models.CharField(choices=[('de', 'Deutsch'), ('en', 'English')], default='de', max_length=10)), + ('logo_image', models.ImageField(blank=True, null=True, upload_to='branding/')), + ('pdf_letterhead', models.FileField(blank=True, null=True, upload_to='branding/')), + ('primary_color', models.CharField(blank=True, default='#000078', max_length=20)), + ('secondary_color', models.CharField(blank=True, default='#c0002b', max_length=20)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'Portal Branding', + 'verbose_name_plural': 'Portal Branding', + }, + ), + ] diff --git a/backend/workflows/migrations/0037_alter_portalbranding_logo_image_and_more.py b/backend/workflows/migrations/0037_alter_portalbranding_logo_image_and_more.py new file mode 100644 index 0000000..0fb23eb --- /dev/null +++ b/backend/workflows/migrations/0037_alter_portalbranding_logo_image_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.5 on 2026-03-26 10:25 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0036_portalbranding'), + ] + + operations = [ + migrations.AlterField( + model_name='portalbranding', + name='logo_image', + field=models.FileField(blank=True, null=True, upload_to='branding/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['svg', 'png', 'jpg', 'jpeg', 'webp'])]), + ), + migrations.AlterField( + model_name='portalbranding', + name='pdf_letterhead', + field=models.FileField(blank=True, null=True, upload_to='branding/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['pdf'])]), + ), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index 7f6498c..f986b51 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.core.validators import FileExtensionValidator from django.db import models from django.utils.translation import get_language from django.utils.translation import gettext_lazy as _ @@ -24,6 +25,40 @@ class EmployeeProfile(models.Model): return f"{self.full_name} <{self.work_email}>" +class PortalBranding(models.Model): + name = models.CharField(max_length=80, default='Default', unique=True) + portal_title = models.CharField(max_length=255, default='TUBCO Onboarding & Offboarding Portal') + company_name = models.CharField(max_length=255, default='TUBCO') + support_email = models.EmailField(blank=True, default='info@tub.co') + default_language = models.CharField( + max_length=10, + choices=[('de', 'Deutsch'), ('en', 'English')], + default='de', + ) + logo_image = models.FileField( + upload_to='branding/', + blank=True, + null=True, + validators=[FileExtensionValidator(allowed_extensions=['svg', 'png', 'jpg', 'jpeg', 'webp'])], + ) + pdf_letterhead = models.FileField( + upload_to='branding/', + blank=True, + null=True, + validators=[FileExtensionValidator(allowed_extensions=['pdf'])], + ) + primary_color = models.CharField(max_length=20, blank=True, default='#000078') + secondary_color = models.CharField(max_length=20, blank=True, default='#c0002b') + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = 'Portal Branding' + verbose_name_plural = 'Portal Branding' + + def __str__(self) -> str: + return self.portal_title or self.company_name or self.name + + class AdminAuditLog(models.Model): actor = models.ForeignKey( settings.AUTH_USER_MODEL, diff --git a/backend/workflows/roles.py b/backend/workflows/roles.py index f487490..890d2d0 100644 --- a/backend/workflows/roles.py +++ b/backend/workflows/roles.py @@ -4,12 +4,14 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.utils.translation import gettext_lazy as _ +ROLE_PLATFORM_OWNER = 'platform_owner' ROLE_SUPER_ADMIN = 'super_admin' ROLE_ADMIN = 'admin' ROLE_IT_STAFF = 'it_staff' ROLE_STAFF = 'staff' ROLE_GROUP_NAMES = { + ROLE_PLATFORM_OWNER: 'Platform Owner', ROLE_SUPER_ADMIN: 'Super Admin', ROLE_ADMIN: 'Admin', ROLE_IT_STAFF: 'IT Staff', @@ -17,6 +19,7 @@ ROLE_GROUP_NAMES = { } ROLE_LABELS = { + ROLE_PLATFORM_OWNER: _('Platform Owner'), ROLE_SUPER_ADMIN: _('Super Admin'), ROLE_ADMIN: _('Admin'), ROLE_IT_STAFF: _('IT Staff'), @@ -24,19 +27,20 @@ ROLE_LABELS = { } CAPABILITIES = { - 'manage_users': {ROLE_SUPER_ADMIN}, - 'access_requests_dashboard': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, - 'run_intro_session': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, - 'generate_intro_pdfs': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, - 'retry_requests': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, - 'delete_requests': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'manage_integrations': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'manage_welcome_emails': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'manage_builders': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'view_audit_log': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'manage_backups': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'view_docs': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, - 'access_django_admin_link': {ROLE_SUPER_ADMIN}, + 'manage_users': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN}, + 'manage_product_branding': {ROLE_PLATFORM_OWNER}, + 'access_requests_dashboard': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, + 'run_intro_session': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, + 'generate_intro_pdfs': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, + 'retry_requests': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, + 'delete_requests': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'manage_integrations': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'manage_welcome_emails': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'manage_builders': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'view_audit_log': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'manage_backups': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'view_docs': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'access_django_admin_link': {ROLE_PLATFORM_OWNER}, } @@ -54,16 +58,17 @@ def assign_user_role(user, role_key: str) -> None: user.groups.remove(*role_groups) user.groups.add(Group.objects.get(name=ROLE_GROUP_NAMES[role_key])) + is_product_owner = role_key == ROLE_PLATFORM_OWNER is_super_admin = role_key == ROLE_SUPER_ADMIN - user.is_staff = is_super_admin - user.is_superuser = is_super_admin + user.is_staff = is_product_owner or is_super_admin + user.is_superuser = is_product_owner user.save(update_fields=['is_staff', 'is_superuser']) def ensure_bootstrap_role_assignments() -> None: user_model = get_user_model() bootstrap_roles = { - 'admin_test': ROLE_SUPER_ADMIN, + 'admin_test': ROLE_PLATFORM_OWNER, 'user_test': ROLE_STAFF, } role_group_names = set(ROLE_GROUP_NAMES.values()) @@ -72,6 +77,12 @@ def ensure_bootstrap_role_assignments() -> None: user = user_model.objects.get(username=username) except user_model.DoesNotExist: continue + if role_key == ROLE_PLATFORM_OWNER and not any( + get_user_role_key(existing_user) == ROLE_PLATFORM_OWNER + for existing_user in user_model.objects.all() + ): + assign_user_role(user, ROLE_PLATFORM_OWNER) + continue if user.groups.filter(name__in=role_group_names).exists(): continue assign_user_role(user, role_key) @@ -81,15 +92,15 @@ def get_user_role_key(user) -> str: if not getattr(user, 'is_authenticated', False): return ROLE_STAFF if getattr(user, 'is_superuser', False): - return ROLE_SUPER_ADMIN + return ROLE_PLATFORM_OWNER group_names = set(user.groups.values_list('name', flat=True)) - for role_key in (ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF): + for role_key in (ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF): if ROLE_GROUP_NAMES[role_key] in group_names: return role_key if getattr(user, 'is_staff', False): - return ROLE_ADMIN + return ROLE_SUPER_ADMIN return ROLE_STAFF @@ -111,6 +122,7 @@ def template_role_context(user) -> dict[str, object]: return { 'role_key': role_key, 'role_label': str(ROLE_LABELS[role_key]), + 'can_manage_product_branding': user_has_capability(user, 'manage_product_branding'), 'can_manage_users': user_has_capability(user, 'manage_users'), 'can_access_requests_dashboard': user_has_capability(user, 'access_requests_dashboard'), 'can_run_intro_session': user_has_capability(user, 'run_intro_session'), diff --git a/backend/workflows/static/workflows/css/home.css b/backend/workflows/static/workflows/css/home.css index a9976a1..11817de 100644 --- a/backend/workflows/static/workflows/css/home.css +++ b/backend/workflows/static/workflows/css/home.css @@ -362,6 +362,8 @@ gap: 10px; align-items: flex-end; flex-wrap: wrap; + position: relative; + padding-left: 16px; } .section-head h2 { @@ -376,6 +378,32 @@ font-size: 13px; } + .section-divider { + height: 1px; + margin: 24px 0 14px; + border-radius: 999px; + background: linear-gradient(90deg, rgba(0, 0, 120, 0.18), rgba(0, 0, 120, 0.05) 40%, rgba(140, 29, 29, 0.10)); + } + + .section-head::before { + content: ""; + position: absolute; + left: 0; + top: 2px; + width: 4px; + height: 34px; + border-radius: 999px; + background: linear-gradient(180deg, rgba(0, 0, 120, 0.95), rgba(0, 0, 120, 0.30)); + } + + .section-head-platform::before { + background: linear-gradient(180deg, rgba(140, 29, 29, 0.90), rgba(140, 29, 29, 0.28)); + } + + .section-head-admin::before { + background: linear-gradient(180deg, rgba(159, 118, 33, 0.92), rgba(159, 118, 33, 0.28)); + } + .apps-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); diff --git a/backend/workflows/tasks.py b/backend/workflows/tasks.py index 56c305d..9e804b8 100644 --- a/backend/workflows/tasks.py +++ b/backend/workflows/tasks.py @@ -13,6 +13,7 @@ from jinja2 import Template from pypdf import PageObject, PdfReader, PdfWriter from xhtml2pdf import pisa +from .branding import get_default_notification_templates, get_portal_letterhead_path from .models import EmployeeProfile, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, WorkflowConfig from .emailing import send_system_email from .services import upload_to_nextcloud @@ -678,7 +679,7 @@ def _render_notification_template(template_key: str, context: dict, language_cod subject_template = db_template.translated_subject_template(lang) body_template = db_template.translated_body_template(lang) else: - fallback = DEFAULT_NOTIFICATION_TEMPLATES[template_key] + fallback = get_default_notification_templates()[template_key] subject_template = fallback.get(f'subject_{lang}', '') or fallback['subject'] body_template = fallback.get(f'body_{lang}', '') or fallback['body'] @@ -865,7 +866,7 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path: temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_{safe_name}.pdf' template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_template.html' - letterhead_path = settings.PDF_TEMPLATES_DIR / 'templates.pdf' + letterhead_path = get_portal_letterhead_path() devices = _split_multiline(request_obj.needed_devices) software = _split_multiline(request_obj.needed_software) @@ -998,7 +999,7 @@ def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest, language_code temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_intro_{safe_name}.pdf' template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_intro_template.html' - letterhead_path = settings.PDF_TEMPLATES_DIR / 'templates.pdf' + letterhead_path = get_portal_letterhead_path() salutation = (request_obj.get_gender_display() or '').strip() display_name = f"{salutation} {request_obj.full_name}".strip() if salutation else request_obj.full_name @@ -1048,7 +1049,7 @@ def _generate_onboarding_intro_session_pdf( temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_intro_session_{safe_name}_{version}.pdf' template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_intro_session_pdf.html' - letterhead_path = settings.PDF_TEMPLATES_DIR / 'templates.pdf' + letterhead_path = get_portal_letterhead_path() salutation = (request_obj.get_gender_display() or '').strip() display_name = f"{salutation} {request_obj.full_name}".strip() if salutation else request_obj.full_name @@ -1109,7 +1110,7 @@ def _generate_offboarding_pdf(request_obj: OffboardingRequest) -> Path: temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_offboarding_{safe_name}.pdf' template_path = settings.PDF_TEMPLATES_DIR / 'offboarding_template.html' - letterhead_path = settings.PDF_TEMPLATES_DIR / 'templates.pdf' + letterhead_path = get_portal_letterhead_path() latest_onboarding = ( OnboardingRequest.objects.filter(work_email=request_obj.work_email) .order_by('-created_at') diff --git a/backend/workflows/templates/workflows/branding_settings.html b/backend/workflows/templates/workflows/branding_settings.html new file mode 100644 index 0000000..4f9138e --- /dev/null +++ b/backend/workflows/templates/workflows/branding_settings.html @@ -0,0 +1,70 @@ +{% extends 'workflows/base_shell.html' %} +{% load static i18n %} + +{% block title %}{% trans "Branding" %}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block shell_body %} +{% include 'workflows/includes/app_header.html' with header_show_home=1 header_show_lang=1 header_inside_shell=1 %} +

{% trans "Branding" %}

+

{% trans "Portalname, Firmenauftritt, Logo und PDF-Briefkopf zentral verwalten." %}

+ +{% include 'workflows/includes/messages.html' %} + +
+
+ {% csrf_token %} +
+
+ + {{ form.portal_title }} +
+
+ + {{ form.company_name }} +
+
+ + {{ form.support_email }} +
+
+ + {{ form.default_language }} +
+
+ + {{ form.primary_color }} +
+
+ + {{ form.secondary_color }} +
+
+ + {{ form.logo_image }} +
{% trans "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB." %}
+ {% for error in form.logo_image.errors %}
{{ error }}
{% endfor %} + {% if branding.logo_image %} +
{% trans "Aktuelles Logo:" %} {% trans "öffnen" %}
+ {% endif %} +
+
+ + {{ form.pdf_letterhead }} +
{% trans "Erlaubtes Format: PDF. Maximal 10 MB." %}
+ {% for error in form.pdf_letterhead.errors %}
{{ error }}
{% endfor %} + {% if branding.pdf_letterhead %} +
{% trans "Aktueller Briefkopf:" %} {% trans "öffnen" %}
+ {% endif %} +
+
+
+
{% trans "TUBCO bleibt als Standard erhalten, bis hier Werte geändert oder Dateien hochgeladen werden." %}
+ +
+
+
+{% endblock %} diff --git a/backend/workflows/templates/workflows/developer_handbook.html b/backend/workflows/templates/workflows/developer_handbook.html index 94841e4..76e4552 100644 --- a/backend/workflows/templates/workflows/developer_handbook.html +++ b/backend/workflows/templates/workflows/developer_handbook.html @@ -17,7 +17,7 @@ Project Wiki -

Engineering runbook for development, deployment, maintenance, and extension of the TUBCO Onboarding & Offboarding Portal.

+

Engineering runbook for development, deployment, maintenance, and extension of the current company portal deployment.

Overview @@ -133,7 +133,7 @@ docker compose exec -T web django-admin compilemessages

7) PDF Pipeline

  • PDF generation is HTML-to-PDF using xhtml2pdf.
  • -
  • Letterhead overlay is applied from templates.pdf.
  • +
  • Letterhead overlay defaults to templates.pdf, but can now be replaced from Admin Apps → Branding.
  • Main logic lives in backend/workflows/tasks.py.
  • Fixed PDF labels/headings are rendered from task-level DE/EN text dictionaries, not hard-coded directly in request processing logic.
  • PDF language follows the normalized request preferred_language, with German fallback.
  • @@ -171,7 +171,16 @@ docker compose exec -T web django-admin compilemessages
  • Do not point remote backup at the same Nextcloud directory used for normal onboarding/offboarding document uploads.
-

10) Builder Architecture

+

10) Branding

+
    +
  • Portal-level branding is stored in the singleton model PortalBranding.
  • +
  • Configured from Admin Apps → Branding.
  • +
  • Current scope: portal title, company name, support email, default language, logo, PDF letterhead, and primary/secondary colors.
  • +
  • Shared header/logo rendering now uses the branding context processor instead of hardcoded TUBCO asset references.
  • +
  • User invitation emails and welcome-template fallbacks also use the configured branding defaults.
  • +
+ +

11) Builder Architecture

Form Builder

  • Model: FormFieldConfig + FormOption
  • diff --git a/backend/workflows/templates/workflows/home.html b/backend/workflows/templates/workflows/home.html index 568ed9a..8f194c0 100644 --- a/backend/workflows/templates/workflows/home.html +++ b/backend/workflows/templates/workflows/home.html @@ -1,7 +1,7 @@ {% extends 'workflows/base_shell.html' %} {% load static i18n %} -{% block title %}{% trans "TUBCO Onboarding & Offboarding Portal" %}{% endblock %} +{% block title %}{{ portal_title }}{% endblock %} @@ -12,7 +12,7 @@ {% block shell_body %}
    - +
    @@ -32,7 +32,7 @@
    {% trans "Operations Console" %} -

    {% trans "TUBCO Onboarding & Offboarding Portal" %}

    +

    {{ portal_title }}

    {% trans "Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud." %}

    {% trans "Rolle:" %} {{ role_label }} @@ -51,7 +51,7 @@
    {% include 'workflows/includes/messages.html' %} -
    +

    {% trans "Apps" %}

    {% trans "Wählen Sie den gewünschten Prozess." %}

    @@ -107,8 +107,24 @@ {% endif %}
    + {% if can_manage_product_branding %} + +
    +

    {% trans "Platform Apps" %}

    +

    {% trans "Produktweite Konfiguration und Produktsteuerung." %}

    +
    +
    +
    +

    {% trans "Branding" %}

    +

    {% trans "Logo, Portalname, Farben und PDF-Briefkopf verwalten." %}

    +{% trans "Öffnen" %} +
    +
    + {% endif %} + {% if can_manage_users or can_manage_integrations or can_view_audit_log or can_manage_backups or can_manage_welcome_emails or can_manage_builders or can_view_docs or can_access_django_admin_link %} -
    + +

    {% trans "Admin Apps" %}

    {% trans "Konfiguration, Tests und Steuerung." %}

    diff --git a/backend/workflows/templates/workflows/includes/app_header.html b/backend/workflows/templates/workflows/includes/app_header.html index 7c70bf4..9bd189e 100644 --- a/backend/workflows/templates/workflows/includes/app_header.html +++ b/backend/workflows/templates/workflows/includes/app_header.html @@ -1,8 +1,8 @@ -{% load static i18n %} +{% load i18n %} {% get_current_language as CURRENT_LANGUAGE %}
    - +
    {% if header_show_lang %} diff --git a/backend/workflows/templates/workflows/project_wiki.html b/backend/workflows/templates/workflows/project_wiki.html index 31e8982..3810443 100644 --- a/backend/workflows/templates/workflows/project_wiki.html +++ b/backend/workflows/templates/workflows/project_wiki.html @@ -120,7 +120,7 @@
    • Template source: /backend/media/templates/onboarding_template.html and offboarding_template.html.
    • Additional onboarding intro template: /backend/media/templates/onboarding_intro_template.html.
    • -
    • Letterhead: /backend/media/templates/templates.pdf.
    • +
    • Letterhead defaults to /backend/media/templates/templates.pdf, but can be overridden from Admin Apps → Branding.
    • Output folder: /backend/media/pdfs/.
    • Fixed PDF labels and notes are rendered through task-level DE/EN text maps and follow the request language with German fallback.
    • Signature images are embedded for compatibility with xhtml2pdf rendering.
    • @@ -178,6 +178,7 @@
    • Form Builder: manage field visibility/order/options.
    • Einweisungs-Builder: manage custom checklist items for the intro PDF and live introduction checklist, including section, visibility, and conditional display logic.
    • Integrations: Nextcloud, SMTP, default routing addresses, notification rules, workflow rules, and remote backup target settings.
    • +
    • Branding: portal title, company name, logo, support email, default language, PDF letterhead, and basic brand colors.
    • Benutzer & Rollen: super-admin-only page for creating users, assigning roles, activating/deactivating access, sending access or password-reset links by email, and deleting accounts when appropriate.
    • Welcome Emails: scheduled jobs, pause/resume/cancel/trigger now.
    • Audit Log: staff-only trace of important admin changes such as builder edits, settings updates, PDF generation, welcome-email operations, and request deletions. Supports filtering by action, user, and date range.
    • @@ -207,6 +208,7 @@
    • Remote backup target configuration lives under Admin Apps → IntegrationenBackup-Ziel.
    • Nextcloud remote backups must use a separate backup directory, not the normal onboarding/offboarding document directory.
    • Longer-running admin actions such as backup create/verify and integration tests use the same shared progress overlay after confirmation.
    • +
    • Brand assets such as logo and PDF letterhead are managed separately under Admin Apps → Branding.

    Deployment Notes

    diff --git a/backend/workflows/templates/workflows/requests_dashboard.html b/backend/workflows/templates/workflows/requests_dashboard.html index ec06e1c..501a469 100644 --- a/backend/workflows/templates/workflows/requests_dashboard.html +++ b/backend/workflows/templates/workflows/requests_dashboard.html @@ -12,7 +12,7 @@ {% block shell_body %}
    - +
    @@ -300,7 +300,7 @@ {% endblock %} diff --git a/backend/workflows/templates/workflows/user_management.html b/backend/workflows/templates/workflows/user_management.html index f74769f..5a68b68 100644 --- a/backend/workflows/templates/workflows/user_management.html +++ b/backend/workflows/templates/workflows/user_management.html @@ -12,7 +12,7 @@

    {% trans "Benutzer & Rollen" %}

    -

    {% trans "Super Admins verwalten Benutzerkonten, Rollen und den aktiven Zugriff." %}

    +

    {% trans "Platform Owner und Super Admins verwalten Benutzerkonten, Rollen und den aktiven Zugriff." %}

    @@ -137,7 +137,7 @@
    -

    {% trans "Hinweis: Der aktuell angemeldete Super Admin kann sich hier nicht selbst deaktivieren oder auf eine niedrigere Rolle setzen." %}

    +

    {% trans "Hinweis: Der letzte aktive Platform Owner oder Super Admin kann sich hier nicht selbst entfernen oder auf eine niedrigere Rolle setzen." %}

    diff --git a/backend/workflows/urls.py b/backend/workflows/urls.py index 88062a2..2b7afca 100644 --- a/backend/workflows/urls.py +++ b/backend/workflows/urls.py @@ -30,6 +30,8 @@ urlpatterns = [ path('admin-tools/welcome-emails//resume/', views.resume_welcome_email, name='resume_welcome_email'), path('admin-tools/welcome-emails//cancel/', views.cancel_welcome_email, name='cancel_welcome_email'), path('admin-tools/handbook/', views.handbook_page, name='handbook_page'), + path('admin-tools/branding/', views.portal_branding_page, name='portal_branding_page'), + path('admin-tools/branding/save/', views.save_portal_branding, name='save_portal_branding'), path('admin-tools/users/', views.user_management_page, name='user_management_page'), path('admin-tools/users/create/', views.create_user_from_admin, name='create_user_from_admin'), path('admin-tools/users//update/', views.update_user_from_admin, name='update_user_from_admin'), diff --git a/backend/workflows/views.py b/backend/workflows/views.py index 7e97f97..d966876 100644 --- a/backend/workflows/views.py +++ b/backend/workflows/views.py @@ -25,7 +25,8 @@ from django.utils.translation import get_language, override from django.urls import reverse from .backup_ops import create_backup_bundle, delete_backup_bundle, list_backup_bundles, verify_backup_bundle -from .forms import OffboardingRequestForm, OnboardingRequestForm, UserManagementCreateForm +from .branding import get_branding_email_copy, get_default_notification_templates +from .forms import OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, UserManagementCreateForm from .form_builder import ( DEFAULT_FIELD_ORDER, LOCKED_FIELD_RULES, @@ -34,12 +35,11 @@ from .form_builder import ( ONBOARDING_PAGE_ORDER, ensure_form_field_configs, ) -from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig +from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalBranding, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig from .emailing import send_system_email -from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_SUPER_ADMIN, assign_user_role, get_user_role_key, get_user_role_label, user_has_capability +from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, assign_user_role, get_user_role_key, get_user_role_label, user_has_capability from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud from .tasks import ( - DEFAULT_NOTIFICATION_TEMPLATES, _generate_onboarding_intro_pdf, _generate_onboarding_intro_session_pdf, build_intro_sections_for_request, @@ -341,6 +341,7 @@ def home(request): def _user_management_rows(): user_model = get_user_model() role_order = { + ROLE_PLATFORM_OWNER: 0, ROLE_SUPER_ADMIN: 0, 'admin': 1, 'it_staff': 2, @@ -372,19 +373,30 @@ def _render_user_management(request, create_form=None, status_code: int = 200): row.action_label = _audit_action_label(row.action) role_key = (row.details or {}).get('role') row.role_label = str(ROLE_LABELS[role_key]) if role_key in ROLE_LABELS else role_key + include_product_owner = get_user_role_key(request.user) == ROLE_PLATFORM_OWNER return render( request, 'workflows/user_management.html', { - 'create_form': create_form or UserManagementCreateForm(), + 'create_form': create_form or UserManagementCreateForm(include_product_owner=include_product_owner), 'rows': _user_management_rows(), - 'role_choices': [(key, str(ROLE_LABELS[key])) for key in ROLE_GROUP_NAMES], + 'role_choices': [ + (key, str(ROLE_LABELS[key])) + for key in ROLE_GROUP_NAMES + if include_product_owner or key != ROLE_PLATFORM_OWNER + ], + 'include_product_owner': include_product_owner, 'recent_user_events': recent_user_events, }, status=status_code, ) +def _platform_owner_user_count() -> int: + user_model = get_user_model() + return sum(1 for user in user_model.objects.all() if get_user_role_key(user) == ROLE_PLATFORM_OWNER and user.is_active) + + def _super_admin_user_count() -> int: user_model = get_user_model() return sum(1 for user in user_model.objects.all() if get_user_role_key(user) == ROLE_SUPER_ADMIN and user.is_active) @@ -404,6 +416,20 @@ def _would_remove_last_super_admin(user, new_role_key: str | None = None, new_is return False +def _would_remove_last_platform_owner(user, new_role_key: str | None = None, new_is_active: bool | None = None, deleting: bool = False) -> bool: + if get_user_role_key(user) != ROLE_PLATFORM_OWNER or not user.is_active: + return False + if _platform_owner_user_count() > 1: + return False + if deleting: + return True + if new_role_key is not None and new_role_key != ROLE_PLATFORM_OWNER: + return True + if new_is_active is not None and not new_is_active: + return True + return False + + def _send_user_access_email(request, target_user, *, invitation: bool) -> None: email = (target_user.email or '').strip() if not email: @@ -413,17 +439,19 @@ def _send_user_access_email(request, target_user, *, invitation: bool) -> None: token = default_token_generator.make_token(target_user) reset_path = reverse('password_reset_confirm', kwargs={'uidb64': uid, 'token': token}) reset_url = request.build_absolute_uri(reset_path) + branding_copy = get_branding_email_copy() if invitation: subject = _('Zugangseinladung für %(username)s') % {'username': target_user.username} body = _( 'Hallo %(name)s,\n\n' - 'für Sie wurde ein Benutzerkonto im TUBCO Onboarding- und Offboarding-Portal angelegt.\n' + 'für Sie wurde ein Benutzerkonto im %(portal_title)s angelegt.\n' 'Bitte öffnen Sie den folgenden Link, um Ihr Passwort zu setzen:\n' '%(url)s\n\n' 'Wenn Sie diese Einladung nicht erwartet haben, melden Sie sich bitte bei Ihrem Administrator.' ) % { 'name': _display_user_name(target_user), + 'portal_title': branding_copy['portal_title'], 'url': reset_url, } else: @@ -447,10 +475,67 @@ def user_management_page(request): return _render_user_management(request) +@_require_capability('manage_product_branding') +def portal_branding_page(request): + branding, created = PortalBranding.objects.get_or_create(name='Default') + form = PortalBrandingForm(instance=branding) + return render( + request, + 'workflows/branding_settings.html', + { + 'form': form, + 'branding': branding, + }, + ) + + +@_require_capability('manage_product_branding') +@require_POST +def save_portal_branding(request): + branding, created = PortalBranding.objects.get_or_create(name='Default') + form = PortalBrandingForm(request.POST, request.FILES, instance=branding) + if not form.is_valid(): + messages.error(request, _('Branding konnte nicht gespeichert werden. Bitte prüfen Sie die Eingaben.')) + return render( + request, + 'workflows/branding_settings.html', + { + 'form': form, + 'branding': branding, + }, + status=400, + ) + + branding = form.save() + _audit( + request, + 'portal_branding_saved', + target_type='portal_branding', + target_id=branding.id, + target_label=branding.portal_title, + details={ + 'company_name': branding.company_name, + 'support_email': branding.support_email, + 'default_language': branding.default_language, + 'has_custom_logo': bool(branding.logo_image), + 'has_custom_letterhead': bool(branding.pdf_letterhead), + }, + ) + messages.success(request, _('Portal-Branding wurde gespeichert.')) + return render( + request, + 'workflows/branding_settings.html', + { + 'form': PortalBrandingForm(instance=branding), + 'branding': branding, + }, + ) + + @_require_capability('manage_users') @require_POST def create_user_from_admin(request): - form = UserManagementCreateForm(request.POST) + form = UserManagementCreateForm(request.POST, include_product_owner=(get_user_role_key(request.user) == ROLE_PLATFORM_OWNER)) if not form.is_valid(): messages.error(request, _('Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben.')) return _render_user_management(request, create_form=form, status_code=400) @@ -481,10 +566,20 @@ def update_user_from_admin(request, user_id: int): if role_key not in ROLE_GROUP_NAMES: messages.error(request, _('Ungültige Rolle.')) return redirect('user_management_page') + if role_key == ROLE_PLATFORM_OWNER and get_user_role_key(request.user) != ROLE_PLATFORM_OWNER: + messages.error(request, _('Nur Platform Owner dürfen diese Rolle vergeben.')) + return redirect('user_management_page') - if target_user == request.user and (role_key != ROLE_SUPER_ADMIN or not is_active): + current_role = get_user_role_key(request.user) + if target_user == request.user and current_role == ROLE_PLATFORM_OWNER and (role_key != ROLE_PLATFORM_OWNER or not is_active): + messages.error(request, _('Der aktuell angemeldete Platform Owner kann sich hier nicht selbst sperren oder herabstufen.')) + return redirect('user_management_page') + if target_user == request.user and current_role == ROLE_SUPER_ADMIN and (role_key != ROLE_SUPER_ADMIN or not is_active): messages.error(request, _('Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren oder herabstufen.')) return redirect('user_management_page') + if _would_remove_last_platform_owner(target_user, new_role_key=role_key, new_is_active=is_active): + messages.error(request, _('Der letzte aktive Platform Owner kann nicht deaktiviert oder herabgestuft werden.')) + return redirect('user_management_page') if _would_remove_last_super_admin(target_user, new_role_key=role_key, new_is_active=is_active): messages.error(request, _('Der letzte aktive Super Admin kann nicht deaktiviert oder herabgestuft werden.')) return redirect('user_management_page') @@ -535,9 +630,16 @@ def delete_user_from_admin(request, user_id: int): user_model = get_user_model() target_user = get_object_or_404(user_model, id=user_id) + current_role = get_user_role_key(request.user) + if target_user == request.user and current_role == ROLE_PLATFORM_OWNER: + messages.error(request, _('Der aktuell angemeldete Platform Owner kann sich hier nicht selbst löschen.')) + return redirect('user_management_page') if target_user == request.user: messages.error(request, _('Der aktuell angemeldete Super Admin kann sich hier nicht selbst löschen.')) return redirect('user_management_page') + if _would_remove_last_platform_owner(target_user, deleting=True): + messages.error(request, _('Der letzte aktive Platform Owner kann nicht gelöscht werden.')) + return redirect('user_management_page') if _would_remove_last_super_admin(target_user, deleting=True): messages.error(request, _('Der letzte aktive Super Admin kann nicht gelöscht werden.')) return redirect('user_management_page') @@ -1584,11 +1686,11 @@ def welcome_emails_page(request): rows = ScheduledWelcomeEmail.objects.select_related('onboarding_request').order_by('-send_at', '-id')[:200] config, _ = WorkflowConfig.objects.get_or_create(name='Default') welcome_template = NotificationTemplate.objects.filter(key='onboarding_welcome').first() - default_welcome = DEFAULT_NOTIFICATION_TEMPLATES.get('onboarding_welcome', {}) - default_subject = (default_welcome.get('subject') or 'Willkommen bei TUB/CO, {{ FULL_NAME }}').strip() - default_body = (default_welcome.get('body') or 'Hallo {{ FULL_NAME }}, willkommen bei TUB/CO.').strip() - default_subject_en = (default_welcome.get('subject_en') or 'Welcome to TUB/CO, {{ FULL_NAME }}').strip() - default_body_en = (default_welcome.get('body_en') or 'Hello {{ FULL_NAME }}, welcome to TUB/CO.').strip() + default_welcome = get_default_notification_templates().get('onboarding_welcome', {}) + default_subject = (default_welcome.get('subject') or '').strip() + default_body = (default_welcome.get('body') or '').strip() + default_subject_en = (default_welcome.get('subject_en') or '').strip() + default_body_en = (default_welcome.get('body_en') or '').strip() subject_value = (welcome_template.subject_template if welcome_template else '').strip() or default_subject body_value = (welcome_template.body_template if welcome_template else '').strip() or default_body subject_value_en = (welcome_template.subject_template_en if welcome_template else '').strip() or default_subject_en @@ -1650,11 +1752,11 @@ def save_welcome_email_settings(request): subject_en = request.POST.get('welcome_subject_en') body_en = request.POST.get('welcome_body_en') if subject is not None or body is not None or subject_en is not None or body_en is not None: - default_welcome = DEFAULT_NOTIFICATION_TEMPLATES.get('onboarding_welcome', {}) - default_subject = (default_welcome.get('subject') or 'Willkommen bei TUB/CO, {{ FULL_NAME }}').strip() - default_body = (default_welcome.get('body') or 'Hallo {{ FULL_NAME }}, willkommen bei TUB/CO.').strip() - default_subject_en = (default_welcome.get('subject_en') or 'Welcome to TUB/CO, {{ FULL_NAME }}').strip() - default_body_en = (default_welcome.get('body_en') or 'Hello {{ FULL_NAME }}, welcome to TUB/CO.').strip() + default_welcome = get_default_notification_templates().get('onboarding_welcome', {}) + default_subject = (default_welcome.get('subject') or '').strip() + default_body = (default_welcome.get('body') or '').strip() + default_subject_en = (default_welcome.get('subject_en') or '').strip() + default_body_en = (default_welcome.get('body_en') or '').strip() subject_clean = (subject or '').strip() or default_subject body_clean = (body or '').strip() or default_body subject_clean_en = (subject_en or '').strip() or default_subject_en