""" References models - Reference Section for document management This module implements a file server system for managing reference documents with folder categorization, version control, and role-based access control. """ import os from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.db import models from apps.core.models import TenantModel, TimeStampedModel, UUIDModel, SoftDeleteModel User = get_user_model() def document_upload_path(instance, filename): """ Generate upload path for reference documents. Format: references//YYYY/MM/DD/_ """ hospital_id = instance.hospital.id if instance.hospital else 'default' from django.utils import timezone date_str = timezone.now().strftime('%Y/%m/%d') # Get file extension ext = os.path.splitext(filename)[1] return f'references/{hospital_id}/{date_str}/{instance.id}{ext}' class ReferenceFolder(UUIDModel, TimeStampedModel, SoftDeleteModel, TenantModel): """ Reference Folder model for organizing documents into hierarchical folders. Features: - Hospital-specific folders (via TenantModel) - Nested folder hierarchy via self-referential parent field - Bilingual support (English/Arabic) - Role-based access control - Soft delete support """ # Bilingual folder name name = models.CharField(max_length=200, db_index=True) name_ar = models.CharField(max_length=200, blank=True, verbose_name="Name (Arabic)") # Description description = models.TextField(blank=True) description_ar = models.TextField(blank=True, verbose_name="Description (Arabic)") # Hierarchy - self-referential for nested folders parent = models.ForeignKey( 'self', on_delete=models.CASCADE, null=True, blank=True, related_name='subfolders', help_text="Parent folder for nested structure" ) # UI customization icon = models.CharField( max_length=50, blank=True, help_text="Icon class (e.g., 'fa-folder', 'fa-file-pdf')" ) color = models.CharField( max_length=7, blank=True, help_text="Hex color code (e.g., '#007bff')" ) # Ordering order = models.IntegerField( default=0, help_text="Display order within parent folder" ) # Role-based access control access_roles = models.ManyToManyField( 'auth.Group', blank=True, related_name='accessible_folders', help_text="Roles that can access this folder (empty = all roles)" ) # Status is_active = models.BooleanField(default=True, db_index=True) class Meta: ordering = ['parent__order', 'order', 'name'] verbose_name = 'Reference Folder' verbose_name_plural = 'Reference Folders' indexes = [ models.Index(fields=['hospital', 'parent', 'order']), models.Index(fields=['hospital', 'is_active']), ] def __str__(self): hospital_name = self.hospital.name if self.hospital else 'No Hospital' return f"{hospital_name} - {self.name}" def get_full_path(self): """Get full folder path as a list of folder names""" path = [self.name] parent = self.parent while parent: path.insert(0, parent.name) parent = parent.parent return ' / '.join(path) def has_access(self, user): """ Check if user has access to this folder. Returns True if: - Folder has no access_roles (public to all) - User belongs to any of the access_roles """ if not self.access_roles.exists(): return True return user.groups.filter(id__in=self.access_roles.all()).exists() def get_subfolders(self): """Get all immediate subfolders""" return self.subfolders.filter(is_active=True, is_deleted=False) def get_documents(self): """Get all documents in this folder""" return self.documents.filter(is_published=True, is_deleted=False) def get_document_count(self): """Get count of documents in this folder (including subfolders)""" count = self.get_documents().count() for subfolder in self.get_subfolders(): count += subfolder.get_document_count() return count class ReferenceDocument(UUIDModel, TimeStampedModel, SoftDeleteModel, TenantModel): """ Reference Document model for storing and managing documents. Features: - Multi-version support - Multiple file types (PDF, Word, Excel, etc.) - Role-based access control - Download tracking - Bilingual support - Soft delete support """ # Folder association folder = models.ForeignKey( ReferenceFolder, on_delete=models.CASCADE, related_name='documents', null=True, blank=True, help_text="Folder containing this document" ) # Bilingual title title = models.CharField(max_length=500, db_index=True) title_ar = models.CharField(max_length=500, blank=True, verbose_name="Title (Arabic)") # File information file = models.FileField(upload_to=document_upload_path, max_length=500) filename = models.CharField(max_length=500, help_text="Original filename") file_type = models.CharField( max_length=50, blank=True, help_text="File extension/type (e.g., pdf, docx, xlsx)" ) file_size = models.IntegerField(help_text="File size in bytes") # Description description = models.TextField(blank=True) description_ar = models.TextField(blank=True, verbose_name="Description (Arabic)") # Versioning version = models.CharField( max_length=20, default='1.0', help_text="Document version (e.g., 1.0, 1.1, 2.0)" ) is_latest_version = models.BooleanField( default=True, db_index=True, help_text="Is this the latest version?" ) parent_document = models.ForeignKey( 'self', on_delete=models.SET_NULL, null=True, blank=True, related_name='versions', help_text="Previous version of this document" ) # Upload tracking uploaded_by = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, related_name='uploaded_documents' ) # Usage tracking download_count = models.IntegerField(default=0, help_text="Number of downloads") last_accessed_at = models.DateTimeField(null=True, blank=True) # Visibility is_published = models.BooleanField( default=True, db_index=True, help_text="Is this document visible to users?" ) # Role-based access control access_roles = models.ManyToManyField( 'auth.Group', blank=True, related_name='accessible_documents', help_text="Roles that can access this document (empty = all roles)" ) # Tags for search tags = models.CharField( max_length=500, blank=True, help_text="Comma-separated tags for search (e.g., policy, procedure, handbook)" ) # Additional metadata metadata = models.JSONField(default=dict, blank=True) class Meta: ordering = ['title', '-created_at'] verbose_name = 'Reference Document' verbose_name_plural = 'Reference Documents' indexes = [ models.Index(fields=['hospital', 'folder', 'is_latest_version']), models.Index(fields=['hospital', 'is_published']), models.Index(fields=['folder', 'title']), ] def __str__(self): title = self.title or self.filename version = f" v{self.version}" if self.version else "" return f"{title}{version}" def clean(self): """Validate file type""" if self.file: ext = os.path.splitext(self.file.name)[1].lower().lstrip('.') # Allowed file types - can be extended as needed allowed_types = [ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf', 'csv', 'jpg', 'jpeg', 'png', 'gif' ] if ext not in allowed_types: raise ValidationError({ 'file': f'File type "{ext}" is not allowed. Allowed types: {", ".join(allowed_types)}' }) def save(self, *args, **kwargs): """Auto-populate file_type and file_size""" if self.file: # Extract file extension ext = os.path.splitext(self.file.name)[1].lower().lstrip('.') self.file_type = ext # Get file size if not already set if not self.file_size and hasattr(self.file, 'size'): self.file_size = self.file.size super().save(*args, **kwargs) def has_access(self, user): """ Check if user has access to this document. Returns True if: - Document has no access_roles (public to all) - User belongs to any of the access_roles """ if not self.access_roles.exists(): return True return user.groups.filter(id__in=self.access_roles.all()).exists() def increment_download_count(self): """Increment download count and update last_accessed_at""" self.download_count += 1 from django.utils import timezone self.last_accessed_at = timezone.now() self.save(update_fields=['download_count', 'last_accessed_at']) def get_tags_list(self): """Get tags as a list""" return [tag.strip() for tag in self.tags.split(',') if tag.strip()] def get_file_icon(self): """Get icon class based on file type""" icon_map = { 'pdf': 'fa-file-pdf text-danger', 'doc': 'fa-file-word text-primary', 'docx': 'fa-file-word text-primary', 'xls': 'fa-file-excel text-success', 'xlsx': 'fa-file-excel text-success', 'ppt': 'fa-file-powerpoint text-warning', 'pptx': 'fa-file-powerpoint text-warning', 'jpg': 'fa-file-image text-info', 'jpeg': 'fa-file-image text-info', 'png': 'fa-file-image text-info', 'gif': 'fa-file-image text-info', 'txt': 'fa-file-lines text-secondary', } return icon_map.get(self.file_type.lower(), 'fa-file text-secondary') def get_file_size_display(self): """Get human-readable file size""" size = self.file_size for unit in ['B', 'KB', 'MB', 'GB']: if size < 1024: return f"{size:.2f} {unit}" size /= 1024 return f"{size:.2f} TB" def get_version_history(self): """Get all versions of this document""" versions = list(self.versions.all()) versions.append(self) return sorted(versions, key=lambda x: x.version, reverse=True) class ReferenceDocumentAccess(UUIDModel, TimeStampedModel): """ Reference Document Access model for tracking document access. Features: - Audit trail for document downloads/views - Track user actions on documents - IP address logging """ ACTION_CHOICES = [ ('view', 'Viewed'), ('download', 'Downloaded'), ('preview', 'Previewed'), ] # Document reference document = models.ForeignKey( ReferenceDocument, on_delete=models.CASCADE, related_name='access_logs' ) # User who accessed the document user = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, related_name='document_accesses' ) # Action type action = models.CharField( max_length=20, choices=ACTION_CHOICES, db_index=True ) # Context information ip_address = models.GenericIPAddressField(null=True, blank=True) user_agent = models.TextField(blank=True) class Meta: ordering = ['-created_at'] verbose_name = 'Document Access Log' verbose_name_plural = 'Document Access Logs' indexes = [ models.Index(fields=['document', '-created_at']), models.Index(fields=['user', '-created_at']), models.Index(fields=['action', '-created_at']), ] def __str__(self): user_str = self.user.email if self.user else 'Anonymous' return f"{user_str} - {self.get_action_display()} - {self.document}"