Marwan Alwali 263292f6be update
2025-11-04 00:50:06 +03:00

854 lines
23 KiB
Python

"""
Core app models for hospital management system.
Provides foundational infrastructure including tenant management,
audit logging, system configuration, and integration utilities.
"""
import uuid
from django.db import models
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.utils import timezone
from django.core.validators import RegexValidator
from django.conf import settings
class Tenant(models.Model):
"""
Multi-tenant support for hospital organizations.
Each tenant represents a separate hospital or healthcare organization.
"""
class OrganizationType(models.TextChoices):
HOSPITAL = 'HOSPITAL', 'Hospital'
CLINIC = 'CLINIC', 'Clinic'
HEALTH_SYSTEM = 'HEALTH_SYSTEM', 'Health System'
AMBULATORY = 'AMBULATORY', 'Ambulatory Care'
SPECIALTY = 'SPECIALTY', 'Specialty Practice'
URGENT_CARE = 'URGENT_CARE', 'Urgent Care'
REHABILITATION = 'REHABILITATION', 'Rehabilitation Center'
LONG_TERM_CARE = 'LONG_TERM_CARE', 'Long-term Care'
class SubscriptionPlan(models.TextChoices):
BASIC = 'BASIC', 'Basic'
STANDARD = 'STANDARD', 'Standard'
PREMIUM = 'PREMIUM', 'Premium'
ENTERPRISE = 'ENTERPRISE', 'Enterprise'
# Tenant Information
tenant_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique tenant identifier'
)
name = models.CharField(
max_length=200,
help_text='Organization name'
)
display_name = models.CharField(
max_length=200,
help_text='Display name for the organization'
)
description = models.TextField(
blank=True,
null=True,
help_text='Organization description'
)
# Organization Details
organization_type = models.CharField(
max_length=50,
choices=OrganizationType.choices,
default=OrganizationType.HOSPITAL
)
# Contact Information
address_line1 = models.CharField(
max_length=200,
help_text='Address line 1'
)
address_line2 = models.CharField(
max_length=200,
blank=True,
null=True,
help_text='Address line 2'
)
city = models.CharField(
max_length=100,
help_text='City'
)
state = models.CharField(
max_length=100,
help_text='State or province'
)
postal_code = models.CharField(
max_length=20,
help_text='Postal code'
)
country = models.CharField(
max_length=100,
default='Saudi Arabia',
help_text='Country'
)
# Contact Details
phone_number = models.CharField(
max_length=20,
validators=[RegexValidator(
regex=r'^\+?1?\d{9,15}$',
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.'
)],
help_text='Primary phone number'
)
email = models.EmailField(
help_text='Primary email address'
)
website = models.URLField(
blank=True,
null=True,
help_text='Organization website'
)
# Licensing and Accreditation
license_number = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Healthcare license number'
)
accreditation_body = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Accreditation body (e.g., Joint Commission)'
)
accreditation_number = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Accreditation number'
)
accreditation_expiry = models.DateField(
blank=True,
null=True,
help_text='Accreditation expiry date'
)
# Configuration
timezone = models.CharField(
max_length=50,
default='UTC',
help_text='Organization timezone'
)
locale = models.CharField(
max_length=10,
default='en-US',
help_text='Organization locale'
)
currency = models.CharField(
max_length=3,
default='SAR',
help_text='Organization currency code'
)
# Subscription and Billing
subscription_plan = models.CharField(
max_length=50,
choices=SubscriptionPlan.choices,
default=SubscriptionPlan.BASIC
)
max_users = models.PositiveIntegerField(
default=50,
help_text='Maximum number of users allowed'
)
max_patients = models.PositiveIntegerField(
default=1000,
help_text='Maximum number of patients allowed'
)
# Status
is_active = models.BooleanField(
default=True,
help_text='Tenant is active'
)
is_trial = models.BooleanField(
default=False,
help_text='Tenant is on trial'
)
trial_expires_at = models.DateTimeField(
blank=True,
null=True,
help_text='Trial expiration date'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'core_tenant'
verbose_name = 'Tenant'
verbose_name_plural = 'Tenants'
ordering = ['name']
def __str__(self):
return self.name
@property
def is_trial_expired(self):
"""Check if trial has expired."""
if not self.is_trial or not self.trial_expires_at:
return False
return timezone.now() > self.trial_expires_at
def get_user_count(self):
"""Get current user count for this tenant."""
User = get_user_model()
return User.objects.filter(tenant=self).count()
def get_patient_count(self):
"""Get current patient count for this tenant."""
from patients.models import PatientProfile
return PatientProfile.objects.filter(tenant=self).count()
class AuditLogEntry(models.Model):
"""
Comprehensive audit logging for HIPAA/GDPR compliance.
Tracks all user actions and system events.
"""
class RiskLevel(models.TextChoices):
LOW = 'LOW', 'Low'
MEDIUM = 'MEDIUM', 'Medium'
HIGH = 'HIGH', 'High'
CRITICAL = 'CRITICAL', 'Critical'
class EventType(models.TextChoices):
CREATE = 'CREATE', 'Create'
READ = 'READ', 'Read'
UPDATE = 'UPDATE', 'Update'
DELETE = 'DELETE', 'Delete'
LOGIN = 'LOGIN', 'Login'
LOGOUT = 'LOGOUT', 'Logout'
ACCESS = 'ACCESS', 'Access'
EXPORT = 'EXPORT', 'Export'
PRINT = 'PRINT', 'Print'
SHARE = 'SHARE', 'Share'
SYSTEM = 'SYSTEM', 'System Event'
ERROR = 'ERROR', 'Error'
SECURITY = 'SECURITY', 'Security Event'
class EventCategory(models.TextChoices):
AUTHENTICATION = 'AUTHENTICATION', 'Authentication'
AUTHORIZATION = 'AUTHORIZATION', 'Authorization'
DATA_ACCESS = 'DATA_ACCESS', 'Data Access'
DATA_MODIFICATION = 'DATA_MODIFICATION', 'Data Modification'
SYSTEM_ADMINISTRATION = 'SYSTEM_ADMINISTRATION', 'System Administration'
PATIENT_DATA = 'PATIENT_DATA', 'Patient Data'
CLINICAL_DATA = 'CLINICAL_DATA', 'Clinical Data'
FINANCIAL_DATA = 'FINANCIAL_DATA', 'Financial Data'
SECURITY = 'SECURITY', 'Security'
INTEGRATION = 'INTEGRATION', 'Integration'
REPORTING = 'REPORTING', 'Reporting'
# Tenant
tenant = models.ForeignKey(
Tenant,
on_delete=models.CASCADE,
related_name='audit_logs'
)
# Log Information
log_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique log identifier'
)
# Event Information
event_type = models.CharField(
max_length=50,
choices=EventType.choices,
)
event_category = models.CharField(
max_length=50,
choices=EventCategory.choices,
)
# User Information
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='audit_logs'
)
user_email = models.EmailField(
blank=True,
null=True,
help_text='User email at time of event'
)
user_role = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='User role at time of event'
)
# Session Information
session_key = models.CharField(
max_length=40,
blank=True,
null=True,
help_text='Session key'
)
ip_address = models.GenericIPAddressField(
blank=True,
null=True,
help_text='IP address'
)
user_agent = models.TextField(
blank=True,
null=True,
help_text='User agent string'
)
# Object Information
content_type = models.ForeignKey(
ContentType,
on_delete=models.SET_NULL,
null=True,
blank=True
)
object_id = models.PositiveIntegerField(
null=True,
blank=True
)
content_object = GenericForeignKey('content_type', 'object_id')
object_repr = models.CharField(
max_length=200,
blank=True,
null=True,
help_text='String representation of the object'
)
# Event Details
action = models.CharField(
max_length=200,
help_text='Action performed'
)
description = models.TextField(
help_text='Detailed description of the event'
)
# Data Changes
changes = models.JSONField(
default=dict,
help_text='Field changes (before/after values)'
)
additional_data = models.JSONField(
default=dict,
help_text='Additional event data'
)
# Patient Context
patient_id = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Patient identifier if applicable'
)
patient_mrn = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Patient MRN if applicable'
)
# Risk Assessment
risk_level = models.CharField(
max_length=20,
choices=RiskLevel.choices,
default=RiskLevel.LOW
)
# Compliance Flags
hipaa_relevant = models.BooleanField(
default=False,
help_text='Event is HIPAA relevant'
)
gdpr_relevant = models.BooleanField(
default=False,
help_text='Event is GDPR relevant'
)
# Status
is_successful = models.BooleanField(
default=True,
help_text='Event was successful'
)
error_message = models.TextField(
blank=True,
null=True,
help_text='Error message if event failed'
)
# Metadata
timestamp = models.DateTimeField(
default=timezone.now,
help_text='Event timestamp'
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'core_audit_log_entry'
verbose_name = 'Audit Log Entry'
verbose_name_plural = 'Audit Log Entries'
ordering = ['-timestamp']
indexes = [
models.Index(fields=['tenant', 'event_type', 'timestamp']),
models.Index(fields=['user', 'timestamp']),
models.Index(fields=['patient_mrn', 'timestamp']),
models.Index(fields=['content_type', 'object_id']),
models.Index(fields=['risk_level', 'timestamp']),
]
def __str__(self):
return f"{self.event_type} - {self.action} by {self.user_email or 'System'}"
class SystemConfiguration(models.Model):
"""
System configuration settings for tenant-specific and global configurations.
"""
class DataType(models.TextChoices):
STRING = 'STRING', 'String'
INTEGER = 'INTEGER', 'Integer'
FLOAT = 'FLOAT', 'Float'
BOOLEAN = 'BOOLEAN', 'Boolean'
JSON = 'JSON', 'JSON'
DATE = 'DATE', 'Date'
DATETIME = 'DATETIME', 'DateTime'
# Tenant (null for global configurations)
tenant = models.ForeignKey(
Tenant,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='configurations'
)
# Configuration Information
key = models.CharField(
max_length=200,
help_text='Configuration key'
)
value = models.TextField(
help_text='Configuration value'
)
data_type = models.CharField(
max_length=20,
choices=DataType.choices,
default=DataType.STRING
)
# Configuration Metadata
category = models.CharField(
max_length=100,
help_text='Configuration category'
)
description = models.TextField(
blank=True,
null=True,
help_text='Configuration description'
)
# Validation
validation_rules = models.JSONField(
default=dict,
help_text='Validation rules for the configuration value'
)
default_value = models.TextField(
blank=True,
null=True,
help_text='Default value'
)
# Access Control
is_sensitive = models.BooleanField(
default=False,
help_text='Configuration contains sensitive data'
)
is_encrypted = models.BooleanField(
default=False,
help_text='Configuration value is encrypted'
)
required_permission = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Permission required to modify this configuration'
)
# Status
is_active = models.BooleanField(
default=True,
help_text='Configuration is active'
)
is_readonly = models.BooleanField(
default=False,
help_text='Configuration is read-only'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
updated_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='updated_configurations'
)
class Meta:
db_table = 'core_system_configuration'
verbose_name = 'System Configuration'
verbose_name_plural = 'System Configurations'
unique_together = ['tenant', 'key']
ordering = ['category', 'key']
def __str__(self):
tenant_name = self.tenant.name if self.tenant else 'Global'
return f"{tenant_name} - {self.key}"
def get_typed_value(self):
"""Get the configuration value converted to its proper type."""
if self.data_type == 'INTEGER':
return int(self.value)
elif self.data_type == 'FLOAT':
return float(self.value)
elif self.data_type == 'BOOLEAN':
return self.value.lower() in ('true', '1', 'yes', 'on')
elif self.data_type == 'JSON':
import json
return json.loads(self.value)
elif self.data_type == 'DATE':
from datetime import datetime
return datetime.strptime(self.value, '%Y-%m-%d').date()
elif self.data_type == 'DATETIME':
from datetime import datetime
return datetime.fromisoformat(self.value)
else:
return self.value
class SystemNotification(models.Model):
"""
System-wide notifications and announcements.
"""
class TargetAudience(models.TextChoices):
ALL_USERS = 'ALL_USERS', 'All Users'
ADMINISTRATORS = 'ADMINISTRATORS', 'Administrators'
CLINICAL_STAFF = 'CLINICAL_STAFF', 'Clinical Staff'
SUPPORT_STAFF = 'SUPPORT_STAFF', 'Support Staff'
SPECIFIC_ROLES = 'SPECIFIC_ROLES', 'Specific Roles'
SPECIFIC_USERS = 'SPECIFIC_USERS', 'Specific Users'
class NotificationType(models.TextChoices):
INFO = 'INFO', 'Information'
WARNING = 'WARNING', 'Warning'
ERROR = 'ERROR', 'Error'
SUCCESS = 'SUCCESS', 'Success'
MAINTENANCE = 'MAINTENANCE', 'Maintenance'
SECURITY = 'SECURITY', 'Security Alert'
FEATURE = 'FEATURE', 'New Feature'
UPDATE = 'UPDATE', 'System Update'
class NotificationPriority(models.TextChoices):
LOW = 'LOW', 'Low'
MEDIUM = 'MEDIUM', 'Medium'
HIGH = 'HIGH', 'High'
URGENT = 'URGENT', 'Urgent'
# Tenant (null for global notifications)
tenant = models.ForeignKey(
Tenant,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='notifications'
)
# Notification Information
notification_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique notification identifier'
)
title = models.CharField(
max_length=200,
help_text='Notification title'
)
message = models.TextField(
help_text='Notification message'
)
# Notification Type
notification_type = models.CharField(
max_length=30,
choices=NotificationType.choices,
)
# Priority
priority = models.CharField(
max_length=20,
choices=NotificationPriority.choices,
default=NotificationPriority.MEDIUM
)
# Targeting
target_audience = models.CharField(
max_length=30,
choices=TargetAudience.choices,
default=TargetAudience.ALL_USERS
)
target_roles = models.JSONField(
default=list,
help_text='Target user roles (if target_audience is SPECIFIC_ROLES)'
)
target_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
related_name='targeted_notifications'
)
# Display Settings
is_dismissible = models.BooleanField(
default=True,
help_text='Users can dismiss this notification'
)
auto_dismiss_after = models.PositiveIntegerField(
blank=True,
null=True,
help_text='Auto-dismiss after X seconds'
)
show_on_login = models.BooleanField(
default=False,
help_text='Show notification on user login'
)
# Scheduling
start_date = models.DateTimeField(
default=timezone.now,
help_text='Notification start date'
)
end_date = models.DateTimeField(
blank=True,
null=True,
help_text='Notification end date'
)
# Actions
action_url = models.URLField(
blank=True,
null=True,
help_text='Action URL for the notification'
)
action_text = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Action button text'
)
# Status
is_active = models.BooleanField(
default=True,
help_text='Notification is active'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name='created_notifications'
)
class Meta:
db_table = 'core_system_notification'
verbose_name = 'System Notification'
verbose_name_plural = 'System Notifications'
ordering = ['-priority', '-created_at']
def __str__(self):
return self.title
@property
def is_visible(self):
"""Check if notification should be visible now."""
now = timezone.now()
if not self.is_active:
return False
if now < self.start_date:
return False
if self.end_date and now > self.end_date:
return False
return True
class IntegrationLog(models.Model):
"""
Integration logging for external system communications.
"""
class IntegrationStatus(models.TextChoices):
SUCCESS = 'SUCCESS', 'Success'
FAILED = 'FAILED', 'Failed'
PENDING = 'PENDING', 'Pending'
TIMEOUT = 'TIMEOUT', 'Timeout'
RETRY = 'RETRY', 'Retry'
class IntegrationType(models.TextChoices):
HL7 = 'HL7', 'HL7 Message'
DICOM = 'DICOM', 'DICOM Communication'
API = 'API', 'API Call'
DATABASE = 'DATABASE', 'Database Sync'
FILE_TRANSFER = 'FILE_TRANSFER', 'File Transfer'
WEBHOOK = 'WEBHOOK', 'Webhook'
EMAIL = 'EMAIL', 'Email'
SMS = 'SMS', 'SMS'
# Tenant
tenant = models.ForeignKey(
Tenant,
on_delete=models.CASCADE,
related_name='integration_logs'
)
# Log Information
log_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique log identifier'
)
# Integration Information
integration_type = models.CharField(
max_length=30,
choices=IntegrationType.choices,
)
direction = models.CharField(
max_length=10,
choices=[
('INBOUND', 'Inbound'),
('OUTBOUND', 'Outbound'),
]
)
# External System
external_system = models.CharField(
max_length=200,
help_text='External system name'
)
endpoint = models.CharField(
max_length=500,
blank=True,
null=True,
help_text='Integration endpoint'
)
# Message Information
message_type = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Message type (e.g., HL7 message type)'
)
message_id = models.CharField(
max_length=200,
blank=True,
null=True,
help_text='Message identifier'
)
correlation_id = models.UUIDField(
blank=True,
null=True,
help_text='Correlation ID for tracking related messages'
)
# Content
request_data = models.TextField(
blank=True,
null=True,
help_text='Request data sent'
)
response_data = models.TextField(
blank=True,
null=True,
help_text='Response data received'
)
# Status
status = models.CharField(
max_length=20,
choices=IntegrationStatus.choices,
)
# Error Information
error_code = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Error code'
)
error_message = models.TextField(
blank=True,
null=True,
help_text='Error message'
)
# Performance
processing_time_ms = models.PositiveIntegerField(
blank=True,
null=True,
help_text='Processing time in milliseconds'
)
# Metadata
timestamp = models.DateTimeField(
default=timezone.now,
help_text='Log timestamp'
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'core_integration_log'
verbose_name = 'Integration Log'
verbose_name_plural = 'Integration Logs'
ordering = ['-timestamp']
indexes = [
models.Index(fields=['tenant', 'integration_type', 'timestamp']),
models.Index(fields=['external_system', 'status']),
models.Index(fields=['correlation_id']),
]
def __str__(self):
return f"{self.integration_type} - {self.external_system} - {self.status}"