HH/apps/references/models.py

390 lines
12 KiB
Python

"""
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/<hospital_id>/YYYY/MM/DD/<uuid>_<filename>
"""
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}"