""" Accounts models - Custom User model and roles """ import uuid from django.contrib.auth.models import AbstractUser, Group, Permission, BaseUserManager from django.db import models from django.utils.translation import gettext_lazy as _ from apps.core.models import TimeStampedModel, UUIDModel class UserManager(BaseUserManager): """ Custom user manager for email-based authentication. """ def create_user(self, email, password=None, **extra_fields): """ Create and save a regular user with the given email and password. """ if not email: raise ValueError("The Email field must be set") email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, email, password=None, **extra_fields): """ Create and save a superuser with the given email and password. """ extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) extra_fields.setdefault("is_active", True) if extra_fields.get("is_staff") is not True: raise ValueError("Superuser must have is_staff=True.") if extra_fields.get("is_superuser") is not True: raise ValueError("Superuser must have is_superuser=True.") return self.create_user(email, password, **extra_fields) class User(AbstractUser, TimeStampedModel): """ Custom User model extending Django's AbstractUser. Uses UUID as primary key and adds additional fields for PX360. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) # Override email to make it unique and required email = models.EmailField(unique=True) # Override username to be optional and non-unique (for backward compatibility) # Note: Using email as USERNAME_FIELD for authentication username = models.CharField(max_length=150, blank=True, default="", unique=False) # Use email as username field for authentication USERNAME_FIELD = "email" # Required fields when creating superuser REQUIRED_FIELDS = ["first_name", "last_name"] # Custom user manager objects = UserManager() # Additional fields phone = models.CharField(max_length=20, blank=True) employee_id = models.CharField(max_length=50, blank=True) # Organization relationships hospital = models.ForeignKey( "organizations.Hospital", on_delete=models.SET_NULL, null=True, blank=True, related_name="users" ) department = models.ForeignKey( "organizations.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="users" ) # Role - using Django's built-in Group for RBAC # Groups will represent roles: PX Admin, Hospital Admin, Department Manager, etc. # Profile avatar = models.ImageField(upload_to="avatars/", null=True, blank=True) bio = models.TextField(blank=True) # Preferences language = models.CharField(max_length=5, choices=[("en", "English"), ("ar", "Arabic")], default="en") # Notification preferences notification_email_enabled = models.BooleanField(default=True, help_text="Enable email notifications") notification_sms_enabled = models.BooleanField(default=False, help_text="Enable SMS notifications") preferred_notification_channel = models.CharField( max_length=10, choices=[("email", "Email"), ("sms", "SMS"), ("both", "Both")], default="email", help_text="Preferred notification channel for general notifications", ) explanation_notification_channel = models.CharField( max_length=10, choices=[("email", "Email"), ("sms", "SMS"), ("both", "Both")], default="email", help_text="Preferred channel for explanation requests", ) # Status is_active = models.BooleanField(default=True) # Onboarding / Acknowledgement is_provisional = models.BooleanField(default=False, help_text="User is in onboarding process") invitation_token = models.CharField( max_length=100, unique=True, null=True, blank=True, help_text="Token for account activation" ) invitation_expires_at = models.DateTimeField(null=True, blank=True, help_text="When the invitation token expires") acknowledgement_completed = models.BooleanField( default=False, help_text="User has completed acknowledgement wizard" ) acknowledgement_completed_at = models.DateTimeField( null=True, blank=True, help_text="When acknowledgement was completed" ) current_wizard_step = models.IntegerField(default=0, help_text="Current step in onboarding wizard") wizard_completed_steps = models.JSONField(default=list, blank=True, help_text="List of completed wizard step IDs") class Meta: ordering = ["-date_joined"] indexes = [ models.Index(fields=["is_active", "-date_joined"]), ] def __str__(self): return f"{self.get_full_name()} ({self.email})" def get_role_names(self): """Get list of role names for this user""" return list(self.groups.values_list("name", flat=True)) def has_role(self, role_name): """Check if user has a specific role""" return self.groups.filter(name=role_name).exists() def is_px_admin(self): """Check if user is PX Admin""" return self.has_role("PX Admin") def is_hospital_admin(self): """Check if user is Hospital Admin""" return self.has_role("Hospital Admin") def is_department_manager(self): """Check if user is Department Manager""" return self.has_role("Department Manager") def is_px_staff(self): """Check if user is PX Staff""" return self.has_role("PX Staff") def is_source_user(self): """Check if user is a PX Source User""" return self.has_role("PX Source User") def is_executive(self): """Check if user is Executive (C-Suite/Top Management)""" return self.has_role("Executive") def get_source_user_profile_active(self): """Get active source user profile if exists""" if hasattr(self, "source_user_profile"): profile = self.source_user_profile if profile and profile.is_active: return profile return None def needs_onboarding(self): """Check if user needs to complete onboarding""" return self.is_provisional and not self.acknowledgement_completed def get_onboarding_progress_percentage(self): """Get onboarding progress percentage""" from .services import OnboardingService return OnboardingService.get_user_progress_percentage(self) class AcknowledgementCategory(UUIDModel, TimeStampedModel): """ Categories/Departments for acknowledgements. Admins can add, edit, or deactivate categories. Replaces hardcoded department choices with database-driven categories. """ name_en = models.CharField(max_length=200) name_ar = models.CharField(max_length=200, blank=True, help_text="Arabic name") code = models.CharField(max_length=50, unique=True, help_text="Unique code (e.g., CLINICS, ADMISSIONS)") description = models.TextField(blank=True, help_text="Category description") # Display & ordering icon = models.CharField(max_length=50, blank=True, help_text="Icon class (e.g., 'building', 'user')") color = models.CharField(max_length=7, default="#007bbd", help_text="Hex color code") order = models.IntegerField(default=0, help_text="Display order") # Status is_active = models.BooleanField(default=True, db_index=True) is_default = models.BooleanField(default=False, help_text="One of the 14 default categories") class Meta: ordering = ["order", "name_en"] verbose_name_plural = "Acknowledgement Categories" indexes = [ models.Index(fields=["is_active", "order"]), models.Index(fields=["is_default"]), ] def __str__(self): return self.name_en def get_localized_name(self): from django.utils.translation import get_language if get_language() == "ar" and self.name_ar: return self.name_ar return self.name_en def activate(self): self.is_active = True self.save(update_fields=["is_active"]) def deactivate(self): self.is_active = False self.save(update_fields=["is_active"]) class AcknowledgementContent(UUIDModel, TimeStampedModel): """ Acknowledgement content sections. Provides bilingual educational content for each acknowledgement category. """ # Link to category (replaces role field) category = models.ForeignKey( AcknowledgementCategory, on_delete=models.CASCADE, related_name="content_sections", help_text="Category this content belongs to", ) # Content section code = models.CharField(max_length=100, unique=True, help_text="Unique code for this content section") title_en = models.CharField(max_length=200) title_ar = models.CharField(max_length=200, blank=True) description_en = models.TextField() description_ar = models.TextField(blank=True) # Content details content_en = models.TextField(blank=True) content_ar = models.TextField(blank=True) # Visual elements icon = models.CharField(max_length=50, blank=True, help_text="Icon class (e.g., 'fa-user', 'fa-shield')") color = models.CharField(max_length=7, blank=True, help_text="Hex color code (e.g., '#007bff')") # Organization order = models.IntegerField(default=0, help_text="Display order") # Status is_active = models.BooleanField(default=True) class Meta: ordering = ["category", "order", "code"] indexes = [ models.Index(fields=["category", "is_active", "order"]), ] def __str__(self): return f"{self.category.name_en} - {self.title_en}" def get_localized_title(self): from django.utils.translation import get_language if get_language() == "ar" and self.title_ar: return self.title_ar return self.title_en def get_localized_description(self): from django.utils.translation import get_language if get_language() == "ar" and self.description_ar: return self.description_ar return self.description_en def get_localized_content(self): from django.utils.translation import get_language if get_language() == "ar" and self.content_ar: return self.content_ar return self.content_en class AcknowledgementChecklistItem(UUIDModel, TimeStampedModel): """ Checklist items that users must acknowledge. Each item is linked to a category (department). """ # Link to category (replaces role field) category = models.ForeignKey( AcknowledgementCategory, on_delete=models.PROTECT, related_name="checklist_items", help_text="Category/Department this acknowledgement belongs to", ) # Linked content (optional) content = models.ForeignKey( AcknowledgementContent, on_delete=models.SET_NULL, null=True, blank=True, related_name="checklist_items", help_text="Related content section", ) # Item details code = models.CharField(max_length=100, unique=True, help_text="Unique code for this checklist item") text_en = models.CharField(max_length=500) text_ar = models.CharField(max_length=500, blank=True) description_en = models.TextField(blank=True) description_ar = models.TextField(blank=True) # Configuration is_required = models.BooleanField(default=True, help_text="Item must be acknowledged") order = models.IntegerField(default=0, help_text="Display order in checklist") # Status is_active = models.BooleanField(default=True) class Meta: ordering = ["category", "order", "code"] indexes = [ models.Index(fields=["category", "is_active", "order"]), ] def __str__(self): return f"{self.category.name_en} - {self.text_en}" def get_localized_text(self): from django.utils.translation import get_language if get_language() == "ar" and self.text_ar: return self.text_ar return self.text_en def get_localized_description(self): from django.utils.translation import get_language if get_language() == "ar" and self.description_ar: return self.description_ar return self.description_en class UserAcknowledgement(UUIDModel, TimeStampedModel): """ Records of user acknowledgements. Tracks when each user acknowledges specific items with digital signatures. """ # User user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="acknowledgements") # Checklist item checklist_item = models.ForeignKey( AcknowledgementChecklistItem, on_delete=models.CASCADE, related_name="user_acknowledgements" ) # Acknowledgement details is_acknowledged = models.BooleanField(default=True) acknowledged_at = models.DateTimeField(auto_now_add=True) # Digital signature signature = models.TextField(blank=True, help_text="Digital signature data (base64 encoded)") signature_ip = models.GenericIPAddressField(null=True, blank=True, help_text="IP address when signed") signature_user_agent = models.TextField(blank=True, help_text="User agent when signed") # PDF document pdf_file = models.FileField( upload_to="acknowledgements/pdfs/", null=True, blank=True, help_text="PDF document of signed acknowledgement" ) # Metadata metadata = models.JSONField(default=dict, blank=True, help_text="Additional metadata") class Meta: ordering = ["-acknowledged_at"] unique_together = [["user", "checklist_item"]] indexes = [ models.Index(fields=["user", "-acknowledged_at"]), models.Index(fields=["checklist_item", "-acknowledged_at"]), ] def __str__(self): return f"{self.user.email} - {self.checklist_item.text_en}" class UserProvisionalLog(UUIDModel, TimeStampedModel): """ Audit trail for provisional user lifecycle. Tracks all key events in the onboarding process. """ EVENT_TYPES = [ ("created", _("User Created")), ("invitation_sent", _("Invitation Sent")), ("invitation_resent", _("Invitation Resent")), ("wizard_started", _("Wizard Started")), ("step_completed", _("Wizard Step Completed")), ("wizard_completed", _("Wizard Completed")), ("user_activated", _("User Activated")), ("invitation_expired", _("Invitation Expired")), ] # User user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="provisional_logs") # Event details event_type = models.CharField(max_length=50, choices=EVENT_TYPES, db_index=True) description = models.TextField() # Context ip_address = models.GenericIPAddressField(null=True, blank=True) user_agent = models.TextField(blank=True) # Additional data metadata = models.JSONField(default=dict, blank=True, help_text="Additional event data") class Meta: ordering = ["-created_at"] indexes = [ models.Index(fields=["user", "-created_at"]), models.Index(fields=["event_type", "-created_at"]), ] def __str__(self): return f"{self.user.email} - {self.get_event_type_display()} - {self.created_at}" class Role(models.Model): """ Role model for managing predefined roles and their permissions. This is a helper model - actual role assignment uses Django Groups. """ ROLE_CHOICES = [ ("px_admin", _("PX Admin")), ("hospital_admin", _("Hospital Admin")), ("department_manager", _("Department Manager")), ("px_staff", _("PX Staff")), ("physician", _("Physician")), ("nurse", _("Nurse")), ("staff", _("Staff")), ("viewer", _("Viewer")), ("executive", _("Executive")), ] name = models.CharField(max_length=50, unique=True, choices=ROLE_CHOICES) display_name = models.CharField(max_length=100) description = models.TextField(blank=True) # Link to Django Group group = models.OneToOneField(Group, on_delete=models.CASCADE, related_name="role_config") # Permissions permissions = models.ManyToManyField(Permission, blank=True) # Hierarchy level (for escalation logic) level = models.IntegerField(default=0, help_text="Higher number = higher authority") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["-level", "name"] def __str__(self): return self.display_name # Import version models to ensure they are registered with Django from .version_models import ContentVersion, ChecklistItemVersion, ContentChangeLog # ============================================================================ # SIMPLE ACKNOWLEDGEMENT MODELS # ============================================================================ class SimpleAcknowledgement(UUIDModel, TimeStampedModel): """ Simple acknowledgement item that employees must sign. """ title = models.CharField(max_length=200, help_text="Acknowledgement title") description = models.TextField(blank=True, help_text="Detailed description") pdf_document = models.FileField( upload_to="acknowledgements/documents/", null=True, blank=True, help_text="PDF document for employees to review" ) # Status is_active = models.BooleanField(default=True, help_text="Show in employee checklist") is_required = models.BooleanField(default=True, help_text="Must be signed by all employees") # Display order order = models.IntegerField(default=0) class Meta: ordering = ["order", "title"] verbose_name = "Acknowledgement" verbose_name_plural = "Acknowledgements" def __str__(self): return self.title @property def signed_count(self): """Number of employees who have signed this acknowledgement.""" return self.employee_signatures.filter(is_signed=True).count() class EmployeeAcknowledgement(UUIDModel, TimeStampedModel): """ Records which employees have signed which acknowledgements. """ employee = models.ForeignKey( "accounts.User", on_delete=models.CASCADE, related_name="employee_acknowledgements", help_text="Employee who signed", ) acknowledgement = models.ForeignKey( SimpleAcknowledgement, on_delete=models.CASCADE, related_name="employee_signatures", help_text="Acknowledgement that was signed", ) # Sent tracking sent_at = models.DateTimeField(null=True, blank=True, help_text="When acknowledgement was sent to employee") sent_by = models.ForeignKey( "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="sent_acknowledgements", help_text="Admin who sent this acknowledgement", ) # Signature details is_signed = models.BooleanField(default=False) signed_at = models.DateTimeField(null=True, blank=True) signature_name = models.CharField(max_length=200, blank=True, help_text="Name used when signing") signature_employee_id = models.CharField(max_length=100, blank=True, help_text="Employee ID when signing") # Signed PDF signed_pdf = models.FileField( upload_to="acknowledgements/signed/%Y/%m/", null=True, blank=True, help_text="PDF with employee signature" ) # IP and user agent for audit ip_address = models.GenericIPAddressField(null=True, blank=True) user_agent = models.TextField(blank=True) # Notes notes = models.TextField(blank=True, help_text="Admin notes about this acknowledgement") class Meta: ordering = ["-sent_at", "-signed_at"] unique_together = [["employee", "acknowledgement"]] verbose_name = "Employee Acknowledgement" verbose_name_plural = "Employee Acknowledgements" def __str__(self): if self.is_signed: return f"✓ {self.employee.get_full_name()} - {self.acknowledgement.title}" elif self.sent_at: return f"→ {self.employee.get_full_name()} - {self.acknowledgement.title} (Sent)" return f"○ {self.employee.get_full_name()} - {self.acknowledgement.title}"