Initial commit - Customer Portal for Coolify
This commit is contained in:
490
customer_portal/models/settings.py
Executable file
490
customer_portal/models/settings.py
Executable file
@@ -0,0 +1,490 @@
|
||||
"""Portal Settings model.
|
||||
|
||||
Stores configuration for the customer portal, including which fields
|
||||
are visible and editable for customers.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import Column, DateTime, Integer, String, Text
|
||||
|
||||
from customer_portal.models import Base
|
||||
|
||||
# Default field configuration
|
||||
DEFAULT_FIELD_CONFIG = {
|
||||
"profile_fields": {
|
||||
"name": {"visible": True, "editable": False, "label": "Name"},
|
||||
"email": {"visible": True, "editable": False, "label": "E-Mail"},
|
||||
"phone": {"visible": True, "editable": True, "label": "Telefon"},
|
||||
"address_street": {"visible": True, "editable": True, "label": "Strasse"},
|
||||
"address_zip": {"visible": True, "editable": True, "label": "PLZ"},
|
||||
"address_city": {"visible": True, "editable": True, "label": "Ort"},
|
||||
},
|
||||
"sections": {
|
||||
"contact": {"visible": True, "label": "Kontaktdaten"},
|
||||
"address": {"visible": True, "label": "Adresse"},
|
||||
"email_settings": {"visible": True, "label": "E-Mail Einstellungen"},
|
||||
"account_info": {"visible": True, "label": "Konto"},
|
||||
},
|
||||
"wp_fields": {}, # WordPress booking fields (dynamically loaded from WP API)
|
||||
"custom_fields_visible": False,
|
||||
"sync_button_visible": False,
|
||||
}
|
||||
|
||||
# Default mail configuration
|
||||
DEFAULT_MAIL_CONFIG = {
|
||||
"mail_server": "",
|
||||
"mail_port": 587,
|
||||
"mail_use_tls": True,
|
||||
"mail_use_ssl": False,
|
||||
"mail_username": "",
|
||||
"mail_password": "", # Stored encrypted or empty
|
||||
"mail_default_sender": "",
|
||||
"mail_default_sender_name": "Kundenportal",
|
||||
}
|
||||
|
||||
# Default OTP configuration
|
||||
DEFAULT_OTP_CONFIG = {
|
||||
"otp_expiry_minutes": 10,
|
||||
"otp_length": 6,
|
||||
"otp_max_attempts": 3,
|
||||
"prefill_token_expiry": 300, # 5 minutes
|
||||
}
|
||||
|
||||
# Default WordPress configuration
|
||||
DEFAULT_WORDPRESS_CONFIG = {
|
||||
"wp_api_url": "",
|
||||
"wp_api_secret": "",
|
||||
"wp_booking_page_url": "",
|
||||
}
|
||||
|
||||
# Default customer preferences for new customers
|
||||
DEFAULT_CUSTOMER_DEFAULTS = {
|
||||
"email_notifications": True, # Buchungsbestaetigungen
|
||||
"email_reminders": True, # Kurserinnerungen
|
||||
"email_invoices": True, # Rechnungen
|
||||
"email_marketing": False, # Marketing/Newsletter (Opt-in)
|
||||
}
|
||||
|
||||
# Default field mapping (Portal → WordPress)
|
||||
# Format: portal_field: wp_field
|
||||
# When syncing, data flows: WordPress booking → Portal field
|
||||
DEFAULT_FIELD_MAPPING = {
|
||||
"mappings": {
|
||||
# Portal field → WordPress field name
|
||||
"db.phone": "telefon",
|
||||
"db.address_street": "adresse",
|
||||
"db.address_zip": "", # Not mapped by default
|
||||
"db.address_city": "", # Not mapped by default
|
||||
"custom.vorname": "vorname",
|
||||
"custom.nachname": "nachname",
|
||||
"custom.geburtsdatum": "geburtsdatum",
|
||||
"custom.pferdename": "name_des_pferdes",
|
||||
"custom.geschlecht_pferd": "geschlecht_des_pferdes",
|
||||
},
|
||||
"auto_sync_on_booking": True, # Sync when new booking arrives
|
||||
}
|
||||
|
||||
# Default admin customer view configuration
|
||||
DEFAULT_ADMIN_CUSTOMER_VIEW = {
|
||||
"field_labels": {
|
||||
# Standard-Labels fuer bekannte Felder
|
||||
"address_zip": "PLZ",
|
||||
"address_city": "Ort",
|
||||
"address_street": "Strasse",
|
||||
"vorname": "Vorname",
|
||||
"nachname": "Nachname",
|
||||
"geburtsdatum": "Geburtsdatum",
|
||||
"pferdename": "Pferdename",
|
||||
"geschlecht_pferd": "Geschlecht Pferd",
|
||||
},
|
||||
"hidden_fields": [], # Diese Felder nie anzeigen
|
||||
"field_order": [], # Reihenfolge (leer = alphabetisch)
|
||||
"contact_fields": ["name", "email", "phone", "address"], # In Kontaktdaten anzeigen
|
||||
"sections": {
|
||||
"contact": {"visible": True, "label": "Kontaktdaten"},
|
||||
"personal": {"visible": True, "label": "Personendaten"},
|
||||
"email_settings": {"visible": True, "label": "E-Mail-Einstellungen"},
|
||||
"account_info": {"visible": True, "label": "Konto-Informationen"},
|
||||
},
|
||||
}
|
||||
|
||||
# Default branding configuration
|
||||
DEFAULT_BRANDING_CONFIG = {
|
||||
"company_name": "Kundenportal",
|
||||
"logo_url": "",
|
||||
"favicon_url": "",
|
||||
"colors": {
|
||||
"primary": "#198754", # Buttons, Links (Bootstrap success green)
|
||||
"primary_hover": "#157347", # Hover states
|
||||
"background": "#1a1d21", # Page background
|
||||
"header_bg": "#212529", # Topbar/Sidebar background
|
||||
"sidebar_bg": "#1a1d21", # Sidebar background
|
||||
"text": "#f8f9fa", # Main text color
|
||||
"muted": "#6c757d", # Muted text
|
||||
"border": "#2d3238", # Border color
|
||||
},
|
||||
}
|
||||
|
||||
# Default CSV export/import configuration
|
||||
DEFAULT_CSV_CONFIG = {
|
||||
"export_fields": [
|
||||
{"key": "id", "label": "ID", "enabled": True},
|
||||
{"key": "email", "label": "E-Mail", "enabled": True},
|
||||
{"key": "name", "label": "Name", "enabled": True},
|
||||
{"key": "phone", "label": "Telefon", "enabled": True},
|
||||
{"key": "address_street", "label": "Strasse", "enabled": True},
|
||||
{"key": "address_zip", "label": "PLZ", "enabled": True},
|
||||
{"key": "address_city", "label": "Ort", "enabled": True},
|
||||
{
|
||||
"key": "email_notifications",
|
||||
"label": "E-Mail-Benachrichtigungen",
|
||||
"enabled": True,
|
||||
},
|
||||
{"key": "email_reminders", "label": "E-Mail-Erinnerungen", "enabled": True},
|
||||
{"key": "email_invoices", "label": "E-Mail-Rechnungen", "enabled": True},
|
||||
{"key": "email_marketing", "label": "E-Mail-Marketing", "enabled": True},
|
||||
{"key": "created_at", "label": "Erstellt am", "enabled": False},
|
||||
{"key": "updated_at", "label": "Aktualisiert am", "enabled": False},
|
||||
],
|
||||
"include_custom_fields": False, # Include custom fields (Personendaten) as JSON
|
||||
"delimiter": ";", # CSV delimiter
|
||||
}
|
||||
|
||||
|
||||
class PortalSettings(Base):
|
||||
"""Portal configuration settings.
|
||||
|
||||
Stores all portal settings as JSON for flexibility.
|
||||
Uses a key-value approach for different setting groups.
|
||||
"""
|
||||
|
||||
__tablename__ = "portal_settings"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
key = Column(String(100), unique=True, nullable=False, index=True)
|
||||
value = Column(Text, nullable=False, default="{}")
|
||||
updated_at = Column(
|
||||
DateTime,
|
||||
default=lambda: datetime.now(UTC),
|
||||
onupdate=lambda: datetime.now(UTC),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<PortalSettings {self.key}>"
|
||||
|
||||
def get_value(self) -> Any:
|
||||
"""Get setting value as Python object."""
|
||||
try:
|
||||
return json.loads(self.value)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return {}
|
||||
|
||||
def set_value(self, data: Any) -> None:
|
||||
"""Set setting value from Python object."""
|
||||
self.value = json.dumps(data, ensure_ascii=False)
|
||||
|
||||
@classmethod
|
||||
def get_setting(cls, db, key: str, default: Any = None) -> Any:
|
||||
"""Get a setting by key.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
key: Setting key
|
||||
default: Default value if not found
|
||||
|
||||
Returns:
|
||||
Setting value or default
|
||||
"""
|
||||
setting = db.query(cls).filter(cls.key == key).first()
|
||||
if setting:
|
||||
return setting.get_value()
|
||||
return default
|
||||
|
||||
@classmethod
|
||||
def set_setting(cls, db, key: str, value: Any) -> "PortalSettings":
|
||||
"""Set a setting by key.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
key: Setting key
|
||||
value: Setting value
|
||||
|
||||
Returns:
|
||||
PortalSettings instance
|
||||
"""
|
||||
setting = db.query(cls).filter(cls.key == key).first()
|
||||
if not setting:
|
||||
setting = cls(key=key)
|
||||
db.add(setting)
|
||||
setting.set_value(value)
|
||||
db.commit()
|
||||
return setting
|
||||
|
||||
@classmethod
|
||||
def get_field_config(cls, db) -> dict:
|
||||
"""Get the field configuration.
|
||||
|
||||
Returns merged default config with saved config.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Field configuration dictionary
|
||||
"""
|
||||
saved = cls.get_setting(db, "field_config", {})
|
||||
# Merge with defaults
|
||||
config = DEFAULT_FIELD_CONFIG.copy()
|
||||
if saved:
|
||||
# Deep merge for profile_fields
|
||||
if "profile_fields" in saved:
|
||||
for field, settings in saved["profile_fields"].items():
|
||||
if field in config["profile_fields"]:
|
||||
config["profile_fields"][field].update(settings)
|
||||
else:
|
||||
config["profile_fields"][field] = settings
|
||||
# Deep merge for sections
|
||||
if "sections" in saved:
|
||||
for section, settings in saved["sections"].items():
|
||||
if section in config["sections"]:
|
||||
config["sections"][section].update(settings)
|
||||
else:
|
||||
config["sections"][section] = settings
|
||||
# WordPress fields (complete override, not merge)
|
||||
if "wp_fields" in saved:
|
||||
config["wp_fields"] = saved["wp_fields"]
|
||||
# Simple overrides
|
||||
if "custom_fields_visible" in saved:
|
||||
config["custom_fields_visible"] = saved["custom_fields_visible"]
|
||||
if "sync_button_visible" in saved:
|
||||
config["sync_button_visible"] = saved["sync_button_visible"]
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def save_field_config(cls, db, config: dict) -> None:
|
||||
"""Save the field configuration.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
config: Field configuration dictionary
|
||||
"""
|
||||
cls.set_setting(db, "field_config", config)
|
||||
|
||||
# Mail configuration methods
|
||||
@classmethod
|
||||
def get_mail_config(cls, db) -> dict:
|
||||
"""Get mail configuration with defaults."""
|
||||
saved = cls.get_setting(db, "mail_config", {})
|
||||
config = DEFAULT_MAIL_CONFIG.copy()
|
||||
config.update(saved)
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def save_mail_config(cls, db, config: dict) -> None:
|
||||
"""Save mail configuration."""
|
||||
cls.set_setting(db, "mail_config", config)
|
||||
|
||||
# OTP configuration methods
|
||||
@classmethod
|
||||
def get_otp_config(cls, db) -> dict:
|
||||
"""Get OTP configuration with defaults."""
|
||||
saved = cls.get_setting(db, "otp_config", {})
|
||||
config = DEFAULT_OTP_CONFIG.copy()
|
||||
config.update(saved)
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def save_otp_config(cls, db, config: dict) -> None:
|
||||
"""Save OTP configuration."""
|
||||
cls.set_setting(db, "otp_config", config)
|
||||
|
||||
# WordPress configuration methods
|
||||
@classmethod
|
||||
def get_wordpress_config(cls, db) -> dict:
|
||||
"""Get WordPress configuration with defaults."""
|
||||
saved = cls.get_setting(db, "wordpress_config", {})
|
||||
config = DEFAULT_WORDPRESS_CONFIG.copy()
|
||||
config.update(saved)
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def save_wordpress_config(cls, db, config: dict) -> None:
|
||||
"""Save WordPress configuration."""
|
||||
cls.set_setting(db, "wordpress_config", config)
|
||||
|
||||
# CSV configuration methods
|
||||
@classmethod
|
||||
def get_csv_config(cls, db) -> dict:
|
||||
"""Get CSV export/import configuration with defaults."""
|
||||
saved = cls.get_setting(db, "csv_config", {})
|
||||
config = {
|
||||
"export_fields": DEFAULT_CSV_CONFIG["export_fields"].copy(),
|
||||
"include_custom_fields": DEFAULT_CSV_CONFIG["include_custom_fields"],
|
||||
"delimiter": DEFAULT_CSV_CONFIG["delimiter"],
|
||||
}
|
||||
if saved:
|
||||
# Merge export_fields
|
||||
if "export_fields" in saved:
|
||||
saved_map = {f["key"]: f for f in saved["export_fields"]}
|
||||
for field in config["export_fields"]:
|
||||
if field["key"] in saved_map:
|
||||
field.update(saved_map[field["key"]])
|
||||
if "include_custom_fields" in saved:
|
||||
config["include_custom_fields"] = saved["include_custom_fields"]
|
||||
if "delimiter" in saved:
|
||||
config["delimiter"] = saved["delimiter"]
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def save_csv_config(cls, db, config: dict) -> None:
|
||||
"""Save CSV configuration."""
|
||||
cls.set_setting(db, "csv_config", config)
|
||||
|
||||
# Customer defaults configuration methods
|
||||
@classmethod
|
||||
def get_customer_defaults(cls, db) -> dict:
|
||||
"""Get default preferences for new customers.
|
||||
|
||||
These settings are applied when a new customer is created.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Customer defaults dictionary
|
||||
"""
|
||||
saved = cls.get_setting(db, "customer_defaults", {})
|
||||
config = DEFAULT_CUSTOMER_DEFAULTS.copy()
|
||||
config.update(saved)
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def save_customer_defaults(cls, db, config: dict) -> None:
|
||||
"""Save default preferences for new customers.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
config: Customer defaults dictionary
|
||||
"""
|
||||
cls.set_setting(db, "customer_defaults", config)
|
||||
|
||||
# Field mapping configuration methods
|
||||
@classmethod
|
||||
def get_field_mapping(cls, db) -> dict:
|
||||
"""Get WordPress to Portal field mapping configuration.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Field mapping dictionary with 'mappings' and 'auto_sync_on_booking'
|
||||
"""
|
||||
saved = cls.get_setting(db, "field_mapping", {})
|
||||
config = {
|
||||
"mappings": DEFAULT_FIELD_MAPPING["mappings"].copy(),
|
||||
"auto_sync_on_booking": DEFAULT_FIELD_MAPPING["auto_sync_on_booking"],
|
||||
}
|
||||
if saved:
|
||||
if "mappings" in saved:
|
||||
config["mappings"].update(saved["mappings"])
|
||||
if "auto_sync_on_booking" in saved:
|
||||
config["auto_sync_on_booking"] = saved["auto_sync_on_booking"]
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def save_field_mapping(cls, db, config: dict) -> None:
|
||||
"""Save field mapping configuration.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
config: Field mapping dictionary
|
||||
"""
|
||||
cls.set_setting(db, "field_mapping", config)
|
||||
|
||||
# Branding configuration methods
|
||||
@classmethod
|
||||
def get_branding(cls, db) -> dict:
|
||||
"""Get branding configuration with defaults.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Branding configuration dictionary
|
||||
"""
|
||||
saved = cls.get_setting(db, "branding", {})
|
||||
config = {
|
||||
"company_name": DEFAULT_BRANDING_CONFIG["company_name"],
|
||||
"logo_url": DEFAULT_BRANDING_CONFIG["logo_url"],
|
||||
"favicon_url": DEFAULT_BRANDING_CONFIG["favicon_url"],
|
||||
"colors": DEFAULT_BRANDING_CONFIG["colors"].copy(),
|
||||
}
|
||||
if saved:
|
||||
if "company_name" in saved:
|
||||
config["company_name"] = saved["company_name"]
|
||||
if "logo_url" in saved:
|
||||
config["logo_url"] = saved["logo_url"]
|
||||
if "favicon_url" in saved:
|
||||
config["favicon_url"] = saved["favicon_url"]
|
||||
if "colors" in saved:
|
||||
config["colors"].update(saved["colors"])
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def save_branding(cls, db, config: dict) -> None:
|
||||
"""Save branding configuration.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
config: Branding configuration dictionary
|
||||
"""
|
||||
cls.set_setting(db, "branding", config)
|
||||
|
||||
# Admin Customer View configuration methods
|
||||
@classmethod
|
||||
def get_admin_customer_view(cls, db) -> dict:
|
||||
"""Get admin customer view configuration with defaults.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
Admin customer view configuration dictionary
|
||||
"""
|
||||
saved = cls.get_setting(db, "admin_customer_view", {})
|
||||
config = {
|
||||
"field_labels": DEFAULT_ADMIN_CUSTOMER_VIEW["field_labels"].copy(),
|
||||
"hidden_fields": DEFAULT_ADMIN_CUSTOMER_VIEW["hidden_fields"].copy(),
|
||||
"field_order": DEFAULT_ADMIN_CUSTOMER_VIEW["field_order"].copy(),
|
||||
"contact_fields": DEFAULT_ADMIN_CUSTOMER_VIEW["contact_fields"].copy(),
|
||||
"sections": {
|
||||
k: v.copy() for k, v in DEFAULT_ADMIN_CUSTOMER_VIEW["sections"].items()
|
||||
},
|
||||
}
|
||||
if saved:
|
||||
if "field_labels" in saved:
|
||||
config["field_labels"].update(saved["field_labels"])
|
||||
if "hidden_fields" in saved:
|
||||
config["hidden_fields"] = saved["hidden_fields"]
|
||||
if "field_order" in saved:
|
||||
config["field_order"] = saved["field_order"]
|
||||
if "contact_fields" in saved:
|
||||
config["contact_fields"] = saved["contact_fields"]
|
||||
if "sections" in saved:
|
||||
for section_key, section_data in saved["sections"].items():
|
||||
if section_key in config["sections"]:
|
||||
config["sections"][section_key].update(section_data)
|
||||
return config
|
||||
|
||||
@classmethod
|
||||
def save_admin_customer_view(cls, db, config: dict) -> None:
|
||||
"""Save admin customer view configuration.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
config: Admin customer view configuration dictionary
|
||||
"""
|
||||
cls.set_setting(db, "admin_customer_view", config)
|
||||
Reference in New Issue
Block a user