"""
Django admin configuration for core app.
"""
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext_lazy as _
from django.utils.html import format_html
from django.utils import timezone
from simple_history.admin import SimpleHistoryAdmin
from .models import (
Tenant,
User,
Patient,
Clinic,
File,
SubFile,
Consent,
ConsentTemplate,
ConsentToken,
Attachment,
AuditLog,
SettingTemplate,
TenantSetting,
ContactMessage,
)
from .settings_service import get_tenant_settings_service
class TenantSettingInline(admin.TabularInline):
"""Inline admin for tenant settings."""
model = TenantSetting
extra = 0
fields = ['template', 'get_setting_info', 'value', 'file_value', 'updated_by', 'updated_at']
readonly_fields = ['get_setting_info', 'updated_by', 'updated_at']
def get_setting_info(self, obj):
"""Display setting information."""
if obj.template:
required_badge = ''
if obj.template.is_required:
has_value = bool(obj.value or obj.file_value or obj.encrypted_value)
if has_value:
required_badge = format_html(
'✓ Required'
)
else:
required_badge = format_html(
'✗ Required (Missing)'
)
help_text = obj.template.help_text_en if obj.template.help_text_en else ''
return format_html(
'{}
'
'{}
'
'{}
'
'{}',
obj.template.label_en,
obj.template.get_category_display(),
help_text,
required_badge
)
return '-'
get_setting_info.short_description = _('Setting Information')
def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""Filter templates by category for better organization."""
if db_field.name == "template":
kwargs["queryset"] = SettingTemplate.objects.filter(is_active=True).order_by('category', 'order')
return super().formfield_for_foreignkey(db_field, request, **kwargs)
@admin.register(Tenant)
class TenantAdmin(admin.ModelAdmin):
"""Admin interface for Tenant model with settings management."""
list_display = ['name', 'code', 'is_active', 'settings_status', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['name', 'code', 'name_ar']
readonly_fields = ['id', 'created_at', 'updated_at', 'settings_status_detail']
inlines = [TenantSettingInline]
fieldsets = (
(None, {
'fields': ('name', 'name_ar', 'code', 'is_active')
}),
(_('ZATCA Information'), {
'fields': ('vat_number', 'address', 'city', 'postal_code', 'country_code'),
}),
(_('Settings Status'), {
'fields': ('settings_status_detail',),
}),
(_('Legacy Settings (Deprecated)'), {
'fields': ('settings',),
'classes': ('collapse',)
}),
(_('Metadata'), {
'fields': ('id', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def settings_status(self, obj):
"""Display settings completion status."""
service = get_tenant_settings_service(obj)
missing = service.get_missing_required_settings()
if not missing:
return format_html(
'✓ Complete'
)
else:
return format_html(
'✗ {} Missing',
len(missing)
)
settings_status.short_description = _('Settings Status')
def settings_status_detail(self, obj):
"""Display detailed settings status."""
service = get_tenant_settings_service(obj)
missing = service.get_missing_required_settings()
if not missing:
return format_html(
'
'
'✓ All required settings are configured'
'
'
)
else:
missing_list = ''
for template in missing:
missing_list += f'- {template.label_en} ({template.get_category_display()})
'
missing_list += '
'
return format_html(
''
'✗ Missing Required Settings:'
'{}'
'
',
missing_list
)
settings_status_detail.short_description = _('Settings Status')
@admin.register(User)
class UserAdmin(SimpleHistoryAdmin, BaseUserAdmin):
"""Admin interface for User model."""
list_display = ['username', 'email', 'first_name', 'last_name', 'role', 'tenant', 'is_active']
list_filter = ['role', 'is_active', 'is_staff', 'tenant', 'date_joined']
search_fields = ['username', 'email', 'first_name', 'last_name', 'employee_id']
readonly_fields = ['id', 'date_joined', 'last_login']
fieldsets = (
(None, {
'fields': ('username', 'password')
}),
(_('Personal Info'), {
'fields': ('first_name', 'last_name', 'email', 'phone_number', 'employee_id')
}),
(_('Organization'), {
'fields': ('tenant', 'role')
}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
'classes': ('collapse',)
}),
(_('Important Dates'), {
'fields': ('last_login', 'date_joined'),
'classes': ('collapse',)
}),
(_('Metadata'), {
'fields': ('id',),
'classes': ('collapse',)
}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'password1', 'password2', 'tenant', 'role'),
}),
)
@admin.register(Patient)
class PatientAdmin(SimpleHistoryAdmin):
"""Admin interface for Patient model."""
list_display = ['mrn', 'full_name_en', 'date_of_birth', 'sex', 'phone', 'tenant', 'created_at']
list_filter = ['sex', 'tenant', 'created_at']
search_fields = ['mrn', 'national_id', 'first_name_en', 'last_name_en', 'first_name_ar', 'last_name_ar', 'phone', 'email']
readonly_fields = ['id', 'mrn', 'age', 'created_at', 'updated_at']
fieldsets = (
(_('Identification'), {
'fields': ('mrn', 'national_id', 'tenant')
}),
(_('Personal Information (English)'), {
'fields': ('first_name_en','father_name_en','grandfather_name_en', 'last_name_en')
}),
(_('Personal Information (Arabic)'), {
'fields': ('first_name_ar','father_name_ar','grandfather_name_ar', 'last_name_ar'),
'classes': ('collapse',)
}),
(_('Demographics'), {
'fields': ('date_of_birth', 'age', 'sex')
}),
(_('Contact Information'), {
'fields': ('phone', 'email', 'address', 'city', 'postal_code')
}),
(_('Caregiver Information'), {
'fields': ('caregiver_name', 'caregiver_phone', 'caregiver_relationship'),
'classes': ('collapse',)
}),
(_('Emergency Contact'), {
'fields': ('emergency_contact',),
'classes': ('collapse',)
}),
(_('Metadata'), {
'fields': ('id', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@admin.register(Clinic)
class ClinicAdmin(admin.ModelAdmin):
"""Admin interface for Clinic model."""
list_display = ['name_en', 'specialty', 'code', 'tenant', 'is_active']
list_filter = ['specialty', 'is_active', 'tenant']
search_fields = ['name_en', 'name_ar', 'code']
readonly_fields = ['id', 'created_at', 'updated_at']
fieldsets = (
(None, {
'fields': ('name_en', 'name_ar', 'specialty', 'code', 'tenant', 'is_active')
}),
(_('Metadata'), {
'fields': ('id', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@admin.register(File)
class FileAdmin(admin.ModelAdmin):
"""Admin interface for File model."""
list_display = ['file_number', 'patient', 'status', 'opened_date', 'tenant']
list_filter = ['status', 'tenant', 'opened_date']
search_fields = ['file_number', 'patient__mrn', 'patient__first_name_en', 'patient__last_name_en']
readonly_fields = ['id', 'file_number', 'opened_date']
fieldsets = (
(None, {
'fields': ('file_number', 'patient', 'tenant', 'status')
}),
(_('Dates'), {
'fields': ('opened_date',)
}),
(_('Metadata'), {
'fields': ('id',),
'classes': ('collapse',)
}),
)
@admin.register(SubFile)
class SubFileAdmin(admin.ModelAdmin):
"""Admin interface for SubFile model."""
list_display = ['sub_file_number', 'file', 'clinic', 'status', 'assigned_provider', 'opened_date']
list_filter = ['status', 'clinic', 'opened_date']
search_fields = ['sub_file_number', 'file__file_number', 'file__patient__mrn']
readonly_fields = ['id', 'opened_date', 'created_at', 'updated_at']
fieldsets = (
(None, {
'fields': ('sub_file_number', 'file', 'clinic', 'status', 'assigned_provider')
}),
(_('Dates'), {
'fields': ('opened_date', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
(_('Metadata'), {
'fields': ('id',),
'classes': ('collapse',)
}),
)
@admin.register(ConsentTemplate)
class ConsentTemplateAdmin(admin.ModelAdmin):
"""Admin interface for Consent Template model."""
list_display = ['consent_type', 'title_en', 'version', 'is_active', 'tenant', 'created_at']
list_filter = ['consent_type', 'is_active', 'tenant', 'version']
search_fields = ['title_en', 'title_ar', 'content_en', 'content_ar']
readonly_fields = ['id', 'created_at', 'updated_at']
fieldsets = (
(None, {
'fields': ('tenant', 'consent_type', 'version', 'is_active')
}),
(_('Titles'), {
'fields': ('title_en', 'title_ar')
}),
(_('Content (English)'), {
'fields': ('content_en',),
'description': 'Use placeholders: {patient_name}, {patient_mrn}, {date}, {patient_dob}, {patient_age}'
}),
(_('Content (Arabic)'), {
'fields': ('content_ar',),
'classes': ('collapse',),
'description': 'Use placeholders: {patient_name}, {patient_mrn}, {date}, {patient_dob}, {patient_age}'
}),
(_('Metadata'), {
'fields': ('id', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@admin.register(Consent)
class ConsentAdmin(SimpleHistoryAdmin):
"""Admin interface for Consent model."""
list_display = ['patient', 'consent_type', 'signed_by_name', 'signed_at', 'is_active', 'tenant']
list_filter = ['consent_type', 'is_active', 'tenant', 'signed_at', 'created_at']
search_fields = ['patient__mrn', 'patient__first_name_en', 'patient__last_name_en', 'signed_by_name']
readonly_fields = ['id', 'signature_hash', 'created_at', 'updated_at']
fieldsets = (
(None, {
'fields': ('patient', 'tenant', 'consent_type', 'is_active', 'version')
}),
(_('Content'), {
'fields': ('content_text',)
}),
(_('Signature Information'), {
'fields': ('signed_by_name', 'signed_by_relationship', 'signed_at', 'signature_method',
'signature_image', 'signature_hash')
}),
(_('Technical Details'), {
'fields': ('signed_ip', 'signed_user_agent'),
'classes': ('collapse',)
}),
(_('Metadata'), {
'fields': ('id', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@admin.register(ConsentToken)
class ConsentTokenAdmin(admin.ModelAdmin):
"""Admin interface for Consent Token model."""
list_display = ['consent', 'email', 'is_valid_status', 'sent_by', 'created_at', 'expires_at', 'used_at']
list_filter = ['is_active', 'created_at', 'expires_at', 'used_at']
search_fields = ['email', 'consent__patient__first_name_en', 'consent__patient__last_name_en', 'consent__patient__mrn']
readonly_fields = ['id', 'token', 'created_at', 'updated_at', 'is_valid_status']
date_hierarchy = 'created_at'
fieldsets = (
(None, {
'fields': ('consent', 'email', 'token', 'is_active')
}),
(_('Timing'), {
'fields': ('created_at', 'expires_at', 'used_at')
}),
(_('Audit'), {
'fields': ('sent_by', 'is_valid_status'),
}),
(_('Metadata'), {
'fields': ('id', 'updated_at'),
'classes': ('collapse',)
}),
)
def is_valid_status(self, obj):
"""Display if token is currently valid."""
if obj.is_valid():
return format_html(
'✓ Valid'
)
elif obj.used_at:
return format_html(
'Used on {}',
obj.used_at.strftime('%Y-%m-%d %H:%M')
)
elif obj.expires_at < timezone.now():
return format_html(
'Expired'
)
else:
return format_html(
'Inactive'
)
is_valid_status.short_description = _('Status')
def has_add_permission(self, request):
"""Tokens should be created via the system, not manually."""
return False
@admin.register(Attachment)
class AttachmentAdmin(admin.ModelAdmin):
"""Admin interface for Attachment model."""
list_display = ['file', 'file_type', 'content_type', 'uploaded_by', 'tenant', 'created_at']
list_filter = ['file_type', 'content_type', 'tenant', 'created_at']
search_fields = ['description', 'file']
readonly_fields = ['id', 'created_at', 'updated_at']
fieldsets = (
(None, {
'fields': ('content_type', 'object_id', 'tenant')
}),
(_('File Information'), {
'fields': ('file', 'file_type', 'description', 'uploaded_by')
}),
(_('Metadata'), {
'fields': ('id', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@admin.register(AuditLog)
class AuditLogAdmin(admin.ModelAdmin):
"""Admin interface for AuditLog model."""
list_display = ['user', 'action', 'content_type', 'timestamp', 'tenant']
list_filter = ['action', 'content_type', 'tenant', 'timestamp']
search_fields = ['user__username', 'user__email']
readonly_fields = ['id', 'timestamp', 'changes', 'ip_address']
fieldsets = (
(None, {
'fields': ('user', 'action', 'content_type', 'object_id', 'tenant')
}),
(_('Details'), {
'fields': ('timestamp', 'changes', 'ip_address')
}),
(_('Metadata'), {
'fields': ('id',),
'classes': ('collapse',)
}),
)
def has_add_permission(self, request):
"""Audit logs should not be manually created."""
return False
def has_change_permission(self, request, obj=None):
"""Audit logs should not be modified."""
return False
def has_delete_permission(self, request, obj=None):
"""Audit logs should not be deleted."""
return False
@admin.register(SettingTemplate)
class SettingTemplateAdmin(admin.ModelAdmin):
"""Admin interface for Setting Template model."""
list_display = ['key', 'label_en', 'category', 'data_type', 'is_required', 'is_active', 'order']
list_filter = ['category', 'data_type', 'is_required', 'is_active']
search_fields = ['key', 'label_en', 'label_ar', 'help_text_en']
readonly_fields = ['id', 'created_at', 'updated_at']
list_editable = ['order', 'is_active']
fieldsets = (
(None, {
'fields': ('key', 'category', 'data_type', 'is_required', 'is_active', 'order')
}),
(_('Labels'), {
'fields': ('label_en', 'label_ar')
}),
(_('Help Text'), {
'fields': ('help_text_en', 'help_text_ar')
}),
(_('Validation'), {
'fields': ('default_value', 'validation_regex', 'choices'),
'classes': ('collapse',)
}),
(_('Metadata'), {
'fields': ('id', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def get_queryset(self, request):
"""Order by category and order by default."""
qs = super().get_queryset(request)
return qs.order_by('category', 'order', 'label_en')
@admin.register(TenantSetting)
class TenantSettingAdmin(SimpleHistoryAdmin):
"""Admin interface for Tenant Setting model."""
list_display = ['tenant', 'get_template_label', 'get_category', 'get_value_preview', 'updated_by', 'updated_at']
list_filter = ['template__category', 'tenant', 'updated_at']
search_fields = ['tenant__name', 'template__label_en', 'template__key', 'value']
readonly_fields = ['id', 'created_at', 'updated_at', 'get_value_preview']
fieldsets = (
(None, {
'fields': ('tenant', 'template')
}),
(_('Value'), {
'fields': ('value', 'file_value', 'get_value_preview')
}),
(_('Audit'), {
'fields': ('updated_by', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
(_('Metadata'), {
'fields': ('id',),
'classes': ('collapse',)
}),
)
def get_template_label(self, obj):
"""Display template label."""
return obj.template.label_en
get_template_label.short_description = _('Setting')
get_template_label.admin_order_field = 'template__label_en'
def get_category(self, obj):
"""Display category."""
return obj.template.get_category_display()
get_category.short_description = _('Category')
get_category.admin_order_field = 'template__category'
def get_value_preview(self, obj):
"""Display value preview (masked for encrypted)."""
if obj.template.data_type == SettingTemplate.DataType.ENCRYPTED:
if obj.encrypted_value:
return format_html('***ENCRYPTED***')
return '-'
elif obj.template.data_type == SettingTemplate.DataType.FILE:
if obj.file_value:
return format_html('View File', obj.file_value.url)
return '-'
else:
value = obj.value[:100] if obj.value else '-'
return value
get_value_preview.short_description = _('Value Preview')
@admin.register(ContactMessage)
class ContactMessageAdmin(admin.ModelAdmin):
"""Admin interface for Contact Message model."""
list_display = ['name', 'email', 'phone', 'get_message_preview', 'submitted_at', 'status_badge', 'responded_by']
list_filter = ['is_read', 'submitted_at', 'responded_at']
search_fields = ['name', 'email', 'phone', 'message', 'ip_address']
readonly_fields = ['id', 'submitted_at', 'ip_address', 'user_agent', 'created_at', 'updated_at']
date_hierarchy = 'submitted_at'
actions = ['mark_as_read', 'mark_as_unread']
fieldsets = (
(_('Contact Information'), {
'fields': ('name', 'email', 'phone')
}),
(_('Message'), {
'fields': ('message',)
}),
(_('Status'), {
'fields': ('is_read', 'responded_at', 'responded_by', 'notes')
}),
(_('Technical Details'), {
'fields': ('submitted_at', 'ip_address', 'user_agent'),
'classes': ('collapse',)
}),
(_('Metadata'), {
'fields': ('id', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def get_message_preview(self, obj):
"""Display message preview (first 100 characters)."""
if len(obj.message) > 100:
return f"{obj.message[:100]}..."
return obj.message
get_message_preview.short_description = _('Message Preview')
def status_badge(self, obj):
"""Display read/unread status with badge."""
if obj.responded_at:
return format_html(
'✓ Responded'
)
elif obj.is_read:
return format_html(
'👁 Read'
)
else:
return format_html(
'● New'
)
status_badge.short_description = _('Status')
def mark_as_read(self, request, queryset):
"""Mark selected messages as read."""
updated = queryset.update(is_read=True)
self.message_user(
request,
f'{updated} message(s) marked as read.'
)
mark_as_read.short_description = _('Mark selected as read')
def mark_as_unread(self, request, queryset):
"""Mark selected messages as unread."""
updated = queryset.update(is_read=False)
self.message_user(
request,
f'{updated} message(s) marked as unread.'
)
mark_as_unread.short_description = _('Mark selected as unread')
def save_model(self, request, obj, form, change):
"""Auto-mark as read when admin views/edits."""
if change and not obj.is_read:
obj.mark_as_read()
super().save_model(request, obj, form, change)
def has_add_permission(self, request):
"""Contact messages should only be created via the form."""
return False
def get_queryset(self, request):
"""Order by unread first, then by date."""
qs = super().get_queryset(request)
return qs.order_by('is_read', '-submitted_at')