From 811bcd8745342ec3013775abf72c0da7ab597032 Mon Sep 17 00:00:00 2001 From: Md Bayazid Bostame Date: Thu, 26 Mar 2026 14:43:10 +0100 Subject: [PATCH] snapshot: preserve trial lifecycle and product-grade expiry UX --- backend/config/settings.py | 1 + backend/locale/en/LC_MESSAGES/django.mo | Bin 31745 -> 35206 bytes backend/locale/en/LC_MESSAGES/django.po | 559 +++++++++++++----- backend/workflows/admin.py | 7 +- backend/workflows/app_registry.py | 15 + backend/workflows/branding.py | 55 +- backend/workflows/context_processors.py | 3 +- backend/workflows/forms.py | 47 +- .../cleanup_expired_trial_workspace.py | 23 + backend/workflows/middleware.py | 34 ++ .../migrations/0043_portaltrialconfig.py | 33 ++ backend/workflows/models.py | 20 + backend/workflows/roles.py | 2 + backend/workflows/services.py | 5 + .../static/workflows/css/admin_tools.css | 80 +++ .../static/workflows/css/app_chrome.css | 104 ++++ .../templates/workflows/base_shell.html | 37 ++ .../workflows/developer_handbook.html | 12 + .../templates/workflows/project_wiki.html | 7 +- .../templates/workflows/trial_expired.html | 46 ++ .../templates/workflows/trial_management.html | 127 ++++ backend/workflows/trial.py | 59 ++ backend/workflows/urls.py | 2 + backend/workflows/views.py | 66 ++- 24 files changed, 1196 insertions(+), 148 deletions(-) create mode 100644 backend/workflows/management/commands/cleanup_expired_trial_workspace.py create mode 100644 backend/workflows/middleware.py create mode 100644 backend/workflows/migrations/0043_portaltrialconfig.py create mode 100644 backend/workflows/templates/workflows/trial_expired.html create mode 100644 backend/workflows/templates/workflows/trial_management.html create mode 100644 backend/workflows/trial.py diff --git a/backend/config/settings.py b/backend/config/settings.py index 5707e2c..0b714f5 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -44,6 +44,7 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'workflows.middleware.TrialModeMiddleware', ] ROOT_URLCONF = 'config.urls' diff --git a/backend/locale/en/LC_MESSAGES/django.mo b/backend/locale/en/LC_MESSAGES/django.mo index 1f6f36c3922d82ff390a587d53c5aa2892e8bb6d..699e7d3367a371f567cb197cafec442cdc0aab11 100644 GIT binary patch delta 12403 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 delta 8993 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 MxWm7EhO^B2AD}@bMF0Q* diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index 3a1ed4e..62fc5c0 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -2,14 +2,14 @@ msgid "" msgstr "" "Project-Id-Version: tubco-portal\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-26 12:55+0000\n" +"POT-Creation-Date: 2026-03-26 13:38+0000\n" "PO-Revision-Date: 2026-03-24 00:00+0000\n" "Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: workflows/app_registry.py:32 workflows/models.py:303 workflows/models.py:384 +#: workflows/app_registry.py:32 workflows/models.py:323 workflows/models.py:404 #: workflows/templates/workflows/onboarding_form.html:25 #: workflows/templates/workflows/requests_dashboard.html:68 #: workflows/templates/workflows/requests_dashboard.html:131 @@ -36,7 +36,7 @@ msgstr "Multi-step form" msgid "E-Mail Routing" msgstr "Email routing" -#: workflows/app_registry.py:43 workflows/models.py:304 workflows/models.py:385 +#: workflows/app_registry.py:43 workflows/models.py:324 workflows/models.py:405 #: workflows/templates/workflows/requests_dashboard.html:78 #: workflows/templates/workflows/requests_dashboard.html:132 msgid "Offboarding" @@ -95,6 +95,8 @@ msgstr "Search" #: workflows/templates/workflows/onboarding_intro_session.html:37 #: workflows/templates/workflows/request_timeline.html:70 #: workflows/templates/workflows/requests_dashboard.html:136 +#: workflows/templates/workflows/trial_expired.html:20 +#: workflows/templates/workflows/trial_management.html:25 #: workflows/templates/workflows/welcome_emails.html:85 msgid "Status" msgstr "Status" @@ -121,150 +123,162 @@ msgstr "" #: workflows/app_registry.py:121 workflows/app_registry.py:130 #: workflows/app_registry.py:139 workflows/app_registry.py:148 #: workflows/app_registry.py:157 workflows/app_registry.py:166 +#: workflows/app_registry.py:175 msgid "Öffnen" msgstr "Open" #: workflows/app_registry.py:74 +#: workflows/templates/workflows/trial_management.html:4 +#: workflows/templates/workflows/trial_management.html:12 +msgid "Trial Management" +msgstr "" + +#: workflows/app_registry.py:75 +msgid "" +"Testlaufzeit, Banner und sichere Einschränkungen für Demo-Umgebungen steuern." +msgstr "" + +#: workflows/app_registry.py:83 #: workflows/templates/workflows/branding_settings.html:4 #: workflows/templates/workflows/branding_settings.html:12 msgid "Branding" msgstr "Branding" -#: workflows/app_registry.py:75 +#: workflows/app_registry.py:84 msgid "Logo, Portalname, Farben und PDF-Briefkopf verwalten." msgstr "Manage logo, portal name, colors, and PDF letterhead." -#: workflows/app_registry.py:83 +#: workflows/app_registry.py:92 #: workflows/templates/workflows/app_registry.html:5 #: workflows/templates/workflows/app_registry.html:13 msgid "App Registry" msgstr "" -#: workflows/app_registry.py:84 +#: workflows/app_registry.py:93 msgid "Apps zentral aktivieren, sortieren und für Kundenauftritte vorbereiten." msgstr "" -#: workflows/app_registry.py:92 +#: workflows/app_registry.py:101 msgid "Integrationen" msgstr "Integrations" -#: workflows/app_registry.py:93 +#: workflows/app_registry.py:102 msgid "Nextcloud- und E-Mail-Setup." msgstr "Nextcloud and email setup." -#: workflows/app_registry.py:101 +#: workflows/app_registry.py:110 #: workflows/templates/workflows/user_management.html:4 #: workflows/templates/workflows/user_management.html:14 msgid "Benutzer & Rollen" msgstr "Users & roles" -#: workflows/app_registry.py:102 +#: workflows/app_registry.py:111 msgid "Benutzer anlegen, Rollen zuweisen und Zugriffe steuern." msgstr "Create users, assign roles, and control access." -#: workflows/app_registry.py:110 workflows/templates/workflows/audit_log.html:4 +#: workflows/app_registry.py:119 workflows/templates/workflows/audit_log.html:4 #: workflows/templates/workflows/audit_log.html:15 msgid "Audit Log" msgstr "" -#: workflows/app_registry.py:111 +#: workflows/app_registry.py:120 msgid "Wichtige Admin-Aktionen nachvollziehen und prüfen." msgstr "" -#: workflows/app_registry.py:119 +#: workflows/app_registry.py:128 #: workflows/templates/workflows/backup_recovery.html:4 #: workflows/templates/workflows/backup_recovery.html:12 msgid "Backup & Recovery" msgstr "Backup & Recovery" -#: workflows/app_registry.py:120 +#: workflows/app_registry.py:129 msgid "Backups erstellen und sicher verifizieren." msgstr "" -#: workflows/app_registry.py:128 +#: workflows/app_registry.py:137 #: workflows/templates/workflows/welcome_emails.html:4 msgid "Welcome E-Mails" msgstr "Welcome Emails" -#: workflows/app_registry.py:129 +#: workflows/app_registry.py:138 msgid "Geplante Welcome Mails verwalten." msgstr "Manage scheduled welcome emails." -#: workflows/app_registry.py:137 +#: workflows/app_registry.py:146 #: workflows/templates/workflows/form_builder.html:4 #: workflows/templates/workflows/form_builder.html:14 msgid "Form Builder" msgstr "Form Builder" -#: workflows/app_registry.py:138 +#: workflows/app_registry.py:147 msgid "Felder, Schritte und Optionen verwalten." msgstr "Manage fields, steps, and options." -#: workflows/app_registry.py:146 +#: workflows/app_registry.py:155 #: workflows/templates/workflows/intro_builder.html:4 #: workflows/templates/workflows/intro_builder.html:17 msgid "Einweisungs-Builder" msgstr "Introduction Builder" -#: workflows/app_registry.py:147 +#: workflows/app_registry.py:156 msgid "Checklistenpunkte für das Einweisungsprotokoll konfigurieren." msgstr "Configure checklist items for the introduction protocol." -#: workflows/app_registry.py:155 workflows/templates/workflows/handbook.html:4 +#: workflows/app_registry.py:164 workflows/templates/workflows/handbook.html:4 #: workflows/templates/workflows/handbook.html:15 msgid "Handbook" msgstr "Handbook" -#: workflows/app_registry.py:156 +#: workflows/app_registry.py:165 msgid "Project wiki and developer documentation in one place." msgstr "Project wiki and developer documentation in one place." -#: workflows/app_registry.py:164 +#: workflows/app_registry.py:173 msgid "Django Admin" msgstr "Django Admin" -#: workflows/app_registry.py:165 +#: workflows/app_registry.py:174 msgid "Vollständige Datenverwaltung." msgstr "Full data management." -#: workflows/app_registry.py:274 +#: workflows/app_registry.py:289 msgid "Nur Platform" msgstr "" -#: workflows/app_registry.py:276 +#: workflows/app_registry.py:291 #: workflows/templates/workflows/app_registry.html:85 msgid "Alle Firmenrollen" msgstr "" -#: workflows/app_registry.py:282 workflows/models.py:106 +#: workflows/app_registry.py:297 workflows/models.py:126 #: workflows/templates/workflows/app_registry.html:43 #: workflows/templates/workflows/app_registry.html:78 msgid "Apps" msgstr "Apps" -#: workflows/app_registry.py:283 +#: workflows/app_registry.py:298 msgid "Wählen Sie den gewünschten Prozess." msgstr "Choose the desired process." -#: workflows/app_registry.py:288 workflows/models.py:107 +#: workflows/app_registry.py:303 workflows/models.py:127 #: workflows/templates/workflows/app_registry.html:44 #: workflows/templates/workflows/app_registry.html:74 msgid "Platform Apps" msgstr "" -#: workflows/app_registry.py:289 +#: workflows/app_registry.py:304 #, fuzzy #| msgid "Konfiguration, Tests und Steuerung." msgid "Produktweite Konfiguration und Produktsteuerung." msgstr "Configuration, tests, and controls." -#: workflows/app_registry.py:294 workflows/models.py:108 +#: workflows/app_registry.py:309 workflows/models.py:128 #: workflows/templates/workflows/app_registry.html:45 #: workflows/templates/workflows/app_registry.html:76 msgid "Admin Apps" msgstr "Admin Apps" -#: workflows/app_registry.py:295 +#: workflows/app_registry.py:310 msgid "Konfiguration, Tests und Steuerung." msgstr "Configuration, tests, and controls." @@ -359,11 +373,11 @@ msgstr "Role:" msgid "Dieser Benutzername ist bereits vergeben." msgstr "This username is already taken." -#: workflows/forms.py:154 workflows/views.py:680 +#: workflows/forms.py:154 workflows/views.py:740 msgid "Ungültige Rolle." msgstr "Invalid role." -#: workflows/forms.py:156 workflows/views.py:683 +#: workflows/forms.py:156 workflows/views.py:743 msgid "Nur Platform Owner dürfen diese Rolle vergeben." msgstr "" @@ -469,7 +483,7 @@ msgstr "" msgid "Land" msgstr "" -#: workflows/forms.py:272 workflows/templates/workflows/base_shell.html:27 +#: workflows/forms.py:272 workflows/templates/workflows/base_shell.html:64 msgid "Website" msgstr "" @@ -507,261 +521,309 @@ msgstr "" msgid "Register- oder Handelsnummer" msgstr "" -#: workflows/forms.py:419 workflows/forms.py:604 +#: workflows/forms.py:297 +msgid "Trial-Modus aktiv" +msgstr "" + +#: workflows/forms.py:298 +msgid "Trial-Beginn" +msgstr "" + +#: workflows/forms.py:299 +msgid "Trial-Ende" +msgstr "" + +#: workflows/forms.py:300 +msgid "Produktive Integrationen begrenzen" +msgstr "" + +#: workflows/forms.py:301 +msgid "Cleanup nach Ablauf zulassen" +msgstr "" + +#: workflows/forms.py:302 +msgid "Banner-Text DE" +msgstr "" + +#: workflows/forms.py:303 +msgid "Banner-Text EN" +msgstr "" + +#: workflows/forms.py:323 +msgid "Bitte ein Trial-Ende festlegen." +msgstr "" + +#: workflows/forms.py:325 +msgid "Das Trial-Ende muss nach dem Trial-Beginn liegen." +msgstr "" + +#: workflows/forms.py:464 workflows/forms.py:649 #, python-format msgid "Bitte nutzen Sie das Format name@%(domain)s." msgstr "" -#: workflows/forms.py:441 workflows/forms.py:618 +#: workflows/forms.py:486 workflows/forms.py:663 #, python-format msgid "Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse." msgstr "" -#: workflows/forms.py:526 +#: workflows/forms.py:571 #, python-format msgid "" "Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen " "(frühestens %(date)s)." msgstr "" -#: workflows/models.py:181 workflows/views.py:200 +#: workflows/management/commands/cleanup_expired_trial_workspace.py:16 +msgid "Bitte mit --yes-delete bestätigen." +msgstr "" + +#: workflows/management/commands/cleanup_expired_trial_workspace.py:18 +msgid "Kein abgelaufener Trial mit aktiviertem Cleanup gefunden." +msgstr "" + +#: workflows/management/commands/cleanup_expired_trial_workspace.py:21 +msgid "Trial-Workspace bereinigt." +msgstr "" + +#: workflows/models.py:201 workflows/views.py:200 msgid "Eingereicht" msgstr "Submitted" -#: workflows/models.py:182 workflows/views.py:201 +#: workflows/models.py:202 workflows/views.py:201 msgid "In Bearbeitung" msgstr "Processing" -#: workflows/models.py:183 workflows/models.py:498 workflows/views.py:202 +#: workflows/models.py:203 workflows/models.py:518 workflows/views.py:202 msgid "Abgeschlossen" msgstr "Completed" -#: workflows/models.py:184 workflows/models.py:438 +#: workflows/models.py:204 workflows/models.py:458 #: workflows/templates/workflows/backup_recovery.html:70 #: workflows/templates/workflows/requests_dashboard.html:222 #: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:203 msgid "Fehlgeschlagen" msgstr "Failed" -#: workflows/models.py:191 +#: workflows/models.py:211 msgid "Herr" msgstr "" -#: workflows/models.py:191 +#: workflows/models.py:211 msgid "Frau" msgstr "" -#: workflows/models.py:191 +#: workflows/models.py:211 msgid "Divers" msgstr "" -#: workflows/models.py:201 +#: workflows/models.py:221 msgid "befristet" msgstr "" -#: workflows/models.py:201 +#: workflows/models.py:221 msgid "unbefristet" msgstr "" -#: workflows/models.py:264 +#: workflows/models.py:284 #: workflows/templates/workflows/onboarding_intro_session.html:28 #: workflows/templates/workflows/requests_dashboard.html:145 msgid "Abteilung" msgstr "Department" -#: workflows/models.py:265 +#: workflows/models.py:285 msgid "Geräte" msgstr "" -#: workflows/models.py:266 +#: workflows/models.py:286 msgid "Software" msgstr "" -#: workflows/models.py:267 +#: workflows/models.py:287 #, fuzzy #| msgid "Vorgänge" msgid "Zugänge" msgstr "Requests" -#: workflows/models.py:268 +#: workflows/models.py:288 msgid "Workspace-Gruppen" msgstr "" -#: workflows/models.py:269 +#: workflows/models.py:289 msgid "Ressourcen" msgstr "" -#: workflows/models.py:270 +#: workflows/models.py:290 msgid "Telefonnummern" msgstr "" -#: workflows/models.py:296 +#: workflows/models.py:316 msgid "Automatisch" msgstr "" -#: workflows/models.py:297 workflows/views.py:95 +#: workflows/models.py:317 workflows/views.py:95 msgid "Stammdaten" msgstr "Master data" -#: workflows/models.py:298 workflows/views.py:96 +#: workflows/models.py:318 workflows/views.py:96 msgid "Vertrag" msgstr "Contract" -#: workflows/models.py:299 workflows/views.py:97 +#: workflows/models.py:319 workflows/views.py:97 msgid "IT-Setup" msgstr "IT setup" -#: workflows/models.py:300 workflows/views.py:98 +#: workflows/models.py:320 workflows/views.py:98 msgid "Abschluss" msgstr "Finish" -#: workflows/models.py:342 +#: workflows/models.py:362 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: IT" msgstr "Onboarding" -#: workflows/models.py:343 +#: workflows/models.py:363 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Onboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:344 +#: workflows/models.py:364 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Visitenkarte" msgstr "Start onboarding" -#: workflows/models.py:345 +#: workflows/models.py:365 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: HR Works" msgstr "Onboarding" -#: workflows/models.py:346 +#: workflows/models.py:366 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Schlüssel" msgstr "Start onboarding" -#: workflows/models.py:347 +#: workflows/models.py:367 msgid "Onboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:348 +#: workflows/models.py:368 #, fuzzy #| msgid "Welcome E-Mails" msgid "Onboarding: Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/models.py:349 +#: workflows/models.py:369 #, fuzzy #| msgid "Offboarding" msgid "Offboarding: IT" msgstr "Offboarding" -#: workflows/models.py:350 +#: workflows/models.py:370 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Offboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:351 +#: workflows/models.py:371 #, fuzzy #| msgid "Offboarding starten" msgid "Offboarding: HR Works Deaktivierung" msgstr "Start offboarding" -#: workflows/models.py:352 +#: workflows/models.py:372 msgid "Offboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:388 +#: workflows/models.py:408 msgid "Immer" msgstr "" -#: workflows/models.py:389 workflows/models.py:467 +#: workflows/models.py:409 workflows/models.py:487 msgid "Enthält" msgstr "" -#: workflows/models.py:390 workflows/models.py:468 +#: workflows/models.py:410 workflows/models.py:488 msgid "Ist gleich" msgstr "" -#: workflows/models.py:391 +#: workflows/models.py:411 msgid "Ist aktiv/Ja" msgstr "" -#: workflows/models.py:392 +#: workflows/models.py:412 #, fuzzy #| msgid "inaktiv" msgid "Ist inaktiv/Nein" msgstr "inactive" -#: workflows/models.py:434 +#: workflows/models.py:454 #: workflows/templates/workflows/welcome_emails.html:100 msgid "Geplant" msgstr "Scheduled" -#: workflows/models.py:435 +#: workflows/models.py:455 #: workflows/templates/workflows/welcome_emails.html:102 msgid "Pausiert" msgstr "Paused" -#: workflows/models.py:436 +#: workflows/models.py:456 #: workflows/templates/workflows/welcome_emails.html:104 msgid "Abgebrochen" msgstr "Cancelled" -#: workflows/models.py:437 +#: workflows/models.py:457 #: workflows/templates/workflows/welcome_emails.html:106 msgid "Gesendet" msgstr "Sent" -#: workflows/models.py:460 workflows/tasks.py:576 +#: workflows/models.py:480 workflows/tasks.py:576 msgid "Geräte und Arbeitsplatz" msgstr "Devices and workplace" -#: workflows/models.py:461 workflows/tasks.py:577 +#: workflows/models.py:481 workflows/tasks.py:577 msgid "Konten und Berechtigungen" msgstr "Accounts and permissions" -#: workflows/models.py:462 workflows/tasks.py:578 +#: workflows/models.py:482 workflows/tasks.py:578 msgid "Software und Tools" msgstr "Software and tools" -#: workflows/models.py:463 workflows/tasks.py:579 +#: workflows/models.py:483 workflows/tasks.py:579 msgid "Prozesse und Hinweise" msgstr "Processes and notes" -#: workflows/models.py:466 +#: workflows/models.py:486 msgid "Immer anzeigen" msgstr "Always show" -#: workflows/models.py:469 +#: workflows/models.py:489 msgid "Ist Ja / aktiv" msgstr "Is yes / active" -#: workflows/models.py:470 +#: workflows/models.py:490 msgid "Ist Nein / inaktiv" msgstr "Is no / inactive" -#: workflows/models.py:497 +#: workflows/models.py:517 msgid "Entwurf" msgstr "Draft" -#: workflows/models.py:517 +#: workflows/models.py:537 #, fuzzy #| msgid "Nextcloud:" msgid "Nextcloud" msgstr "Nextcloud:" -#: workflows/models.py:518 +#: workflows/models.py:538 msgid "S3" msgstr "" -#: workflows/models.py:519 +#: workflows/models.py:539 msgid "NFS" msgstr "" @@ -923,6 +985,7 @@ msgstr "" #: workflows/templates/workflows/auth/password_reset_complete.html:19 #: workflows/templates/workflows/auth/password_reset_confirm.html:43 #: workflows/templates/workflows/auth/password_reset_done.html:19 +#: workflows/templates/workflows/trial_expired.html:39 msgid "Zur Anmeldung" msgstr "Back to sign in" @@ -1046,6 +1109,7 @@ msgstr "" #: workflows/templates/workflows/form_builder.html:91 #: workflows/templates/workflows/integrations_setup.html:263 #: workflows/templates/workflows/intro_builder.html:65 +#: workflows/templates/workflows/trial_management.html:28 #: workflows/templates/workflows/user_management.html:75 msgid "Aktiv" msgstr "Active" @@ -1053,6 +1117,8 @@ msgstr "Active" #: workflows/templates/workflows/app_registry.html:35 #: workflows/templates/workflows/app_registry.html:64 #: workflows/templates/workflows/backup_recovery.html:74 +#: workflows/templates/workflows/trial_management.html:30 +#: workflows/templates/workflows/trial_management.html:43 #, fuzzy #| msgid "Deaktivieren" msgid "Deaktiviert" @@ -1133,6 +1199,7 @@ msgstr "" #: workflows/templates/workflows/app_registry.html:154 #: workflows/templates/workflows/branding_settings.html:141 +#: workflows/templates/workflows/trial_management.html:105 msgid "Deutsch" msgstr "" @@ -1152,6 +1219,7 @@ msgstr "Actions" #: workflows/templates/workflows/app_registry.html:169 #: workflows/templates/workflows/branding_settings.html:152 +#: workflows/templates/workflows/trial_management.html:112 #, fuzzy #| msgid "English label" msgid "English" @@ -1396,36 +1464,73 @@ msgstr "Delete" msgid "Noch keine Backup-Bundles vorhanden." msgstr "No backup bundles available yet." +#: workflows/templates/workflows/base_shell.html:21 +#: workflows/templates/workflows/trial_expired.html:4 +#: workflows/templates/workflows/trial_expired.html:15 +msgid "Trial abgelaufen" +msgstr "Trial expired" + +#: workflows/templates/workflows/base_shell.html:21 +msgid "Trial-Modus" +msgstr "Trial mode" + +#: workflows/templates/workflows/base_shell.html:26 +msgid "Zugriff für Testnutzer gesperrt" +msgstr "" + #: workflows/templates/workflows/base_shell.html:28 +msgid "Kontrollierte Testumgebung aktiv" +msgstr "" + +#: workflows/templates/workflows/base_shell.html:36 +#, python-format +msgid "Diese Testumgebung ist seit %(expires)s abgelaufen." +msgstr "This trial environment has been expired since %(expires)s." + +#: workflows/templates/workflows/base_shell.html:38 +#, python-format +msgid "Diese Testumgebung ist bis %(expires)s aktiv." +msgstr "This trial environment is active until %(expires)s." + +#: workflows/templates/workflows/base_shell.html:41 +msgid "Diese Umgebung läuft im Trial-Modus." +msgstr "This environment is running in trial mode." + +#: workflows/templates/workflows/base_shell.html:47 +#: workflows/templates/workflows/trial_management.html:35 +msgid "Ende" +msgstr "End" + +#: workflows/templates/workflows/base_shell.html:65 msgid "Impressum" msgstr "" -#: workflows/templates/workflows/base_shell.html:29 +#: workflows/templates/workflows/base_shell.html:66 msgid "Datenschutz" msgstr "" -#: workflows/templates/workflows/base_shell.html:38 +#: workflows/templates/workflows/base_shell.html:75 msgid "Bitte bestätigen" msgstr "" -#: workflows/templates/workflows/base_shell.html:42 +#: workflows/templates/workflows/base_shell.html:79 #: workflows/templates/workflows/welcome_emails.html:134 msgid "Abbrechen" msgstr "Cancel" -#: workflows/templates/workflows/base_shell.html:43 +#: workflows/templates/workflows/base_shell.html:80 msgid "Bestätigen" msgstr "" -#: workflows/templates/workflows/base_shell.html:50 +#: workflows/templates/workflows/base_shell.html:87 msgid "Bitte warten" msgstr "Please wait" -#: workflows/templates/workflows/base_shell.html:51 +#: workflows/templates/workflows/base_shell.html:88 msgid "Aktion läuft" msgstr "Action in progress" -#: workflows/templates/workflows/base_shell.html:52 +#: workflows/templates/workflows/base_shell.html:89 msgid "Die Aktion wird im aktuellen Tab ausgeführt." msgstr "The action is running in the current tab." @@ -1849,6 +1954,7 @@ msgstr "Email:" #: workflows/templates/workflows/home.html:44 #: workflows/templates/workflows/integrations_setup.html:122 +#: workflows/templates/workflows/trial_management.html:49 msgid "Testmodus" msgstr "Test mode" @@ -2428,7 +2534,7 @@ msgid "Dienstliche E-Mail" msgstr "Work email" #: workflows/templates/workflows/onboarding_intro_session.html:31 -#: workflows/views.py:933 +#: workflows/views.py:993 msgid "Vertragsbeginn" msgstr "Contract start" @@ -2868,6 +2974,171 @@ msgstr "Delete this option?" msgid "Noch keine Vorgänge vorhanden." msgstr "No requests available yet." +#: workflows/templates/workflows/trial_expired.html:14 +msgid "Trial expired" +msgstr "" + +#: workflows/templates/workflows/trial_expired.html:16 +msgid "" +"Diese Testumgebung ist nicht mehr aktiv. Bitte wenden Sie sich für eine " +"Verlängerung oder ein Produktiv-Setup an den Plattformbetreiber." +msgstr "" + +#: workflows/templates/workflows/trial_expired.html:21 +msgid "Zugriff gesperrt" +msgstr "" + +#: workflows/templates/workflows/trial_expired.html:22 +msgid "" +"Nicht-Platform-Nutzer können diese Umgebung nach Ablauf nicht mehr verwenden." +msgstr "" + +#: workflows/templates/workflows/trial_expired.html:25 +msgid "Nächster Schritt" +msgstr "" + +#: workflows/templates/workflows/trial_expired.html:26 +msgid "Verlängern oder Produktiv-Setup" +msgstr "" + +#: workflows/templates/workflows/trial_expired.html:27 +msgid "" +"Ein Platform Owner kann den Trial verlängern oder das Setup in einen " +"regulären Betrieb überführen." +msgstr "" + +#: workflows/templates/workflows/trial_expired.html:31 +#, fuzzy +#| msgid "Abgelaufen" +msgid "Ablaufzeit" +msgstr "Expired" + +#: workflows/templates/workflows/trial_expired.html:33 +msgid "Das ist der im System hinterlegte Endzeitpunkt der Testumgebung." +msgstr "" + +#: workflows/templates/workflows/trial_expired.html:42 +#, python-format +msgid "Kontakt: %(email)s" +msgstr "Contact: %(email)s" + +#: workflows/templates/workflows/trial_management.html:13 +msgid "" +"Testlaufzeit, Banner und sichere Einschränkungen für Demo- und " +"Pilotumgebungen steuern." +msgstr "" +"Control trial runtime, banner messaging, and safe restrictions for demo and " +"pilot environments." + +#: workflows/templates/workflows/trial_management.html:20 +msgid "Übersicht" +msgstr "Overview" + +#: workflows/templates/workflows/trial_management.html:21 +msgid "Aktueller Trial-Status und die daraus resultierende Systemwirkung." +msgstr "Current trial status and the resulting system effect." + +#: workflows/templates/workflows/trial_management.html:28 +msgid "Abgelaufen" +msgstr "Expired" + +#: workflows/templates/workflows/trial_management.html:37 +msgid "Nicht gesetzt" +msgstr "Not set" + +#: workflows/templates/workflows/trial_management.html:41 +msgid "Nextcloud effektiv" +msgstr "Nextcloud effective" + +#: workflows/templates/workflows/trial_management.html:43 +#: workflows/templates/workflows/trial_management.html:49 +msgid "Unverändert" +msgstr "Unchanged" + +#: workflows/templates/workflows/trial_management.html:47 +msgid "E-Mail effektiv" +msgstr "Email effective" + +#: workflows/templates/workflows/trial_management.html:54 +msgid "" +"Zum Deaktivieren des Trial-Modus entfernen Sie den Haken bei „Trial-Modus " +"aktiv“ und speichern Sie die Seite." +msgstr "" +"To disable trial mode, remove the checkmark from ‘Trial mode enabled’ and " +"save the page." + +#: workflows/templates/workflows/trial_management.html:63 +msgid "Trial-Status" +msgstr "Trial status" + +#: workflows/templates/workflows/trial_management.html:64 +msgid "Aktivieren Sie den Trial-Modus und definieren Sie die gültige Laufzeit." +msgstr "Enable trial mode and define the valid runtime." + +#: workflows/templates/workflows/trial_management.html:70 +msgid "Diese Deployment-Umgebung als Trial führen" +msgstr "Run this deployment as a trial environment" + +#: workflows/templates/workflows/trial_management.html:72 +msgid "" +"Sobald dieser Schalter deaktiviert ist, verschwindet das Trial-Banner und " +"die normalen Integrationsregeln greifen wieder." +msgstr "" +"As soon as this switch is disabled, the trial banner disappears and the " +"normal integration rules apply again." + +#: workflows/templates/workflows/trial_management.html:81 +msgid "Der konfigurierte Trial ist derzeit abgelaufen." +msgstr "The configured trial is currently expired." + +#: workflows/templates/workflows/trial_management.html:88 +msgid "Sicherheitsregeln" +msgstr "Safety rules" + +#: workflows/templates/workflows/trial_management.html:89 +msgid "Testumgebungen sollen keine produktiven Integrationen verwenden." +msgstr "Trial environments should not use production integrations." + +#: workflows/templates/workflows/trial_management.html:92 +msgid "Nextcloud produktiv deaktivieren und E-Mail-Testmodus erzwingen" +msgstr "Disable production Nextcloud and force email test mode" + +#: workflows/templates/workflows/trial_management.html:93 +msgid "Cleanup nach Ablauf vorbereiten" +msgstr "Allow cleanup after expiry" + +#: workflows/templates/workflows/trial_management.html:95 +msgid "" +"Wenn diese Regel aktiv ist, bleiben produktive Integrationen technisch " +"gesperrt, auch wenn lokale Overrides anders gesetzt sind." +msgstr "" +"When this rule is active, production integrations remain technically blocked " +"even if local overrides are set differently." + +#: workflows/templates/workflows/trial_management.html:100 +msgid "Banner" +msgstr "Banner" + +#: workflows/templates/workflows/trial_management.html:101 +msgid "" +"Optionaler Hinweistext für die Shell. Ohne Text wird ein Standardhinweis mit " +"Enddatum verwendet." +msgstr "" +"Optional notice text for the shell. Without custom text, a default notice " +"with the expiry date is used." + +#: workflows/templates/workflows/trial_management.html:122 +msgid "" +"Die eigentliche Datenbereinigung läuft bewusst nicht über die Web-UI. Nutzen " +"Sie dafür den Cleanup-Command im Betrieb." +msgstr "" +"Actual data cleanup is intentionally not done through the web UI. Use the " +"cleanup command during operations instead." + +#: workflows/templates/workflows/trial_management.html:123 +msgid "Trial-Konfiguration speichern" +msgstr "Save trial configuration" + #: workflows/templates/workflows/user_management.html:15 #, fuzzy #| msgid "" @@ -3093,7 +3364,7 @@ msgstr "Devices, software, and access" msgid "Notizen und Freigabe" msgstr "Notes and approval" -#: workflows/views.py:129 workflows/views.py:1019 workflows/views.py:1024 +#: workflows/views.py:129 workflows/views.py:1079 workflows/views.py:1084 msgid "Sie haben keine Berechtigung für diese Aktion." msgstr "You do not have permission for this action." @@ -3386,17 +3657,30 @@ msgstr "User could not be created. Please check the input." msgid "Firmenkonfiguration wurde gespeichert." msgstr "Save offboarding request" -#: workflows/views.py:653 +#: workflows/views.py:669 +#, fuzzy +#| msgid "" +#| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." +msgid "" +"Trial-Konfiguration konnte nicht gespeichert werden. Bitte prüfen Sie die " +"Eingaben." +msgstr "Trial configuration could not be saved. Please check the input." + +#: workflows/views.py:696 +msgid "Trial-Konfiguration wurde gespeichert." +msgstr "Trial configuration was saved." + +#: workflows/views.py:713 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:666 +#: workflows/views.py:726 #, 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:688 +#: workflows/views.py:748 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -3407,14 +3691,14 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:691 +#: workflows/views.py:751 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:694 +#: workflows/views.py:754 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -3425,7 +3709,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:697 +#: workflows/views.py:757 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -3436,18 +3720,18 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:714 +#: workflows/views.py:774 #, python-format msgid "Benutzer wurde aktualisiert: %(username)s" msgstr "User updated: %(username)s" -#: workflows/views.py:736 +#: workflows/views.py:796 #, 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:748 +#: workflows/views.py:808 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -3457,7 +3741,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:751 +#: workflows/views.py:811 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -3467,7 +3751,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:754 +#: workflows/views.py:814 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -3476,7 +3760,7 @@ 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:757 +#: workflows/views.py:817 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -3485,121 +3769,121 @@ 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:770 +#: workflows/views.py:830 #, fuzzy, python-format #| msgid "Benutzer wurde erstellt: %(username)s" msgid "Benutzer wurde gelöscht: %(username)s" msgstr "User created: %(username)s" -#: workflows/views.py:857 +#: workflows/views.py:917 #, python-format msgid "Backup wurde erstellt: %(name)s" msgstr "" -#: workflows/views.py:859 +#: workflows/views.py:919 #, python-format msgid "Backup konnte nicht erstellt werden: %(error)s" msgstr "" -#: workflows/views.py:875 +#: workflows/views.py:935 #, python-format msgid "Backup wurde verifiziert: %(name)s" msgstr "" -#: workflows/views.py:877 +#: workflows/views.py:937 #, python-format msgid "Backup-Verifikation fehlgeschlagen: %(error)s" msgstr "" -#: workflows/views.py:893 +#: workflows/views.py:953 #, python-format msgid "Backup wurde gelöscht: %(name)s" msgstr "" -#: workflows/views.py:895 +#: workflows/views.py:955 #, python-format msgid "Backup konnte nicht gelöscht werden: %(error)s" msgstr "" -#: workflows/views.py:921 +#: workflows/views.py:981 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Anfrage erstellt" msgstr "Request saved" -#: workflows/views.py:923 +#: workflows/views.py:983 #, fuzzy, python-format #| msgid "Sitzungsstatus" msgid "Status: %(status)s" msgstr "Session status" -#: workflows/views.py:935 +#: workflows/views.py:995 #, fuzzy #| msgid "Geplant für" msgid "Geplanter Start" msgstr "Scheduled for" -#: workflows/views.py:945 +#: workflows/views.py:1005 msgid "Geräteübergabe / Hardware-Abholung" msgstr "" -#: workflows/views.py:947 +#: workflows/views.py:1007 msgid "Geplanter Hardware-Termin" msgstr "" -#: workflows/views.py:956 +#: workflows/views.py:1016 #, fuzzy #| msgid "Noch nicht verfügbar" msgid "PDF verfügbar" msgstr "Not available yet" -#: workflows/views.py:982 +#: workflows/views.py:1042 #, fuzzy #| msgid "Einweisung" msgid "Einweisungssitzung" msgstr "Introduction" -#: workflows/views.py:994 +#: workflows/views.py:1054 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/views.py:1033 +#: workflows/views.py:1093 msgid "Keine Einträge ausgewählt." msgstr "No entries selected." -#: workflows/views.py:1076 +#: workflows/views.py:1136 #, python-format msgid "%(count)s Eintrag/Einträge gelöscht." msgstr "%(count)s entry/entries deleted." -#: workflows/views.py:1078 +#: workflows/views.py:1138 #, python-format msgid "%(count)s Auswahl(en) konnten nicht verarbeitet werden." msgstr "%(count)s selection(s) could not be processed." -#: workflows/views.py:1080 +#: workflows/views.py:1140 msgid "Keine passenden Einträge gefunden." msgstr "No matching entries found." -#: workflows/views.py:1308 +#: workflows/views.py:1368 msgid "Einweisungs- und Übergabeprotokoll wurde erzeugt." msgstr "Introduction and handover protocol was generated." -#: workflows/views.py:1325 +#: workflows/views.py:1385 msgid "Einweisungsprotokoll aus Live-Status wurde erzeugt." msgstr "Introduction protocol from live status was generated." -#: workflows/views.py:1354 +#: workflows/views.py:1414 msgid "Einweisung wurde zurückgesetzt." msgstr "Introduction was reset." -#: workflows/views.py:1368 +#: workflows/views.py:1428 msgid "Einweisung wurde als abgeschlossen gespeichert." msgstr "Introduction was saved as completed." -#: workflows/views.py:1381 +#: workflows/views.py:1441 msgid "Einweisung wurde als Entwurf gespeichert." msgstr "Introduction was saved as draft." @@ -3636,11 +3920,6 @@ msgstr "Introduction was saved as draft." #~ msgid "Anmeldung fehlgeschlagen. Bitte Zugangsdaten prüfen." #~ msgstr "Login failed. Please check your credentials." -#, fuzzy -#~| msgid "Nextcloud speichern" -#~ msgid "Nextcloud deaktivieren" -#~ msgstr "Save Nextcloud" - #~ msgid "Aktiv/Inaktiv direkt umschalten." #~ msgstr "Switch active/inactive directly." diff --git a/backend/workflows/admin.py b/backend/workflows/admin.py index 21860b3..b861da5 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, PortalAppConfig, PortalBranding, PortalCompanyConfig, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig +from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig @admin.register(EmployeeProfile) @@ -30,6 +30,11 @@ class PortalCompanyConfigAdmin(admin.ModelAdmin): list_display = ('name', 'legal_company_name', 'website_url', 'hr_contact_email', 'it_contact_email', 'updated_at') +@admin.register(PortalTrialConfig) +class PortalTrialConfigAdmin(admin.ModelAdmin): + list_display = ('name', 'is_trial_mode', 'trial_expires_at', 'restrict_production_integrations', 'auto_cleanup_enabled', 'updated_at') + + @admin.register(PortalAppConfig) class PortalAppConfigAdmin(admin.ModelAdmin): list_display = ( diff --git a/backend/workflows/app_registry.py b/backend/workflows/app_registry.py index d65d47b..2d6aee3 100644 --- a/backend/workflows/app_registry.py +++ b/backend/workflows/app_registry.py @@ -67,6 +67,15 @@ APP_DEFINITIONS: tuple[AppDefinition, ...] = ( action_label=_('Öffnen'), capability='manage_company_config', ), + AppDefinition( + key='trial_management', + section=PortalAppConfig.SECTION_PLATFORM, + route_name='portal_trial_config_page', + title=_('Trial Management'), + description=_('Testlaufzeit, Banner und sichere Einschränkungen für Demo-Umgebungen steuern.'), + action_label=_('Öffnen'), + capability='manage_trial_lifecycle', + ), AppDefinition( key='branding', section=PortalAppConfig.SECTION_PLATFORM, @@ -200,6 +209,12 @@ DEFAULT_ROLE_VISIBILITY = { ROLE_IT_STAFF: False, ROLE_STAFF: False, }, + 'trial_management': { + ROLE_SUPER_ADMIN: False, + ROLE_ADMIN: False, + ROLE_IT_STAFF: False, + ROLE_STAFF: False, + }, 'app_registry': { ROLE_SUPER_ADMIN: False, ROLE_ADMIN: False, diff --git a/backend/workflows/branding.py b/backend/workflows/branding.py index a4300e3..18607e0 100644 --- a/backend/workflows/branding.py +++ b/backend/workflows/branding.py @@ -5,9 +5,10 @@ from email.utils import formataddr from django.conf import settings from django.templatetags.static import static +from django.utils import timezone from django.utils.translation import get_language -from .models import PortalBranding, PortalCompanyConfig +from .models import PortalBranding, PortalCompanyConfig, PortalTrialConfig def get_portal_branding() -> PortalBranding: @@ -52,6 +53,58 @@ def get_portal_company_config() -> PortalCompanyConfig: return company_config +def get_portal_trial_config() -> PortalTrialConfig: + trial_config, _ = PortalTrialConfig.objects.get_or_create( + name='Default', + defaults={ + 'is_trial_mode': False, + 'restrict_production_integrations': True, + 'auto_cleanup_enabled': True, + 'trial_banner_text': '', + 'trial_banner_text_en': '', + }, + ) + return trial_config + + +def is_trial_mode_enabled() -> bool: + return bool(get_portal_trial_config().is_trial_mode) + + +def is_trial_expired() -> bool: + trial_config = get_portal_trial_config() + if not trial_config.is_trial_mode or not trial_config.trial_expires_at: + return False + return timezone.now() >= trial_config.trial_expires_at + + +def should_restrict_trial_integrations() -> bool: + trial_config = get_portal_trial_config() + return bool(trial_config.is_trial_mode and trial_config.restrict_production_integrations) + + +def get_trial_context() -> dict[str, object]: + trial_config = get_portal_trial_config() + lang = (get_language() or 'de').split('-')[0] + banner_text = ((trial_config.trial_banner_text_en or '').strip() if lang == 'en' else '') or (trial_config.trial_banner_text or '').strip() + expired = is_trial_expired() + days_remaining = None + if trial_config.is_trial_mode and trial_config.trial_expires_at: + delta = timezone.localtime(trial_config.trial_expires_at) - timezone.localtime(timezone.now()) + days_remaining = max(0, delta.days + (1 if delta.seconds > 0 else 0)) + return { + 'portal_trial_config': trial_config, + 'portal_trial_enabled': bool(trial_config.is_trial_mode), + 'portal_trial_expired': expired, + 'portal_trial_started_at': trial_config.trial_started_at, + 'portal_trial_expires_at': trial_config.trial_expires_at, + 'portal_trial_days_remaining': days_remaining, + 'portal_trial_banner_text': banner_text, + 'portal_trial_restrict_integrations': bool(trial_config.is_trial_mode and trial_config.restrict_production_integrations), + 'portal_trial_cleanup_enabled': bool(trial_config.auto_cleanup_enabled), + } + + def get_company_email_domain() -> str: branding = get_portal_branding() domain = (branding.company_domain or '').strip().lower().lstrip('@') diff --git a/backend/workflows/context_processors.py b/backend/workflows/context_processors.py index 162e55b..91208d2 100644 --- a/backend/workflows/context_processors.py +++ b/backend/workflows/context_processors.py @@ -1,8 +1,9 @@ -from .branding import get_branding_context +from .branding import get_branding_context, get_trial_context from .roles import template_role_context def role_context(request): context = template_role_context(getattr(request, 'user', None)) context.update(get_branding_context()) + context.update(get_trial_context()) return context diff --git a/backend/workflows/forms.py b/backend/workflows/forms.py index 74fdf94..8ddc4d3 100644 --- a/backend/workflows/forms.py +++ b/backend/workflows/forms.py @@ -8,7 +8,7 @@ from django.utils.translation import get_language, gettext as _, gettext_lazy from .branding import get_company_email_domain from .form_builder import apply_form_field_config -from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, PortalCompanyConfig, WorkflowConfig +from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, PortalCompanyConfig, PortalTrialConfig, 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 @@ -281,6 +281,51 @@ class PortalCompanyConfigForm(forms.ModelForm): } +class PortalTrialConfigForm(forms.ModelForm): + class Meta: + model = PortalTrialConfig + fields = [ + 'is_trial_mode', + 'trial_started_at', + 'trial_expires_at', + 'restrict_production_integrations', + 'auto_cleanup_enabled', + 'trial_banner_text', + 'trial_banner_text_en', + ] + labels = { + 'is_trial_mode': gettext_lazy('Trial-Modus aktiv'), + 'trial_started_at': gettext_lazy('Trial-Beginn'), + 'trial_expires_at': gettext_lazy('Trial-Ende'), + 'restrict_production_integrations': gettext_lazy('Produktive Integrationen begrenzen'), + 'auto_cleanup_enabled': gettext_lazy('Cleanup nach Ablauf zulassen'), + 'trial_banner_text': gettext_lazy('Banner-Text DE'), + 'trial_banner_text_en': gettext_lazy('Banner-Text EN'), + } + widgets = { + 'trial_started_at': forms.DateTimeInput(attrs={'type': 'datetime-local'}), + 'trial_expires_at': forms.DateTimeInput(attrs={'type': 'datetime-local'}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field_name in ('trial_started_at', 'trial_expires_at'): + field = self.fields[field_name] + if self.instance and getattr(self.instance, field_name): + field.initial = timezone.localtime(getattr(self.instance, field_name)).strftime('%Y-%m-%dT%H:%M') + field.input_formats = ['%Y-%m-%dT%H:%M'] + + def clean(self): + cleaned = super().clean() + started = cleaned.get('trial_started_at') + expires = cleaned.get('trial_expires_at') + if cleaned.get('is_trial_mode') and not expires: + self.add_error('trial_expires_at', _('Bitte ein Trial-Ende festlegen.')) + if started and expires and expires <= started: + self.add_error('trial_expires_at', _('Das Trial-Ende muss nach dem Trial-Beginn liegen.')) + return cleaned + + 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/cleanup_expired_trial_workspace.py b/backend/workflows/management/commands/cleanup_expired_trial_workspace.py new file mode 100644 index 0000000..05bc48e --- /dev/null +++ b/backend/workflows/management/commands/cleanup_expired_trial_workspace.py @@ -0,0 +1,23 @@ +from django.core.management.base import BaseCommand, CommandError +from django.utils.translation import gettext as _ + +from workflows.trial import cleanup_trial_workspace_data, trial_cleanup_is_due + + +class Command(BaseCommand): + help = 'Deletes operational trial data after trial expiry while keeping platform configuration.' + + def add_arguments(self, parser): + parser.add_argument('--force', action='store_true', help='Run cleanup even if the trial is not due yet.') + parser.add_argument('--yes-delete', action='store_true', help='Confirm destructive cleanup.') + + def handle(self, *args, **options): + if not options['yes_delete']: + raise CommandError(_('Bitte mit --yes-delete bestätigen.')) + if not options['force'] and not trial_cleanup_is_due(): + raise CommandError(_('Kein abgelaufener Trial mit aktiviertem Cleanup gefunden.')) + + result = cleanup_trial_workspace_data() + self.stdout.write(self.style.SUCCESS(_('Trial-Workspace bereinigt.'))) + for key, value in result.items(): + self.stdout.write(f'- {key}: {value}') diff --git a/backend/workflows/middleware.py b/backend/workflows/middleware.py new file mode 100644 index 0000000..55ec825 --- /dev/null +++ b/backend/workflows/middleware.py @@ -0,0 +1,34 @@ +from django.shortcuts import render + +from .branding import is_trial_expired, is_trial_mode_enabled +from .roles import ROLE_PLATFORM_OWNER, get_user_role_key + + +class TrialModeMiddleware: + EXEMPT_PREFIXES = ( + '/healthz/', + '/i18n/', + '/accounts/login/', + '/accounts/logout/', + '/accounts/password_reset/', + '/accounts/reset/', + '/static/', + '/media/', + ) + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if not is_trial_mode_enabled() or not is_trial_expired(): + return self.get_response(request) + + path = request.path or '/' + if any(path.startswith(prefix) for prefix in self.EXEMPT_PREFIXES): + return self.get_response(request) + + user = getattr(request, 'user', None) + if getattr(user, 'is_authenticated', False) and get_user_role_key(user) == ROLE_PLATFORM_OWNER: + return self.get_response(request) + + return render(request, 'workflows/trial_expired.html', status=403) diff --git a/backend/workflows/migrations/0043_portaltrialconfig.py b/backend/workflows/migrations/0043_portaltrialconfig.py new file mode 100644 index 0000000..7a0ab01 --- /dev/null +++ b/backend/workflows/migrations/0043_portaltrialconfig.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.5 on 2026-03-26 13:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0042_portalcompanyconfig'), + ] + + operations = [ + migrations.CreateModel( + name='PortalTrialConfig', + 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)), + ('is_trial_mode', models.BooleanField(default=False)), + ('trial_started_at', models.DateTimeField(blank=True, null=True)), + ('trial_expires_at', models.DateTimeField(blank=True, null=True)), + ('restrict_production_integrations', models.BooleanField(default=True)), + ('auto_cleanup_enabled', models.BooleanField(default=True)), + ('trial_banner_text', models.CharField(blank=True, default='', max_length=255)), + ('trial_banner_text_en', models.CharField(blank=True, default='', max_length=255)), + ('last_cleanup_at', models.DateTimeField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'Portal Trial Config', + 'verbose_name_plural': 'Portal Trial Config', + }, + ), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index 841bbe4..b0a0741 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -98,6 +98,26 @@ class PortalCompanyConfig(models.Model): return self.legal_company_name or self.name +class PortalTrialConfig(models.Model): + name = models.CharField(max_length=80, default='Default', unique=True) + is_trial_mode = models.BooleanField(default=False) + trial_started_at = models.DateTimeField(null=True, blank=True) + trial_expires_at = models.DateTimeField(null=True, blank=True) + restrict_production_integrations = models.BooleanField(default=True) + auto_cleanup_enabled = models.BooleanField(default=True) + trial_banner_text = models.CharField(max_length=255, blank=True, default='') + trial_banner_text_en = models.CharField(max_length=255, blank=True, default='') + last_cleanup_at = models.DateTimeField(null=True, blank=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = 'Portal Trial Config' + verbose_name_plural = 'Portal Trial Config' + + def __str__(self) -> str: + return self.name + + class PortalAppConfig(models.Model): SECTION_APP = 'app' SECTION_PLATFORM = 'platform' diff --git a/backend/workflows/roles.py b/backend/workflows/roles.py index 437896d..4b81ce7 100644 --- a/backend/workflows/roles.py +++ b/backend/workflows/roles.py @@ -30,6 +30,7 @@ CAPABILITIES = { 'manage_users': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN}, 'manage_product_branding': {ROLE_PLATFORM_OWNER}, 'manage_company_config': {ROLE_PLATFORM_OWNER}, + 'manage_trial_lifecycle': {ROLE_PLATFORM_OWNER}, 'manage_app_registry': {ROLE_PLATFORM_OWNER}, 'access_requests_dashboard': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF}, 'view_request_timeline': {ROLE_PLATFORM_OWNER, ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, @@ -127,6 +128,7 @@ def template_role_context(user) -> dict[str, object]: 'role_label': str(ROLE_LABELS[role_key]), 'can_manage_product_branding': user_has_capability(user, 'manage_product_branding'), 'can_manage_company_config': user_has_capability(user, 'manage_company_config'), + 'can_manage_trial_lifecycle': user_has_capability(user, 'manage_trial_lifecycle'), 'can_manage_app_registry': user_has_capability(user, 'manage_app_registry'), 'can_manage_users': user_has_capability(user, 'manage_users'), 'can_access_requests_dashboard': user_has_capability(user, 'access_requests_dashboard'), diff --git a/backend/workflows/services.py b/backend/workflows/services.py index dfd8f8a..d4ce1ec 100644 --- a/backend/workflows/services.py +++ b/backend/workflows/services.py @@ -5,6 +5,7 @@ import time import requests from django.conf import settings +from .branding import should_restrict_trial_integrations from .models import WorkflowConfig logger = logging.getLogger(__name__) @@ -15,6 +16,8 @@ def _active_workflow_config() -> WorkflowConfig | None: def is_nextcloud_enabled() -> bool: + if should_restrict_trial_integrations(): + return False config = _active_workflow_config() if config and config.nextcloud_enabled_override is not None: return bool(config.nextcloud_enabled_override) @@ -22,6 +25,8 @@ def is_nextcloud_enabled() -> bool: def is_email_test_mode() -> bool: + if should_restrict_trial_integrations(): + return True config = _active_workflow_config() if config and config.email_test_mode_override is not None: return bool(config.email_test_mode_override) diff --git a/backend/workflows/static/workflows/css/admin_tools.css b/backend/workflows/static/workflows/css/admin_tools.css index f05f1f8..619303a 100644 --- a/backend/workflows/static/workflows/css/admin_tools.css +++ b/backend/workflows/static/workflows/css/admin_tools.css @@ -32,6 +32,82 @@ h1 { margin: 12px 0 6px; color: #000078; } .branding-preview-footer-main { color: #20385f; font-size: 11px; font-weight: 700; line-height: 1.35; } .branding-preview-footer-legal { margin-top: 4px; color: #6c7f99; font-size: 10px; line-height: 1.4; } .backup-grid { grid-template-columns: minmax(280px, 720px); } +.trial-overview { padding-bottom: 12px; } +.trial-summary-grid { display: grid; grid-template-columns: repeat(4, minmax(150px, 1fr)); gap: 10px; } +.trial-summary-card { border: 1px solid #d9e4f1; border-radius: 14px; background: rgba(255,255,255,0.86); padding: 12px; display: grid; gap: 6px; } +.trial-summary-label { color: #60738d; font-size: 12px; font-weight: 700; } +.trial-summary-value { color: #17345e; font-size: 16px; line-height: 1.2; } +.trial-summary-value.is-active { color: #166534; } +.trial-summary-value.is-warn { color: #8a5a00; } +.trial-summary-value.is-inactive { color: #7a1f1f; } +.trial-summary-value.is-expired { color: #9f1d1d; } +.trial-expired-shell { padding: 28px 24px 36px; } +.trial-expired-card { + max-width: 900px; + margin: 0 auto; + border: 1px solid #e7d1d1; + border-radius: 24px; + background: + radial-gradient(circle at top right, rgba(201, 68, 68, 0.12), transparent 24%), + linear-gradient(180deg, rgba(255,255,255,0.99), rgba(255,247,247,0.96)); + box-shadow: + inset 0 1px 0 rgba(255,255,255,0.94), + 0 18px 36px rgba(16, 32, 57, 0.08); + padding: 24px; +} +.trial-expired-card h1 { margin: 10px 0 8px; color: #7f1d1d; } +.trial-expired-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 7px 11px; + border-radius: 999px; + border: 1px solid rgba(159, 29, 29, 0.14); + background: rgba(255,255,255,0.75); + color: #9f1d1d; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.05em; + text-transform: uppercase; +} +.trial-expired-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + margin-top: 16px; +} +.trial-expired-panel { + border: 1px solid #ead9d9; + border-radius: 16px; + background: rgba(255,255,255,0.82); + padding: 14px; + display: grid; + gap: 6px; +} +.trial-expired-label { + color: #8e5a5a; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.05em; + text-transform: uppercase; +} +.trial-expired-panel strong { + color: #6f1d1d; + font-size: 15px; + line-height: 1.25; +} +.trial-expired-panel p { + margin: 0; + color: #805c5c; + font-size: 13px; + line-height: 1.5; +} +.trial-expired-contact { + margin-top: 14px; + color: #7a5252; + font-size: 13px; + font-weight: 700; +} label { display: block; margin-bottom: 4px; font-size: 12px; color: #334155; font-weight: 700; } input, select, textarea { width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; padding: 8px 9px; background: #fff; transition: border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 180ms cubic-bezier(0.2, 0.8, 0.2, 1), background-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1); } textarea { min-height: 120px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; } @@ -111,6 +187,10 @@ th { background: #f6f9ff; color: #334155; } .actions { white-space: nowrap; } @media (max-width: 760px) { .grid { grid-template-columns: 1fr; } + .trial-summary-grid { grid-template-columns: 1fr 1fr; } + .trial-expired-shell { padding: 20px 16px 28px; } + .trial-expired-card { padding: 18px; } + .trial-expired-grid { grid-template-columns: 1fr; } .branding-preview-header { flex-direction: column; align-items: flex-start; } .branding-preview-band { flex-wrap: wrap; } .app-registry-filters { grid-template-columns: 1fr; } diff --git a/backend/workflows/static/workflows/css/app_chrome.css b/backend/workflows/static/workflows/css/app_chrome.css index 6179816..954f249 100644 --- a/backend/workflows/static/workflows/css/app_chrome.css +++ b/backend/workflows/static/workflows/css/app_chrome.css @@ -29,6 +29,110 @@ background-color var(--motion-base) var(--motion-ease); } +.app-trial-banner { + width: min(var(--app-shell-width), 100%); + margin: 0 auto 12px; + padding: 0 10px; +} + +.app-trial-banner-inner { + display: flex; + gap: 14px; + align-items: center; + flex-wrap: wrap; + padding: 10px 12px; + border: 1px solid #e9d4a2; + border-radius: 18px; + background: + radial-gradient(circle at top right, rgba(255, 206, 112, 0.22), transparent 28%), + linear-gradient(180deg, rgba(255,251,243,0.98), rgba(255,244,222,0.94)); + color: #875400; + box-shadow: + inset 0 1px 0 rgba(255,255,255,0.88), + 0 10px 20px rgba(16, 32, 57, 0.05); +} + +.app-trial-banner.is-expired .app-trial-banner-inner { + border-color: #efc2c2; + background: + radial-gradient(circle at top right, rgba(222, 92, 92, 0.16), transparent 28%), + linear-gradient(180deg, rgba(255,248,248,0.98), rgba(255,238,238,0.94)); + color: #9f1d1d; +} + +.app-trial-banner-chip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 7px 11px; + border-radius: 999px; + border: 1px solid rgba(135, 84, 0, 0.16); + background: rgba(255,255,255,0.72); + color: inherit; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.05em; + text-transform: uppercase; + white-space: nowrap; +} + +.app-trial-banner.is-expired .app-trial-banner-chip { + border-color: rgba(159, 29, 29, 0.18); +} + +.app-trial-banner-copy { + display: grid; + gap: 2px; + flex: 1 1 440px; + min-width: 240px; +} + +.app-trial-banner-title { + color: #4b3710; + font-size: 13px; + line-height: 1.2; +} + +.app-trial-banner.is-expired .app-trial-banner-title { + color: #7f1d1d; +} + +.app-trial-banner-text { + color: #7f6540; + font-size: 12px; + line-height: 1.45; +} + +.app-trial-banner.is-expired .app-trial-banner-text { + color: #8f3a3a; +} + +.app-trial-banner-meta { + display: grid; + gap: 2px; + padding: 6px 10px; + border-left: 1px solid rgba(135, 84, 0, 0.14); + min-width: 120px; +} + +.app-trial-banner.is-expired .app-trial-banner-meta { + border-left-color: rgba(159, 29, 29, 0.16); +} + +.app-trial-banner-meta-label { + color: #8a6c42; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.app-trial-banner-meta strong { + color: inherit; + font-size: 12px; + line-height: 1.3; +} + .app-header-in-shell { box-sizing: border-box; width: 100%; diff --git a/backend/workflows/templates/workflows/base_shell.html b/backend/workflows/templates/workflows/base_shell.html index 411fca4..197c0ba 100644 --- a/backend/workflows/templates/workflows/base_shell.html +++ b/backend/workflows/templates/workflows/base_shell.html @@ -14,6 +14,43 @@ {% block pre_shell %}{% endblock %} + {% if portal_trial_enabled %} +
+
+
+ {% if portal_trial_expired %}{% trans "Trial abgelaufen" %}{% else %}{% trans "Trial-Modus" %}{% endif %} +
+
+ + {% if portal_trial_expired %} + {% trans "Zugriff für Testnutzer gesperrt" %} + {% else %} + {% trans "Kontrollierte Testumgebung aktiv" %} + {% endif %} + + + {% if portal_trial_banner_text %} + {{ portal_trial_banner_text }} + {% elif portal_trial_expires_at %} + {% if portal_trial_expired %} + {% blocktrans with expires=portal_trial_expires_at|date:"d.m.Y H:i" %}Diese Testumgebung ist seit {{ expires }} abgelaufen.{% endblocktrans %} + {% else %} + {% blocktrans with expires=portal_trial_expires_at|date:"d.m.Y H:i" %}Diese Testumgebung ist bis {{ expires }} aktiv.{% endblocktrans %} + {% endif %} + {% else %} + {% trans "Diese Umgebung läuft im Trial-Modus." %} + {% endif %} + +
+ {% if portal_trial_expires_at %} +
+ {% trans "Ende" %} + {{ portal_trial_expires_at|date:"d.m.Y H:i" }} +
+ {% endif %} +
+
+ {% endif %}
{% block shell_header %}{% endblock %} {% block shell_body %}{% endblock %} diff --git a/backend/workflows/templates/workflows/developer_handbook.html b/backend/workflows/templates/workflows/developer_handbook.html index f599a52..9574168 100644 --- a/backend/workflows/templates/workflows/developer_handbook.html +++ b/backend/workflows/templates/workflows/developer_handbook.html @@ -192,6 +192,18 @@ docker compose exec -T web django-admin compilemessages
  • Management UI: /admin-tools/apps/ for Platform Owner.
  • +

    10c) Trial Lifecycle

    +
      +
    • Deployment-level trial settings are stored in the singleton model PortalTrialConfig.
    • +
    • Management UI: /admin-tools/trial/ for Platform Owner.
    • +
    • Current scope: trial enable/disable, start/end timestamps, bilingual shell banner text, production-integration restriction, and cleanup readiness.
    • +
    • Enforcement lives in workflows.middleware.TrialModeMiddleware, so expiry is handled centrally instead of per-view.
    • +
    • While trial restriction is active, service-level integration checks force Nextcloud off and E-Mail into test mode.
    • +
    • Expired-trial cleanup is intentionally CLI-only: +
      docker compose exec -T web python manage.py cleanup_expired_trial_workspace --yes-delete
      +
    • +
    +

    11) Builder Architecture

    Form Builder

      diff --git a/backend/workflows/templates/workflows/project_wiki.html b/backend/workflows/templates/workflows/project_wiki.html index 8b6bdcf..52fd3b7 100644 --- a/backend/workflows/templates/workflows/project_wiki.html +++ b/backend/workflows/templates/workflows/project_wiki.html @@ -179,7 +179,8 @@
    • 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, company domain, support email, sender display name, logo, favicon, default language, PDF letterhead, footer/legal text, and basic brand colors.
    • -
    • App Registry: platform-level registry for enabling, ordering, and relabeling landing-page apps without editing the home template.
    • +
    • App Registry: platform-level registry for enabling, ordering, relabeling, and role-targeting landing-page apps without editing the home template.
    • +
    • Trial Management: platform-only control surface for trial runtime, expiry, shell banner, and safe demo restrictions.
    • 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.
    • @@ -210,6 +211,10 @@
    • 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.
    • +
    • Trial deployments can force safe integration behavior: Nextcloud is treated as disabled and email remains in test mode while the trial restriction is active.
    • +
    • Expired trials should be cleaned with the dedicated command, not from the browser: +
      docker compose exec -T web python manage.py cleanup_expired_trial_workspace --yes-delete
      +

    Deployment Notes

    diff --git a/backend/workflows/templates/workflows/trial_expired.html b/backend/workflows/templates/workflows/trial_expired.html new file mode 100644 index 0000000..7dbcf9e --- /dev/null +++ b/backend/workflows/templates/workflows/trial_expired.html @@ -0,0 +1,46 @@ +{% extends 'workflows/base_shell.html' %} +{% load static i18n %} + +{% block title %}{% trans "Trial abgelaufen" %}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block shell_body %} +{% include 'workflows/includes/app_header.html' with header_show_home=0 header_show_lang=1 header_inside_shell=1 %} +
    +
    +
    {% trans "Trial expired" %}
    +

    {% trans "Trial abgelaufen" %}

    +

    {% trans "Diese Testumgebung ist nicht mehr aktiv. Bitte wenden Sie sich für eine Verlängerung oder ein Produktiv-Setup an den Plattformbetreiber." %}

    + +
    +
    + {% trans "Status" %} + {% trans "Zugriff gesperrt" %} +

    {% trans "Nicht-Platform-Nutzer können diese Umgebung nach Ablauf nicht mehr verwenden." %}

    +
    +
    + {% trans "Nächster Schritt" %} + {% trans "Verlängern oder Produktiv-Setup" %} +

    {% trans "Ein Platform Owner kann den Trial verlängern oder das Setup in einen regulären Betrieb überführen." %}

    +
    + {% if portal_trial_expires_at %} +
    + {% trans "Ablaufzeit" %} + {{ portal_trial_expires_at|date:"d.m.Y H:i" }} +

    {% trans "Das ist der im System hinterlegte Endzeitpunkt der Testumgebung." %}

    +
    + {% endif %} +
    + + + {% if portal_support_email %} +
    {% blocktrans with email=portal_support_email %}Kontakt: {{ email }}{% endblocktrans %}
    + {% endif %} +
    +
    +{% endblock %} diff --git a/backend/workflows/templates/workflows/trial_management.html b/backend/workflows/templates/workflows/trial_management.html new file mode 100644 index 0000000..fe1c890 --- /dev/null +++ b/backend/workflows/templates/workflows/trial_management.html @@ -0,0 +1,127 @@ +{% extends 'workflows/base_shell.html' %} +{% load static i18n %} + +{% block title %}{% trans "Trial Management" %}{% 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 "Trial Management" %}

    +

    {% trans "Testlaufzeit, Banner und sichere Einschränkungen für Demo- und Pilotumgebungen steuern." %}

    + +{% include 'workflows/includes/messages.html' %} + +
    +
    +
    +

    {% trans "Übersicht" %}

    +

    {% trans "Aktueller Trial-Status und die daraus resultierende Systemwirkung." %}

    +
    +
    +
    + {% trans "Status" %} + + {% if portal_trial_enabled %} + {% if portal_trial_expired %}{% trans "Abgelaufen" %}{% else %}{% trans "Aktiv" %}{% endif %} + {% else %} + {% trans "Deaktiviert" %} + {% endif %} + +
    +
    + {% trans "Ende" %} + + {% if portal_trial_expires_at %}{{ portal_trial_expires_at|date:"d.m.Y H:i" }}{% else %}{% trans "Nicht gesetzt" %}{% endif %} + +
    +
    + {% trans "Nextcloud effektiv" %} + + {% if portal_trial_restrict_integrations and portal_trial_enabled %}{% trans "Deaktiviert" %}{% else %}{% trans "Unverändert" %}{% endif %} + +
    +
    + {% trans "E-Mail effektiv" %} + + {% if portal_trial_restrict_integrations and portal_trial_enabled %}{% trans "Testmodus" %}{% else %}{% trans "Unverändert" %}{% endif %} + +
    +
    +
    + {% trans "Zum Deaktivieren des Trial-Modus entfernen Sie den Haken bei „Trial-Modus aktiv“ und speichern Sie die Seite." %} +
    +
    + +
    + {% csrf_token %} + +
    +
    +

    {% trans "Trial-Status" %}

    +

    {% trans "Aktivieren Sie den Trial-Modus und definieren Sie die gültige Laufzeit." %}

    +
    +
    +
    + +
    + +
    +
    {% trans "Sobald dieser Schalter deaktiviert ist, verschwindet das Trial-Banner und die normalen Integrationsregeln greifen wieder." %}
    +
    +
    + + {{ form.trial_started_at }} +
    +
    + + {{ form.trial_expires_at }} + {% if trial_is_expired %}
    {% trans "Der konfigurierte Trial ist derzeit abgelaufen." %}
    {% endif %} +
    +
    +
    + +
    +
    +

    {% trans "Sicherheitsregeln" %}

    +

    {% trans "Testumgebungen sollen keine produktiven Integrationen verwenden." %}

    +
    +
    + + +
    +
    {% trans "Wenn diese Regel aktiv ist, bleiben produktive Integrationen technisch gesperrt, auch wenn lokale Overrides anders gesetzt sind." %}
    +
    + +
    +
    +

    {% trans "Banner" %}

    +

    {% trans "Optionaler Hinweistext für die Shell. Ohne Text wird ein Standardhinweis mit Enddatum verwendet." %}

    +
    +
    +
    +

    {% trans "Deutsch" %}

    +
    + + {{ form.trial_banner_text }} +
    +
    +
    +

    {% trans "English" %}

    +
    + + {{ form.trial_banner_text_en }} +
    +
    +
    +
    + +
    +
    {% trans "Die eigentliche Datenbereinigung läuft bewusst nicht über die Web-UI. Nutzen Sie dafür den Cleanup-Command im Betrieb." %}
    + +
    +
    +
    +{% endblock %} diff --git a/backend/workflows/trial.py b/backend/workflows/trial.py new file mode 100644 index 0000000..6b2e4df --- /dev/null +++ b/backend/workflows/trial.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import shutil +from pathlib import Path + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.sessions.models import Session +from django.utils import timezone + +from .branding import get_portal_trial_config, is_trial_expired +from .models import AdminAuditLog, EmployeeProfile, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail +from .roles import ROLE_PLATFORM_OWNER, get_user_role_key + + +def cleanup_trial_workspace_data() -> dict[str, int]: + user_model = get_user_model() + deleted_counts = { + 'onboarding_requests': OnboardingRequest.objects.count(), + 'offboarding_requests': OffboardingRequest.objects.count(), + 'intro_sessions': OnboardingIntroductionSession.objects.count(), + 'scheduled_welcome_emails': ScheduledWelcomeEmail.objects.count(), + 'employee_profiles': EmployeeProfile.objects.count(), + 'audit_logs': AdminAuditLog.objects.count(), + 'sessions': Session.objects.count(), + 'users_removed': 0, + 'media_paths_removed': 0, + } + + OnboardingIntroductionSession.objects.all().delete() + ScheduledWelcomeEmail.objects.all().delete() + OnboardingRequest.objects.all().delete() + OffboardingRequest.objects.all().delete() + EmployeeProfile.objects.all().delete() + AdminAuditLog.objects.all().delete() + Session.objects.all().delete() + + for user in user_model.objects.all(): + if get_user_role_key(user) == ROLE_PLATFORM_OWNER: + continue + user.delete() + deleted_counts['users_removed'] += 1 + + for path in (settings.MEDIA_ROOT / 'pdfs', settings.MEDIA_ROOT / 'signatures'): + candidate = Path(path) + if candidate.exists(): + shutil.rmtree(candidate, ignore_errors=True) + candidate.mkdir(parents=True, exist_ok=True) + deleted_counts['media_paths_removed'] += 1 + + trial_config = get_portal_trial_config() + trial_config.last_cleanup_at = timezone.now() + trial_config.save(update_fields=['last_cleanup_at', 'updated_at']) + return deleted_counts + + +def trial_cleanup_is_due() -> bool: + trial_config = get_portal_trial_config() + return bool(trial_config.is_trial_mode and trial_config.auto_cleanup_enabled and is_trial_expired()) diff --git a/backend/workflows/urls.py b/backend/workflows/urls.py index 59836fa..32e9e3b 100644 --- a/backend/workflows/urls.py +++ b/backend/workflows/urls.py @@ -34,6 +34,8 @@ urlpatterns = [ path('admin-tools/branding/save/', views.save_portal_branding, name='save_portal_branding'), path('admin-tools/company/', views.portal_company_config_page, name='portal_company_config_page'), path('admin-tools/company/save/', views.save_portal_company_config, name='save_portal_company_config'), + path('admin-tools/trial/', views.portal_trial_config_page, name='portal_trial_config_page'), + path('admin-tools/trial/save/', views.save_portal_trial_config, name='save_portal_trial_config'), path('admin-tools/apps/', views.portal_app_registry_page, name='portal_app_registry_page'), path('admin-tools/apps/save/', views.save_portal_app_registry, name='save_portal_app_registry'), path('admin-tools/users/', views.user_management_page, name='user_management_page'), diff --git a/backend/workflows/views.py b/backend/workflows/views.py index ce09ea6..ba22be9 100644 --- a/backend/workflows/views.py +++ b/backend/workflows/views.py @@ -26,8 +26,8 @@ from django.urls import reverse from .app_registry import build_portal_app_sections, get_portal_app_registry_rows from .backup_ops import create_backup_bundle, delete_backup_bundle, list_backup_bundles, verify_backup_bundle -from .branding import get_branding_email_copy, get_company_email_domain, get_default_notification_templates -from .forms import OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, UserManagementCreateForm +from .branding import get_branding_email_copy, get_company_email_domain, get_default_notification_templates, get_portal_trial_config, is_trial_expired +from .forms import OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm from .form_builder import ( DEFAULT_FIELD_ORDER, LOCKED_FIELD_RULES, @@ -36,7 +36,7 @@ from .form_builder import ( ONBOARDING_PAGE_ORDER, ensure_form_field_configs, ) -from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig +from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, PortalAppConfig, PortalBranding, PortalCompanyConfig, PortalTrialConfig, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig from .emailing import send_system_email 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 @@ -645,6 +645,66 @@ def save_portal_company_config(request): ) +@_require_capability('manage_trial_lifecycle') +def portal_trial_config_page(request): + trial_config = get_portal_trial_config() + form = PortalTrialConfigForm(instance=trial_config) + return render( + request, + 'workflows/trial_management.html', + { + 'form': form, + 'trial_config': trial_config, + 'trial_is_expired': is_trial_expired(), + }, + ) + + +@_require_capability('manage_trial_lifecycle') +@require_POST +def save_portal_trial_config(request): + trial_config = get_portal_trial_config() + form = PortalTrialConfigForm(request.POST, instance=trial_config) + if not form.is_valid(): + messages.error(request, _('Trial-Konfiguration konnte nicht gespeichert werden. Bitte prüfen Sie die Eingaben.')) + return render( + request, + 'workflows/trial_management.html', + { + 'form': form, + 'trial_config': trial_config, + 'trial_is_expired': is_trial_expired(), + }, + status=400, + ) + + trial_config = form.save() + _audit( + request, + 'portal_trial_config_saved', + target_type='portal_trial_config', + target_id=trial_config.id, + target_label='Default', + details={ + 'is_trial_mode': trial_config.is_trial_mode, + 'trial_started_at': trial_config.trial_started_at.isoformat() if trial_config.trial_started_at else '', + 'trial_expires_at': trial_config.trial_expires_at.isoformat() if trial_config.trial_expires_at else '', + 'restrict_production_integrations': trial_config.restrict_production_integrations, + 'auto_cleanup_enabled': trial_config.auto_cleanup_enabled, + }, + ) + messages.success(request, _('Trial-Konfiguration wurde gespeichert.')) + return render( + request, + 'workflows/trial_management.html', + { + 'form': PortalTrialConfigForm(instance=trial_config), + 'trial_config': trial_config, + 'trial_is_expired': is_trial_expired(), + }, + ) + + @_require_capability('manage_users') @require_POST def create_user_from_admin(request):