feat: add deployment config sync commands

This commit is contained in:
Md Bayazid Bostame
2026-03-29 01:35:40 +01:00
parent 5697f42306
commit a45c605b1e
5 changed files with 362 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
from __future__ import annotations
import json
from pathlib import Path
from django.core.serializers.json import DjangoJSONEncoder
from django.db import transaction
from django.utils import timezone
from .branding import get_portal_branding, get_portal_company_config
BRANDING_FIELDS = (
'name',
'portal_title',
'company_name',
'company_domain',
'support_email',
'sender_display_name',
'login_subtitle',
'footer_text',
'footer_text_en',
'legal_notice',
'legal_notice_en',
'default_language',
'primary_color',
'secondary_color',
)
COMPANY_FIELDS = (
'name',
'legal_company_name',
'street_address',
'postal_code',
'city',
'country',
'website_url',
'imprint_url',
'privacy_url',
'hr_contact_email',
'it_contact_email',
'operations_contact_email',
'phone_number',
'vat_id',
'registration_number',
)
FORMAT_VERSION = 1
def export_portal_deployment_config_payload() -> dict[str, object]:
branding = get_portal_branding()
company_config = get_portal_company_config()
return {
'format': 'portal_deployment_config',
'version': FORMAT_VERSION,
'generated_at': timezone.now(),
'branding': {field: getattr(branding, field) for field in BRANDING_FIELDS},
'company_config': {field: getattr(company_config, field) for field in COMPANY_FIELDS},
'notes': {
'file_fields_not_included': ['logo_image', 'pdf_letterhead', 'favicon_image'],
},
}
def write_portal_deployment_config_export(path: str | Path) -> Path:
payload = export_portal_deployment_config_payload()
output_path = Path(path)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(payload, indent=2, cls=DjangoJSONEncoder) + '\n', encoding='utf-8')
return output_path
def load_portal_deployment_config_payload(path: str | Path) -> dict[str, object]:
payload = json.loads(Path(path).read_text(encoding='utf-8'))
if payload.get('format') != 'portal_deployment_config':
raise ValueError('Unsupported payload format.')
if payload.get('version') != FORMAT_VERSION:
raise ValueError(f"Unsupported payload version: {payload.get('version')}")
return payload
def _validate_mapping(payload: dict[str, object], key: str, expected_fields: tuple[str, ...]) -> dict[str, str]:
value = payload.get(key)
if not isinstance(value, dict):
raise ValueError(f'{key} must be an object.')
fields = set(value.keys())
expected = set(expected_fields)
missing = expected.difference(fields)
extra = fields.difference(expected)
if missing:
raise ValueError(f'Missing {key} fields: {", ".join(sorted(missing))}')
if extra:
raise ValueError(f'Unexpected {key} fields: {", ".join(sorted(extra))}')
normalized: dict[str, str] = {}
for field in expected_fields:
field_value = value[field]
if field_value is None:
field_value = ''
if not isinstance(field_value, str):
raise ValueError(f'Invalid {key}.{field}: expected string')
normalized[field] = field_value
return normalized
def validate_portal_deployment_config_payload(payload: dict[str, object]) -> dict[str, dict[str, str]]:
return {
'branding': _validate_mapping(payload, 'branding', BRANDING_FIELDS),
'company_config': _validate_mapping(payload, 'company_config', COMPANY_FIELDS),
}
@transaction.atomic
def import_portal_deployment_config_payload(payload: dict[str, object], dry_run: bool = False) -> dict[str, int]:
validated = validate_portal_deployment_config_payload(payload)
branding = get_portal_branding()
company_config = get_portal_company_config()
updated = 0
for field, value in validated['branding'].items():
if getattr(branding, field) != value:
setattr(branding, field, value)
updated += 1
for field, value in validated['company_config'].items():
if getattr(company_config, field) != value:
setattr(company_config, field, value)
updated += 1
if not dry_run:
branding.save(update_fields=list(BRANDING_FIELDS))
company_config.save(update_fields=list(COMPANY_FIELDS))
else:
transaction.set_rollback(True)
return {
'updated_fields': updated,
'dry_run': 1 if dry_run else 0,
}