Files
workdock-platform/backend/workflows/branding.py
2026-03-27 01:11:29 +01:00

294 lines
12 KiB
Python

from __future__ import annotations
from pathlib import Path
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, PortalTrialConfig
# Branding is the product/deployment boundary.
# Workdock is the generic default, while stored DB values preserve the current
# customer deployment identity such as TUBCO.
def get_portal_branding() -> PortalBranding:
branding, _ = PortalBranding.objects.get_or_create(
name='Default',
defaults={
'portal_title': 'Workdock',
'company_name': 'Workdock',
'company_domain': 'workdock.de',
'support_email': 'info@workdock.de',
'sender_display_name': 'Workdock',
'login_subtitle': 'Bitte melden Sie sich mit Ihrem Benutzerkonto an.',
'footer_text': 'Workdock',
'footer_text_en': 'Workdock',
'legal_notice': '',
'legal_notice_en': '',
'default_language': 'de',
'primary_color': '#000078',
'secondary_color': '#c0002b',
},
)
return branding
def get_portal_company_config() -> PortalCompanyConfig:
company_config, _ = PortalCompanyConfig.objects.get_or_create(
name='Default',
defaults={
'legal_company_name': 'Workdock',
'country': 'Deutschland',
'website_url': '',
'imprint_url': '',
'privacy_url': '',
'hr_contact_email': '',
'it_contact_email': '',
'operations_contact_email': '',
'phone_number': '',
'vat_id': '',
'registration_number': '',
},
)
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('@')
return domain or 'workdock.de'
def get_portal_logo_url() -> str:
branding = get_portal_branding()
if branding.logo_image:
try:
return branding.logo_image.url
except ValueError:
pass
# The fallback asset file is still the historical TUBCO wordmark. A later
# asset refresh can replace the file without changing the branding contract.
return static('workflows/img/tubco-logo.svg')
def get_portal_favicon_url() -> str:
branding = get_portal_branding()
if branding.favicon_image:
try:
return branding.favicon_image.url
except ValueError:
pass
# Same fallback rule as the logo: keep runtime stable now, replace asset later.
return static('workflows/img/tubco-logo.svg')
def get_portal_letterhead_path() -> Path:
branding = get_portal_branding()
if branding.pdf_letterhead:
try:
candidate = Path(branding.pdf_letterhead.path)
if candidate.exists():
return candidate
except (ValueError, NotImplementedError):
pass
return settings.PDF_TEMPLATES_DIR / 'templates.pdf'
def get_branding_context() -> dict[str, object]:
branding = get_portal_branding()
company_config = get_portal_company_config()
lang = (get_language() or branding.default_language or 'de').split('-')[0]
footer_text = (branding.footer_text_en or '').strip() if lang == 'en' else ''
legal_notice = (branding.legal_notice_en or '').strip() if lang == 'en' else ''
if not footer_text:
footer_text = (branding.footer_text or branding.portal_title).strip()
if not legal_notice:
legal_notice = (branding.legal_notice or '').strip()
return {
'portal_branding': branding,
'portal_title': branding.portal_title,
'portal_company_name': branding.company_name,
'portal_email_domain': get_company_email_domain(),
'portal_support_email': branding.support_email,
'portal_sender_display_name': branding.sender_display_name or branding.company_name,
'portal_login_subtitle': branding.login_subtitle,
'portal_footer_text': footer_text,
'portal_legal_notice': legal_notice,
'portal_default_language': branding.default_language,
'portal_primary_color': branding.primary_color,
'portal_secondary_color': branding.secondary_color,
'portal_logo_url': get_portal_logo_url(),
'portal_favicon_url': get_portal_favicon_url(),
'portal_has_custom_logo': bool(branding.logo_image),
'portal_has_custom_letterhead': bool(branding.pdf_letterhead),
'portal_has_custom_favicon': bool(branding.favicon_image),
'portal_company_config': company_config,
'portal_company_legal_name': company_config.legal_company_name or branding.company_name,
'portal_company_street': company_config.street_address,
'portal_company_postal_code': company_config.postal_code,
'portal_company_city': company_config.city,
'portal_company_country': company_config.country,
'portal_company_website_url': company_config.website_url,
'portal_company_imprint_url': company_config.imprint_url,
'portal_company_privacy_url': company_config.privacy_url,
'portal_company_hr_contact_email': company_config.hr_contact_email,
'portal_company_it_contact_email': company_config.it_contact_email,
'portal_company_operations_contact_email': company_config.operations_contact_email,
'portal_company_phone_number': company_config.phone_number,
'portal_company_vat_id': company_config.vat_id,
'portal_company_registration_number': company_config.registration_number,
}
def get_branding_email_copy() -> dict[str, str]:
branding = get_portal_branding()
company_name = (branding.company_name or 'Workdock').strip()
portal_title = (branding.portal_title or f'{company_name} Portal').strip()
return {
'company_name': company_name,
'company_domain': get_company_email_domain(),
'portal_title': portal_title,
'support_email': (branding.support_email or '').strip(),
'sender_display_name': (branding.sender_display_name or company_name).strip(),
}
def get_company_contact_copy() -> dict[str, str]:
branding = get_portal_branding()
company_config = get_portal_company_config()
company_name = (branding.company_name or 'Workdock').strip()
legal_name = (company_config.legal_company_name or company_name).strip()
domain = get_company_email_domain()
support_email = (branding.support_email or '').strip()
it_contact_email = (company_config.it_contact_email or support_email or f'it@{domain}').strip()
hr_contact_email = (company_config.hr_contact_email or support_email or f'hr@{domain}').strip()
operations_contact_email = (company_config.operations_contact_email or support_email or f'info@{domain}').strip()
address_parts = [
(company_config.street_address or '').strip(),
' '.join(part for part in [(company_config.postal_code or '').strip(), (company_config.city or '').strip()] if part).strip(),
(company_config.country or '').strip(),
]
address = ', '.join(part for part in address_parts if part)
return {
'company_name': company_name,
'legal_company_name': legal_name,
'support_email': support_email,
'it_contact_email': it_contact_email,
'hr_contact_email': hr_contact_email,
'operations_contact_email': operations_contact_email,
'phone_number': (company_config.phone_number or '').strip(),
'website_url': (company_config.website_url or '').strip(),
'imprint_url': (company_config.imprint_url or '').strip(),
'privacy_url': (company_config.privacy_url or '').strip(),
'address': address,
'street_address': (company_config.street_address or '').strip(),
'postal_code': (company_config.postal_code or '').strip(),
'city': (company_config.city or '').strip(),
'country': (company_config.country or '').strip(),
'registration_number': (company_config.registration_number or '').strip(),
'vat_id': (company_config.vat_id or '').strip(),
}
def get_branded_from_email(email_address: str | None) -> str | None:
address = (email_address or '').strip()
if not address:
return None
display_name = (get_branding_email_copy()['sender_display_name'] or '').strip()
if not display_name:
return address
return formataddr((display_name, address))
def get_default_notification_templates() -> dict[str, dict[str, str]]:
from copy import deepcopy
from .tasks import DEFAULT_NOTIFICATION_TEMPLATES
templates = deepcopy(DEFAULT_NOTIFICATION_TEMPLATES)
branding_copy = get_branding_email_copy()
company_contact = get_company_contact_copy()
company_name = branding_copy['company_name']
support_email = company_contact['it_contact_email'] or branding_copy['support_email'] or f"it@{branding_copy['company_domain']}"
welcome = templates.get('onboarding_welcome')
if welcome:
welcome['subject'] = f'Willkommen bei {company_name}, {{ VORNAME }}'
welcome['subject_en'] = f'Welcome to {company_name}, {{ VORNAME }}'
welcome['body'] = (
'Hallo {{ FULL_NAME }},\n\n'
f'herzlich willkommen bei {company_name}.\n'
'Wir freuen uns sehr, dass du ab dem {{ CONTRACT_START }} unser Team in der Abteilung {{ DEPARTMENT }} verstärkst.\n\n'
'Deine dienstliche E-Mail-Adresse lautet: {{ EMAIL }}.\n'
'Im Anhang findest du deine Onboarding-Unterlagen als PDF.\n\n'
f'Wenn du Fragen hast, melde dich gerne jederzeit unter {support_email}.\n\n'
'Viele Grüße\n'
f'{company_name} IT'
)
welcome['body_en'] = (
'Hello {{ FULL_NAME }},\n\n'
f'welcome to {company_name}.\n'
'We are very happy that you will join our {{ DEPARTMENT }} team starting on {{ CONTRACT_START }}.\n\n'
'Your work email address is: {{ EMAIL }}.\n'
'You will find your onboarding documents attached as a PDF.\n\n'
f'If you have any questions, feel free to contact {support_email}.\n\n'
'Best regards,\n'
f'{company_name} IT'
)
return templates