""" Core models for the Tenhal Multidisciplinary Healthcare Platform. This module contains the base models and mixins used throughout the application, as well as core entities like User, Patient, Tenant, File, Consent, etc. """ import uuid from django.contrib.auth.models import AbstractUser from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField from simple_history.models import HistoricalRecords # ============================================================================ # BASE MIXINS # ============================================================================ class UUIDPrimaryKeyMixin(models.Model): """Mixin to use UUID as primary key for all models.""" id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False, verbose_name=_("ID") ) class Meta: abstract = True class TimeStampedMixin(models.Model): """Mixin to add created_at and updated_at timestamps.""" created_at = models.DateTimeField( auto_now_add=True, verbose_name=_("Created At") ) updated_at = models.DateTimeField( auto_now=True, verbose_name=_("Updated At") ) class Meta: abstract = True class TenantOwnedMixin(models.Model): """Mixin for models that belong to a tenant.""" tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='%(app_label)s_%(class)s_related', verbose_name=_("Tenant") ) class Meta: abstract = True class ClinicallySignableMixin(models.Model): """Mixin for clinical documents that can be signed.""" signed_by = models.ForeignKey( 'core.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='%(app_label)s_%(class)s_signed', verbose_name=_("Signed By") ) signed_at = models.DateTimeField( null=True, blank=True, verbose_name=_("Signed At") ) class Meta: abstract = True class SoftDeleteMixin(models.Model): """Mixin for soft delete functionality.""" is_deleted = models.BooleanField( default=False, verbose_name=_("Is Deleted") ) deleted_at = models.DateTimeField( null=True, blank=True, verbose_name=_("Deleted At") ) deleted_by = models.ForeignKey( 'User', on_delete=models.SET_NULL, null=True, blank=True, related_name='deleted_%(class)s_set', verbose_name=_("Deleted By") ) class Meta: abstract = True # ============================================================================ # CORE MODELS # ============================================================================ class Tenant(UUIDPrimaryKeyMixin, TimeStampedMixin): """ Tenant model for multi-tenancy support. Each clinic/organization is a tenant. """ name = models.CharField( max_length=200, verbose_name=_("Name") ) name_ar = models.CharField( max_length=200, blank=True, verbose_name=_("Name (Arabic)") ) code = models.CharField( max_length=50, unique=True, verbose_name=_("Code") ) # ZATCA E-Invoice Required Fields vat_number = models.CharField( max_length=15, unique=True, blank=True, verbose_name=_("VAT Registration Number"), help_text=_("15 digits, must start and end with 3 (e.g., 300000000000003)") ) # Address Information (for ZATCA invoices) address = models.TextField( blank=True, verbose_name=_("Address") ) city = models.CharField( max_length=100, default='Riyadh', verbose_name=_("City") ) postal_code = models.CharField( max_length=20, default='12345', verbose_name=_("Postal Code") ) country_code = models.CharField( max_length=2, default='SA', verbose_name=_("Country Code") ) is_active = models.BooleanField( default=True, verbose_name=_("Is Active") ) settings = models.JSONField( default=dict, blank=True, verbose_name=_("Settings") ) class Meta: verbose_name = _("Tenant") verbose_name_plural = _("Tenants") ordering = ['name'] def __str__(self): return self.name class User(AbstractUser, UUIDPrimaryKeyMixin): """ Custom User model extending Django's AbstractUser. Supports role-based access control and user profiles. """ class Role(models.TextChoices): ADMIN = 'ADMIN', _('Administrator') DOCTOR = 'DOCTOR', _('Doctor') NURSE = 'NURSE', _('Nurse') OT = 'OT', _('Occupational Therapist') SLP = 'SLP', _('Speech-Language Pathologist') ABA = 'ABA', _('ABA Therapist') FRONT_DESK = 'FRONT_DESK', _('Front Desk') FINANCE = 'FINANCE', _('Finance') tenant = models.ForeignKey( Tenant, on_delete=models.CASCADE, related_name='users', null=True, blank=True, verbose_name=_("Tenant") ) role = models.CharField( max_length=20, choices=Role.choices, default=Role.FRONT_DESK, verbose_name=_("Role") ) phone_number = PhoneNumberField( blank=True, verbose_name=_("Phone Number") ) employee_id = models.CharField( max_length=50, blank=True, verbose_name=_("Employee ID") ) # Profile fields profile_picture = models.ImageField( upload_to='profile_pictures/', null=True, blank=True, verbose_name=_("Profile Picture") ) bio = models.TextField( blank=True, verbose_name=_("Bio"), help_text=_("Brief description about yourself") ) preferences = models.JSONField( default=dict, blank=True, verbose_name=_("Preferences"), help_text=_("User preferences (language, notifications, etc.)") ) email_verified = models.BooleanField( default=False, verbose_name=_("Email Verified") ) last_login_ip = models.GenericIPAddressField( null=True, blank=True, verbose_name=_("Last Login IP") ) timezone = models.CharField( max_length=50, default='Asia/Riyadh', verbose_name=_("Timezone") ) history = HistoricalRecords() class Meta: verbose_name = _("User") verbose_name_plural = _("Users") ordering = ['last_name', 'first_name'] def __str__(self): return f"{self.get_full_name()} ({self.get_role_display()})" def get_profile_completion(self): """Calculate profile completion percentage.""" fields = [ self.first_name, self.last_name, self.email, self.phone_number, self.employee_id, self.profile_picture, self.bio, ] completed = sum(1 for field in fields if field) return int((completed / len(fields)) * 100) def get_initials(self): """Get user initials for avatar.""" if self.first_name and self.last_name: return f"{self.first_name[0]}{self.last_name[0]}".upper() elif self.first_name: return self.first_name[0].upper() elif self.username: return self.username[0].upper() return "U" class Patient(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin): """ Patient model storing demographic and contact information. """ class Sex(models.TextChoices): MALE = 'M', _('Male') FEMALE = 'F', _('Female') # Medical Record Number (auto-generated) mrn = models.CharField( max_length=20, unique=True, editable=False, verbose_name=_("MRN") ) # Identification national_id = models.CharField( max_length=20, blank=True, verbose_name=_("National ID") ) # Names (bilingual) first_name_en = models.CharField( max_length=100, verbose_name=_("First Name (English)") ) father_name_en = models.CharField( max_length=100, blank=True, verbose_name=_("Father Name (English)") ) grandfather_name_en = models.CharField( max_length=100, blank=True, verbose_name=_("Grandfather Name (English)") ) last_name_en = models.CharField( max_length=100, verbose_name=_("Last Name (English)") ) first_name_ar = models.CharField( max_length=100, blank=True, verbose_name=_("First Name (Arabic)") ) father_name_ar = models.CharField( max_length=100, blank=True, verbose_name=_("Father Name (Arabic)") ) grandfather_name_ar = models.CharField( max_length=100, blank=True, verbose_name=_("Grandfather Name (Arabic)") ) last_name_ar = models.CharField( max_length=100, blank=True, verbose_name=_("Last Name (Arabic)") ) # Demographics date_of_birth = models.DateField( verbose_name=_("Date of Birth") ) sex = models.CharField( max_length=1, choices=Sex.choices, verbose_name=_("Sex") ) # Contact Information phone = PhoneNumberField( blank=True, verbose_name=_("Phone Number") ) email = models.EmailField( blank=True, verbose_name=_("Email") ) # Caregiver Information caregiver_name = models.CharField( max_length=200, blank=True, verbose_name=_("Caregiver Name") ) caregiver_phone = PhoneNumberField( blank=True, verbose_name=_("Caregiver Phone") ) caregiver_relationship = models.CharField( max_length=100, blank=True, verbose_name=_("Caregiver Relationship") ) # Address address = models.TextField( blank=True, verbose_name=_("Address") ) city = models.CharField( max_length=100, blank=True, verbose_name=_("City") ) postal_code = models.CharField( max_length=20, blank=True, verbose_name=_("Postal Code") ) # Emergency Contact emergency_contact = models.TextField( blank=True, verbose_name=_("Emergency Contact") ) history = HistoricalRecords() class Meta: verbose_name = _("Patient") verbose_name_plural = _("Patients") ordering = ['-created_at'] indexes = [ models.Index(fields=['mrn']), models.Index(fields=['national_id']), models.Index(fields=['tenant', 'created_at']), ] def __str__(self): return f"{self.first_name_en} {self.last_name_en} ({self.mrn})" @property def full_name_en(self): """Full name in English with father and grandfather names.""" parts = [self.first_name_en] if self.father_name_en: parts.append(self.father_name_en) if self.grandfather_name_en: parts.append(self.grandfather_name_en) parts.append(self.last_name_en) return " ".join(parts) @property def full_name_ar(self): """Full name in Arabic with father and grandfather names.""" if not self.first_name_ar or not self.last_name_ar: return "" parts = [self.first_name_ar] if self.father_name_ar: parts.append(self.father_name_ar) if self.grandfather_name_ar: parts.append(self.grandfather_name_ar) parts.append(self.last_name_ar) return " ".join(parts) @property def age(self): from datetime import date today = date.today() return today.year - self.date_of_birth.year - ( (today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day) ) class Clinic(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin): """ Clinic/Department model representing different specialties. """ class Specialty(models.TextChoices): MEDICAL = 'MEDICAL', _('Medical') NURSING = 'NURSING', _('Nursing') ABA = 'ABA', _('ABA Therapy') OT = 'OT', _('Occupational Therapy') SLP = 'SLP', _('Speech-Language Pathology') name_en = models.CharField( max_length=200, verbose_name=_("Name (English)") ) name_ar = models.CharField( max_length=200, blank=True, verbose_name=_("Name (Arabic)") ) specialty = models.CharField( max_length=20, choices=Specialty.choices, verbose_name=_("Specialty") ) code = models.CharField( max_length=50, verbose_name=_("Code") ) is_active = models.BooleanField( default=True, verbose_name=_("Is Active") ) class Meta: verbose_name = _("Clinic") verbose_name_plural = _("Clinics") ordering = ['name_en'] unique_together = [['tenant', 'code']] def __str__(self): return f"{self.name_en} ({self.get_specialty_display()})" class File(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin): """ Main patient file (medical record). Auto-created when a patient is registered. """ class Status(models.TextChoices): ACTIVE = 'ACTIVE', _('Active') CLOSED = 'CLOSED', _('Closed') ARCHIVED = 'ARCHIVED', _('Archived') file_number = models.CharField( max_length=20, unique=True, editable=False, verbose_name=_("File Number") ) patient = models.OneToOneField( Patient, on_delete=models.CASCADE, related_name='file', verbose_name=_("Patient") ) opened_date = models.DateField( auto_now_add=True, verbose_name=_("Opened Date") ) status = models.CharField( max_length=20, choices=Status.choices, default=Status.ACTIVE, verbose_name=_("Status") ) class Meta: verbose_name = _("File") verbose_name_plural = _("Files") ordering = ['-opened_date'] indexes = [ models.Index(fields=['file_number']), models.Index(fields=['patient']), ] def __str__(self): return f"File #{self.file_number} - {self.patient}" class SubFile(UUIDPrimaryKeyMixin, TimeStampedMixin): """ Sub-file per clinic/specialty. Created when patient first visits a specific clinic. """ class Status(models.TextChoices): ACTIVE = 'ACTIVE', _('Active') CLOSED = 'CLOSED', _('Closed') file = models.ForeignKey( File, on_delete=models.CASCADE, related_name='subfiles', verbose_name=_("File") ) clinic = models.ForeignKey( Clinic, on_delete=models.CASCADE, related_name='subfiles', verbose_name=_("Clinic") ) sub_file_number = models.CharField( max_length=20, verbose_name=_("Sub-File Number") ) opened_date = models.DateField( auto_now_add=True, verbose_name=_("Opened Date") ) status = models.CharField( max_length=20, choices=Status.choices, default=Status.ACTIVE, verbose_name=_("Status") ) assigned_provider = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_subfiles', verbose_name=_("Assigned Provider") ) class Meta: verbose_name = _("Sub-File") verbose_name_plural = _("Sub-Files") ordering = ['-opened_date'] unique_together = [['file', 'clinic']] def __str__(self): return f"SubFile #{self.sub_file_number} - {self.clinic.name_en}" class ConsentTemplate(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin): """ Predefined consent text templates for different consent types. """ class ConsentType(models.TextChoices): GENERAL_TREATMENT = 'GENERAL_TREATMENT', _('General Treatment') SERVICE_SPECIFIC = 'SERVICE_SPECIFIC', _('Service Specific') PHOTO_VIDEO = 'PHOTO_VIDEO', _('Photo/Video') DATA_SHARING = 'DATA_SHARING', _('Data Sharing') consent_type = models.CharField( max_length=30, choices=ConsentType.choices, verbose_name=_("Consent Type") ) title_en = models.CharField( max_length=200, verbose_name=_("Title (English)") ) title_ar = models.CharField( max_length=200, blank=True, verbose_name=_("Title (Arabic)") ) content_en = models.TextField( verbose_name=_("Content (English)"), help_text=_("Consent text in English. Use {patient_name}, {patient_mrn}, {date} as placeholders.") ) content_ar = models.TextField( blank=True, verbose_name=_("Content (Arabic)"), help_text=_("Consent text in Arabic. Use {patient_name}, {patient_mrn}, {date} as placeholders.") ) is_active = models.BooleanField( default=True, verbose_name=_("Is Active") ) version = models.PositiveIntegerField( default=1, verbose_name=_("Version") ) class Meta: verbose_name = _("Consent Template") verbose_name_plural = _("Consent Templates") ordering = ['consent_type', '-version'] unique_together = [['tenant', 'consent_type', 'version']] def __str__(self): return f"{self.get_consent_type_display()} - v{self.version}" def get_populated_content(self, patient, language='en'): """ Populate template with patient data. Args: patient: Patient instance language: 'en' or 'ar' Returns: Populated consent text """ from django.utils import timezone content = self.content_en if language == 'en' else self.content_ar if not content: content = self.content_en # Fallback to English # Prepare replacement values replacements = { '{patient_name}': patient.full_name_en if language == 'en' else patient.full_name_ar or patient.full_name_en, '{patient_mrn}': patient.mrn, '{date}': timezone.now().strftime('%Y-%m-%d'), '{patient_dob}': patient.date_of_birth.strftime('%Y-%m-%d'), '{patient_age}': str(patient.age), } # Replace placeholders for placeholder, value in replacements.items(): content = content.replace(placeholder, value) return content class Consent(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin): """ Consent forms with e-signature support. """ class ConsentType(models.TextChoices): GENERAL_TREATMENT = 'GENERAL_TREATMENT', _('General Treatment') SERVICE_SPECIFIC = 'SERVICE_SPECIFIC', _('Service Specific') PHOTO_VIDEO = 'PHOTO_VIDEO', _('Photo/Video') DATA_SHARING = 'DATA_SHARING', _('Data Sharing') class SignatureMethod(models.TextChoices): DRAWN = 'DRAWN', _('Drawn') TYPED = 'TYPED', _('Typed') UPLOADED = 'UPLOADED', _('Uploaded') EXTERNAL = 'EXTERNAL', _('External Provider') patient = models.ForeignKey( Patient, on_delete=models.CASCADE, related_name='consents', verbose_name=_("Patient") ) consent_type = models.CharField( max_length=30, choices=ConsentType.choices, verbose_name=_("Consent Type") ) content_text = models.TextField( verbose_name=_("Content Text") ) # Signature Information signed_by_name = models.CharField( max_length=200, blank=True, verbose_name=_("Signed By Name") ) signed_by_relationship = models.CharField( max_length=100, blank=True, verbose_name=_("Relationship to Patient") ) signed_at = models.DateTimeField( null=True, blank=True, verbose_name=_("Signed At") ) signature_method = models.CharField( max_length=20, choices=SignatureMethod.choices, blank=True, verbose_name=_("Signature Method") ) signature_image = models.ImageField( upload_to='consents/signatures/', null=True, blank=True, verbose_name=_("Signature Image") ) signature_hash = models.CharField( max_length=64, blank=True, verbose_name=_("Signature Hash") ) signed_ip = models.GenericIPAddressField( null=True, blank=True, verbose_name=_("Signed IP Address") ) signed_user_agent = models.TextField( blank=True, verbose_name=_("Signed User Agent") ) # Version Control version = models.PositiveIntegerField( default=1, verbose_name=_("Version") ) is_active = models.BooleanField( default=True, verbose_name=_("Is Active") ) # Expiry Management expiry_date = models.DateField( null=True, blank=True, verbose_name=_("Expiry Date"), help_text=_("Date when this consent expires and needs renewal") ) history = HistoricalRecords() class Meta: verbose_name = _("Consent") verbose_name_plural = _("Consents") ordering = ['-created_at'] indexes = [ models.Index(fields=['patient', 'consent_type']), ] def __str__(self): return f"{self.get_consent_type_display()} - {self.patient}" @property def is_expired(self): """Check if consent has expired.""" if not self.expiry_date: return False from datetime import date return date.today() > self.expiry_date @property def days_until_expiry(self): """Calculate days until consent expires.""" if not self.expiry_date: return None from datetime import date delta = self.expiry_date - date.today() return delta.days @property def needs_renewal(self): """Check if consent needs renewal (within 30 days of expiry or expired).""" if not self.expiry_date: return False days = self.days_until_expiry return days is not None and days <= 30 class ConsentToken(UUIDPrimaryKeyMixin, TimeStampedMixin): """ Secure token for email-based consent signing. Allows parents/guardians to sign consent without logging in. """ consent = models.ForeignKey( 'Consent', on_delete=models.CASCADE, related_name='tokens', verbose_name=_("Consent") ) token = models.CharField( max_length=64, unique=True, verbose_name=_("Token"), help_text=_("Secure token for accessing consent form") ) email = models.EmailField( verbose_name=_("Email Address"), help_text=_("Email address where consent link was sent") ) expires_at = models.DateTimeField( verbose_name=_("Expires At"), help_text=_("Token expiration date/time") ) used_at = models.DateTimeField( null=True, blank=True, verbose_name=_("Used At"), help_text=_("When the token was used to sign consent") ) is_active = models.BooleanField( default=True, verbose_name=_("Is Active") ) sent_by = models.ForeignKey( 'User', on_delete=models.SET_NULL, null=True, related_name='sent_consent_tokens', verbose_name=_("Sent By") ) class Meta: verbose_name = _("Consent Token") verbose_name_plural = _("Consent Tokens") ordering = ['-created_at'] indexes = [ models.Index(fields=['token']), models.Index(fields=['email']), models.Index(fields=['expires_at']), ] def __str__(self): return f"Token for {self.consent} - {self.email}" def is_valid(self): """Check if token is still valid.""" from django.utils import timezone return ( self.is_active and not self.used_at and self.expires_at > timezone.now() ) def mark_as_used(self): """Mark token as used.""" from django.utils import timezone self.used_at = timezone.now() self.is_active = False self.save() @staticmethod def generate_token(): """Generate a secure random token.""" import secrets return secrets.token_urlsafe(48) class Attachment(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin): """ Generic attachment model for any document. Uses GenericForeignKey to attach to any model. """ class FileType(models.TextChoices): PDF = 'PDF', _('PDF') IMAGE = 'IMAGE', _('Image') DOCUMENT = 'DOCUMENT', _('Document') OTHER = 'OTHER', _('Other') # Generic Foreign Key content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, verbose_name=_("Content Type") ) object_id = models.UUIDField( verbose_name=_("Object ID") ) content_object = GenericForeignKey('content_type', 'object_id') file = models.FileField( upload_to='attachments/%Y/%m/%d/', verbose_name=_("File") ) file_type = models.CharField( max_length=20, choices=FileType.choices, verbose_name=_("File Type") ) description = models.TextField( blank=True, verbose_name=_("Description") ) uploaded_by = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, related_name='uploaded_attachments', verbose_name=_("Uploaded By") ) class Meta: verbose_name = _("Attachment") verbose_name_plural = _("Attachments") ordering = ['-created_at'] indexes = [ models.Index(fields=['content_type', 'object_id']), ] def __str__(self): return f"{self.file.name} ({self.get_file_type_display()})" class AuditLog(UUIDPrimaryKeyMixin, TenantOwnedMixin): """ Audit log for tracking all actions in the system. Complements simple_history for additional tracking. """ class Action(models.TextChoices): CREATE = 'CREATE', _('Create') UPDATE = 'UPDATE', _('Update') DELETE = 'DELETE', _('Delete') VIEW = 'VIEW', _('View') EXPORT = 'EXPORT', _('Export') PRINT = 'PRINT', _('Print') # Generic Foreign Key content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("Content Type") ) object_id = models.UUIDField( null=True, blank=True, verbose_name=_("Object ID") ) content_object = GenericForeignKey('content_type', 'object_id') action = models.CharField( max_length=20, choices=Action.choices, verbose_name=_("Action") ) user = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, related_name='audit_logs', verbose_name=_("User") ) timestamp = models.DateTimeField( auto_now_add=True, verbose_name=_("Timestamp") ) changes = models.JSONField( default=dict, blank=True, verbose_name=_("Changes") ) ip_address = models.GenericIPAddressField( null=True, blank=True, verbose_name=_("IP Address") ) class Meta: verbose_name = _("Audit Log") verbose_name_plural = _("Audit Logs") ordering = ['-timestamp'] indexes = [ models.Index(fields=['content_type', 'object_id']), models.Index(fields=['user', 'timestamp']), models.Index(fields=['action', 'timestamp']), ] def __str__(self): return f"{self.user} - {self.get_action_display()} - {self.timestamp}" class SettingTemplate(UUIDPrimaryKeyMixin, TimeStampedMixin): """ Template defining available tenant settings with validation rules. Defines the structure and constraints for tenant configuration. """ class Category(models.TextChoices): BASIC = 'BASIC', _('Basic Information') VAT = 'VAT', _('VAT Registration') ADDRESS = 'ADDRESS', _('Address Information') ZATCA = 'ZATCA', _('ZATCA E-Invoicing') NPHIES = 'NPHIES', _('NPHIES Integration') SMS = 'SMS', _('SMS/WhatsApp Integration') LAB = 'LAB', _('Lab Integration') RADIOLOGY = 'RADIOLOGY', _('Radiology Integration') class DataType(models.TextChoices): STRING = 'STRING', _('String') INTEGER = 'INTEGER', _('Integer') BOOLEAN = 'BOOLEAN', _('Boolean') CHOICE = 'CHOICE', _('Choice') FILE = 'FILE', _('File') ENCRYPTED = 'ENCRYPTED', _('Encrypted') TEXT = 'TEXT', _('Text (Multi-line)') EMAIL = 'EMAIL', _('Email') URL = 'URL', _('URL') PHONE = 'PHONE', _('Phone Number') COLOR = 'COLOR', _('Color') key = models.CharField( max_length=100, unique=True, verbose_name=_("Key"), help_text=_("Unique identifier for this setting (e.g., 'basic_clinic_name_en')") ) category = models.CharField( max_length=20, choices=Category.choices, verbose_name=_("Category") ) label_en = models.CharField( max_length=200, verbose_name=_("Label (English)") ) label_ar = models.CharField( max_length=200, blank=True, verbose_name=_("Label (Arabic)") ) data_type = models.CharField( max_length=20, choices=DataType.choices, verbose_name=_("Data Type") ) is_required = models.BooleanField( default=False, verbose_name=_("Is Required"), help_text=_("Whether this setting must be provided") ) default_value = models.TextField( blank=True, verbose_name=_("Default Value") ) help_text_en = models.TextField( blank=True, verbose_name=_("Help Text (English)") ) help_text_ar = models.TextField( blank=True, verbose_name=_("Help Text (Arabic)") ) validation_regex = models.CharField( max_length=500, blank=True, verbose_name=_("Validation Regex"), help_text=_("Regular expression for validation (optional)") ) choices = models.JSONField( default=list, blank=True, verbose_name=_("Choices"), help_text=_("List of choices for CHOICE data type (e.g., [{'value': 'opt1', 'label': 'Option 1'}])") ) order = models.PositiveIntegerField( default=0, verbose_name=_("Display Order"), help_text=_("Order in which this setting appears in the form") ) is_active = models.BooleanField( default=True, verbose_name=_("Is Active") ) class Meta: verbose_name = _("Setting Template") verbose_name_plural = _("Setting Templates") ordering = ['category', 'order', 'label_en'] indexes = [ models.Index(fields=['category', 'order']), models.Index(fields=['key']), ] def __str__(self): return f"{self.get_category_display()} - {self.label_en}" class TenantSetting(UUIDPrimaryKeyMixin, TimeStampedMixin): """ Actual setting values for each tenant. Stores configuration based on SettingTemplate definitions. """ tenant = models.ForeignKey( Tenant, on_delete=models.CASCADE, related_name='tenant_settings', verbose_name=_("Tenant") ) template = models.ForeignKey( SettingTemplate, on_delete=models.CASCADE, related_name='tenant_values', verbose_name=_("Setting Template") ) value = models.TextField( blank=True, verbose_name=_("Value"), help_text=_("Stores string, integer, boolean, and choice values") ) encrypted_value = models.BinaryField( null=True, blank=True, verbose_name=_("Encrypted Value"), help_text=_("Stores encrypted sensitive data (API keys, tokens, etc.)") ) file_value = models.FileField( upload_to='tenant_settings/%Y/%m/%d/', null=True, blank=True, verbose_name=_("File Value"), help_text=_("Stores file uploads (logos, certificates, etc.)") ) updated_by = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True, related_name='updated_settings', verbose_name=_("Updated By") ) history = HistoricalRecords() class Meta: verbose_name = _("Tenant Setting") verbose_name_plural = _("Tenant Settings") ordering = ['tenant', 'template__category', 'template__order'] unique_together = [['tenant', 'template']] indexes = [ models.Index(fields=['tenant', 'template']), ] def __str__(self): return f"{self.tenant.name} - {self.template.label_en}" def get_typed_value(self): """ Returns the value converted to the appropriate type based on template data_type. """ if self.template.data_type == SettingTemplate.DataType.BOOLEAN: return self.value.lower() in ('true', '1', 'yes') if self.value else False elif self.template.data_type == SettingTemplate.DataType.INTEGER: try: return int(self.value) if self.value else None except (ValueError, TypeError): return None elif self.template.data_type == SettingTemplate.DataType.FILE: return self.file_value elif self.template.data_type == SettingTemplate.DataType.ENCRYPTED: # Will be decrypted by the service layer return self.encrypted_value else: return self.value class ContactMessage(UUIDPrimaryKeyMixin, TimeStampedMixin): """ Contact form submissions from the landing page. Stores inquiries from potential clients and visitors. Admins are notified via email and in-app notifications. """ name = models.CharField( max_length=200, verbose_name=_("Name") ) email = models.EmailField( verbose_name=_("Email") ) phone = PhoneNumberField( blank=True, verbose_name=_("Phone Number") ) message = models.TextField( verbose_name=_("Message") ) # Metadata for security and tracking submitted_at = models.DateTimeField( auto_now_add=True, verbose_name=_("Submitted At") ) ip_address = models.GenericIPAddressField( null=True, blank=True, verbose_name=_("IP Address") ) user_agent = models.TextField( blank=True, verbose_name=_("User Agent"), help_text=_("Browser and device information") ) # Admin tracking is_read = models.BooleanField( default=False, verbose_name=_("Is Read"), help_text=_("Whether an admin has viewed this message") ) responded_at = models.DateTimeField( null=True, blank=True, verbose_name=_("Responded At") ) responded_by = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True, related_name='responded_contact_messages', verbose_name=_("Responded By") ) notes = models.TextField( blank=True, verbose_name=_("Admin Notes"), help_text=_("Internal notes about this inquiry") ) class Meta: verbose_name = _("Contact Message") verbose_name_plural = _("Contact Messages") ordering = ['-submitted_at'] indexes = [ models.Index(fields=['email']), models.Index(fields=['is_read', 'submitted_at']), models.Index(fields=['submitted_at']), ] def __str__(self): return f"{self.name} - {self.email} ({self.submitted_at.strftime('%Y-%m-%d %H:%M')})" def mark_as_read(self): """Mark this message as read.""" if not self.is_read: self.is_read = True self.save(update_fields=['is_read']) def mark_as_responded(self, user): """Mark this message as responded to.""" from django.utils import timezone self.responded_at = timezone.now() self.responded_by = user self.is_read = True self.save(update_fields=['responded_at', 'responded_by', 'is_read'])