""" 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 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, db_index=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, db_index=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' ) # 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=['email']), models.Index(fields=['employee_id']), 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 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 AcknowledgementContent(UUIDModel, TimeStampedModel): """ Acknowledgement content sections for onboarding wizard. Provides bilingual, role-specific educational content. """ ROLE_CHOICES = [ ('px_admin', 'PX Admin'), ('hospital_admin', 'Hospital Admin'), ('department_manager', 'Department Manager'), ('px_coordinator', 'PX Coordinator'), ('physician', 'Physician'), ('nurse', 'Nurse'), ('staff', 'Staff'), ('viewer', 'Viewer'), ] # Target role (leave blank for all roles) role = models.CharField( max_length=50, choices=ROLE_CHOICES, null=True, blank=True, help_text="Target role for this content" ) # 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 in wizard" ) # Status is_active = models.BooleanField(default=True) class Meta: ordering = ['role', 'order', 'code'] indexes = [ models.Index(fields=['role', 'is_active', 'order']), models.Index(fields=['code']), ] def __str__(self): role_text = self.get_role_display() if self.role else "All Roles" return f"{role_text} - {self.title_en}" class AcknowledgementChecklistItem(UUIDModel, TimeStampedModel): """ Checklist items that users must acknowledge during onboarding. Can be role-specific and linked to content sections. """ ROLE_CHOICES = [ ('px_admin', 'PX Admin'), ('hospital_admin', 'Hospital Admin'), ('department_manager', 'Department Manager'), ('px_coordinator', 'PX Coordinator'), ('physician', 'Physician'), ('nurse', 'Nurse'), ('staff', 'Staff'), ('viewer', 'Viewer'), ] # Target role (leave blank for all roles) role = models.CharField( max_length=50, choices=ROLE_CHOICES, null=True, blank=True, help_text="Target role for this item" ) # 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 = ['role', 'order', 'code'] indexes = [ models.Index(fields=['role', 'is_active', 'order']), models.Index(fields=['code']), ] def __str__(self): role_text = self.get_role_display() if self.role else "All Roles" return f"{role_text} - {self.text_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" ) # 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_coordinator', 'PX Coordinator'), ('physician', 'Physician'), ('nurse', 'Nurse'), ('staff', 'Staff'), ('viewer', 'Viewer'), ] 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