snapshot: preserve branding foundation and platform owner split

This commit is contained in:
Md Bayazid Bostame
2026-03-26 11:43:54 +01:00
parent 8926d6860c
commit 51700cfa8b
22 changed files with 966 additions and 242 deletions

View File

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