snapshot: extract remaining workflow view helper clusters
This commit is contained in:
@@ -34,6 +34,16 @@ from .backup_ops import (
|
||||
)
|
||||
from .branding import get_branding_email_copy, get_company_email_domain, get_default_notification_templates, get_portal_trial_config, is_trial_expired
|
||||
from . import account_views, admin_config_views, integrations_views, request_views
|
||||
from .admin_section_builders import (
|
||||
build_branding_sections as _build_branding_sections,
|
||||
build_company_config_sections as _build_company_config_sections,
|
||||
)
|
||||
from .admin_user_helpers import (
|
||||
render_user_management as _render_user_management,
|
||||
send_user_access_email as _send_user_access_email,
|
||||
would_remove_last_platform_owner as _would_remove_last_platform_owner,
|
||||
would_remove_last_super_admin as _would_remove_last_super_admin,
|
||||
)
|
||||
from .forms import AccountAvatarForm, AccountDetailsForm, AccountNotificationPreferencesForm, AccountTOTPDisableForm, AccountTOTPEnableForm, AccountTOTPRegenerateRecoveryCodesForm, AppLoginForm, AppTOTPChallengeForm, OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm
|
||||
from .form_builder import (
|
||||
DEFAULT_FIELD_ORDER,
|
||||
@@ -196,141 +206,18 @@ def save_portal_app_registry(request):
|
||||
return admin_config_views.save_portal_app_registry_impl(request, audit_fn=_audit)
|
||||
|
||||
|
||||
def _user_management_rows():
|
||||
user_model = get_user_model()
|
||||
role_order = {
|
||||
ROLE_PLATFORM_OWNER: 0,
|
||||
ROLE_SUPER_ADMIN: 0,
|
||||
'admin': 1,
|
||||
'it_staff': 2,
|
||||
'staff': 3,
|
||||
}
|
||||
rows = []
|
||||
for user in user_model.objects.all().order_by('-is_active', 'username'):
|
||||
role_key = get_user_role_key(user)
|
||||
rows.append(
|
||||
{
|
||||
'user': user,
|
||||
'role_key': role_key,
|
||||
'role_label': str(ROLE_LABELS[role_key]),
|
||||
'role_sort': role_order.get(role_key, 99),
|
||||
'display_name': _display_user_name(user),
|
||||
}
|
||||
)
|
||||
rows.sort(key=lambda item: (not item['user'].is_active, item['role_sort'], item['user'].username.lower()))
|
||||
return rows
|
||||
|
||||
|
||||
def _render_user_management(request, create_form=None, status_code: int = 200):
|
||||
recent_user_events = list(
|
||||
AdminAuditLog.objects.select_related('actor')
|
||||
.filter(action__in=['user_created', 'user_updated', 'user_password_reset_sent', 'user_deleted'])
|
||||
.order_by('-created_at', '-id')[:12]
|
||||
)
|
||||
for row in recent_user_events:
|
||||
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(include_product_owner=include_product_owner),
|
||||
'rows': _user_management_rows(),
|
||||
'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)
|
||||
|
||||
|
||||
def _would_remove_last_super_admin(user, new_role_key: str | None = None, new_is_active: bool | None = None, deleting: bool = False) -> bool:
|
||||
if get_user_role_key(user) != ROLE_SUPER_ADMIN or not user.is_active:
|
||||
return False
|
||||
if _super_admin_user_count() > 1:
|
||||
return False
|
||||
if deleting:
|
||||
return True
|
||||
if new_role_key is not None and new_role_key != ROLE_SUPER_ADMIN:
|
||||
return True
|
||||
if new_is_active is not None and not new_is_active:
|
||||
return True
|
||||
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:
|
||||
raise ValueError(_('Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt.'))
|
||||
|
||||
uid = urlsafe_base64_encode(force_bytes(target_user.pk))
|
||||
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 %(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:
|
||||
subject = _('Passwort zurücksetzen für %(username)s') % {'username': target_user.username}
|
||||
body = _(
|
||||
'Hallo %(name)s,\n\n'
|
||||
'für Ihr Konto wurde ein Link zum Zurücksetzen des Passworts erstellt.\n'
|
||||
'Bitte öffnen Sie den folgenden Link:\n'
|
||||
'%(url)s\n\n'
|
||||
'Wenn Sie diese Anfrage nicht erwartet haben, können Sie diese E-Mail ignorieren.'
|
||||
) % {
|
||||
'name': _display_user_name(target_user),
|
||||
'url': reset_url,
|
||||
}
|
||||
|
||||
send_system_email(subject=subject, body=body, to=[email])
|
||||
|
||||
|
||||
@_require_capability('manage_users')
|
||||
def user_management_page(request):
|
||||
return admin_config_views.user_management_page_impl(request, render_user_management_fn=_render_user_management)
|
||||
return admin_config_views.user_management_page_impl(
|
||||
request,
|
||||
render_user_management_fn=lambda req, create_form=None, status_code=200: _render_user_management(
|
||||
req,
|
||||
create_form=create_form,
|
||||
status_code=status_code,
|
||||
audit_action_label_fn=_audit_action_label,
|
||||
display_user_name_fn=_display_user_name,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@_require_capability('manage_product_branding')
|
||||
@@ -344,70 +231,6 @@ def save_portal_branding(request):
|
||||
return admin_config_views.save_portal_branding_impl(request, audit_fn=_audit, build_branding_sections_fn=_build_branding_sections)
|
||||
|
||||
|
||||
def _build_branding_sections(form, branding):
|
||||
sections = [
|
||||
{
|
||||
'key': 'identity',
|
||||
'title': _('Identität'),
|
||||
'subtitle': _('Titel, Firmenname und zentrale Spracheinstellungen.'),
|
||||
'fields': ['portal_title', 'company_name', 'company_domain', 'default_language', 'login_subtitle'],
|
||||
'field_full': {'login_subtitle'},
|
||||
'hint_map': {
|
||||
'company_domain': _('Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. B. workdock.de.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
'key': 'appearance',
|
||||
'title': _('Farben & Erscheinungsbild'),
|
||||
'subtitle': _('Zentrale visuelle Markenwerte und Browser-Icon.'),
|
||||
'fields': ['primary_color', 'secondary_color', 'logo_image', 'favicon_image'],
|
||||
'field_full': set(),
|
||||
'hint_map': {
|
||||
'logo_image': _('Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB.'),
|
||||
'favicon_image': _('Erlaubte Formate: ICO, PNG, SVG, WEBP. Maximal 2 MB.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
'key': 'communication',
|
||||
'title': _('Kommunikation'),
|
||||
'subtitle': _('Absender, Support und PDF-Branding für ausgehende Kommunikation.'),
|
||||
'fields': ['support_email', 'sender_display_name', 'pdf_letterhead'],
|
||||
'field_full': {'pdf_letterhead'},
|
||||
'hint_map': {
|
||||
'sender_display_name': _('Wird für ausgehende System-E-Mails als Anzeigename verwendet.'),
|
||||
'pdf_letterhead': _('Erlaubtes Format: PDF. Maximal 10 MB.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
'key': 'legal',
|
||||
'title': _('Footer & Rechtliches'),
|
||||
'subtitle': _('Gemeinsame Footer-Texte und rechtliche Hinweise für die Shell.'),
|
||||
'fields': ['footer_text', 'legal_notice', 'footer_text_en', 'legal_notice_en'],
|
||||
'field_full': {'legal_notice', 'legal_notice_en'},
|
||||
'hint_map': {},
|
||||
},
|
||||
]
|
||||
for section in sections:
|
||||
rows = []
|
||||
for field_name in section['fields']:
|
||||
field = form[field_name]
|
||||
value = getattr(branding, field_name, '') or ''
|
||||
is_file = bool(getattr(field.field.widget, 'input_type', '') == 'file')
|
||||
rows.append(
|
||||
{
|
||||
'name': field_name,
|
||||
'bound_field': field,
|
||||
'label': field.label,
|
||||
'value': value,
|
||||
'is_file': is_file,
|
||||
'is_full': field_name in section.get('field_full', set()),
|
||||
'hint': section.get('hint_map', {}).get(field_name, ''),
|
||||
}
|
||||
)
|
||||
section['rows'] = rows
|
||||
return sections
|
||||
|
||||
|
||||
@_require_capability('manage_company_config')
|
||||
def portal_company_config_page(request):
|
||||
return admin_config_views.portal_company_config_page_impl(request, build_company_config_sections_fn=_build_company_config_sections)
|
||||
@@ -419,50 +242,6 @@ def save_portal_company_config(request):
|
||||
return admin_config_views.save_portal_company_config_impl(request, audit_fn=_audit, build_company_config_sections_fn=_build_company_config_sections)
|
||||
|
||||
|
||||
def _build_company_config_sections(form, company_config):
|
||||
sections = [
|
||||
{
|
||||
'key': 'profile',
|
||||
'title': _('Firmenprofil'),
|
||||
'subtitle': _('Rechtlicher Name und zentrale Stammdaten der Firma.'),
|
||||
'fields': ['legal_company_name', 'phone_number', 'website_url', 'country'],
|
||||
},
|
||||
{
|
||||
'key': 'address',
|
||||
'title': _('Adresse & Register'),
|
||||
'subtitle': _('Anschrift sowie optionale Register- und Steuerangaben.'),
|
||||
'fields': ['street_address', 'postal_code', 'city', 'registration_number', 'vat_id'],
|
||||
},
|
||||
{
|
||||
'key': 'contacts',
|
||||
'title': _('Kontaktpunkte'),
|
||||
'subtitle': _('Zentrale Ansprechpartner für HR, IT und Operations.'),
|
||||
'fields': ['hr_contact_email', 'it_contact_email', 'operations_contact_email'],
|
||||
},
|
||||
{
|
||||
'key': 'public',
|
||||
'title': _('Recht & Öffentlichkeit'),
|
||||
'subtitle': _('Öffentliche Links für Website, Impressum und Datenschutz.'),
|
||||
'fields': ['imprint_url', 'privacy_url'],
|
||||
'hint': _('Diese Links können später im Portal-Footer oder in öffentlichen Seiten verwendet werden.'),
|
||||
},
|
||||
]
|
||||
for section in sections:
|
||||
rows = []
|
||||
for field_name in section['fields']:
|
||||
field = form[field_name]
|
||||
rows.append(
|
||||
{
|
||||
'name': field_name,
|
||||
'bound_field': field,
|
||||
'label': field.label,
|
||||
'value': getattr(company_config, field_name, '') or '',
|
||||
}
|
||||
)
|
||||
section['rows'] = rows
|
||||
return sections
|
||||
|
||||
|
||||
@_require_capability('manage_trial_lifecycle')
|
||||
def portal_trial_config_page(request):
|
||||
return admin_config_views.portal_trial_config_page_impl(request)
|
||||
@@ -479,8 +258,19 @@ def save_portal_trial_config(request):
|
||||
def create_user_from_admin(request):
|
||||
return admin_config_views.create_user_from_admin_impl(
|
||||
request,
|
||||
render_user_management_fn=_render_user_management,
|
||||
send_user_access_email_fn=_send_user_access_email,
|
||||
render_user_management_fn=lambda req, create_form=None, status_code=200: _render_user_management(
|
||||
req,
|
||||
create_form=create_form,
|
||||
status_code=status_code,
|
||||
audit_action_label_fn=_audit_action_label,
|
||||
display_user_name_fn=_display_user_name,
|
||||
),
|
||||
send_user_access_email_fn=lambda req, target_user, invitation: _send_user_access_email(
|
||||
req,
|
||||
target_user,
|
||||
invitation=invitation,
|
||||
display_user_name_fn=_display_user_name,
|
||||
),
|
||||
audit_fn=_audit,
|
||||
display_user_name_fn=_display_user_name,
|
||||
)
|
||||
@@ -505,7 +295,12 @@ def send_password_reset_from_admin(request, user_id: int):
|
||||
return admin_config_views.send_password_reset_from_admin_impl(
|
||||
request,
|
||||
user_id,
|
||||
send_user_access_email_fn=_send_user_access_email,
|
||||
send_user_access_email_fn=lambda req, target_user, invitation: _send_user_access_email(
|
||||
req,
|
||||
target_user,
|
||||
invitation=invitation,
|
||||
display_user_name_fn=_display_user_name,
|
||||
),
|
||||
audit_fn=_audit,
|
||||
display_user_name_fn=_display_user_name,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user