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 1f6f36c..699e7d3 100644 Binary files a/backend/locale/en/LC_MESSAGES/django.mo and b/backend/locale/en/LC_MESSAGES/django.mo differ 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 %} + + {% endif %}/admin-tools/apps/ for Platform Owner.PortalTrialConfig./admin-tools/trial/ for Platform Owner.workflows.middleware.TrialModeMiddleware, so expiry is handled centrally instead of per-view.docker compose exec -T web python manage.py cleanup_expired_trial_workspace --yes-delete
+ Branding.docker compose exec -T web python manage.py cleanup_expired_trial_workspace --yes-delete
+ {% trans "Diese Testumgebung ist nicht mehr aktiv. Bitte wenden Sie sich für eine Verlängerung oder ein Produktiv-Setup an den Plattformbetreiber." %}
+ +{% trans "Nicht-Platform-Nutzer können diese Umgebung nach Ablauf nicht mehr verwenden." %}
+{% trans "Ein Platform Owner kann den Trial verlängern oder das Setup in einen regulären Betrieb überführen." %}
+{% trans "Das ist der im System hinterlegte Endzeitpunkt der Testumgebung." %}
+{% trans "Testlaufzeit, Banner und sichere Einschränkungen für Demo- und Pilotumgebungen steuern." %}
+ +{% include 'workflows/includes/messages.html' %} + +{% trans "Aktueller Trial-Status und die daraus resultierende Systemwirkung." %}
+