658 lines
23 KiB
Python
658 lines
23 KiB
Python
"""
|
|
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(
|
|
'<span style="color: green; font-weight: bold;">✓ Required</span>'
|
|
)
|
|
else:
|
|
required_badge = format_html(
|
|
'<span style="color: red; font-weight: bold;">✗ Required (Missing)</span>'
|
|
)
|
|
|
|
help_text = obj.template.help_text_en if obj.template.help_text_en else ''
|
|
|
|
return format_html(
|
|
'<strong>{}</strong><br/>'
|
|
'<small style="color: #666;">{}</small><br/>'
|
|
'<small style="color: #999;">{}</small><br/>'
|
|
'{}',
|
|
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(
|
|
'<span style="color: green; font-weight: bold;">✓ Complete</span>'
|
|
)
|
|
else:
|
|
return format_html(
|
|
'<span style="color: red; font-weight: bold;">✗ {} Missing</span>',
|
|
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(
|
|
'<div style="padding: 10px; background-color: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px;">'
|
|
'<strong style="color: #155724;">✓ All required settings are configured</strong>'
|
|
'</div>'
|
|
)
|
|
else:
|
|
missing_list = '<ul>'
|
|
for template in missing:
|
|
missing_list += f'<li><strong>{template.label_en}</strong> ({template.get_category_display()})</li>'
|
|
missing_list += '</ul>'
|
|
|
|
return format_html(
|
|
'<div style="padding: 10px; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;">'
|
|
'<strong style="color: #721c24;">✗ Missing Required Settings:</strong>'
|
|
'{}'
|
|
'</div>',
|
|
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(
|
|
'<span style="color: green; font-weight: bold;">✓ Valid</span>'
|
|
)
|
|
elif obj.used_at:
|
|
return format_html(
|
|
'<span style="color: blue;">Used on {}</span>',
|
|
obj.used_at.strftime('%Y-%m-%d %H:%M')
|
|
)
|
|
elif obj.expires_at < timezone.now():
|
|
return format_html(
|
|
'<span style="color: red;">Expired</span>'
|
|
)
|
|
else:
|
|
return format_html(
|
|
'<span style="color: gray;">Inactive</span>'
|
|
)
|
|
|
|
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('<span style="color: #666;">***ENCRYPTED***</span>')
|
|
return '-'
|
|
elif obj.template.data_type == SettingTemplate.DataType.FILE:
|
|
if obj.file_value:
|
|
return format_html('<a href="{}" target="_blank">View File</a>', 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(
|
|
'<span style="background-color: #28a745; color: white; padding: 3px 8px; border-radius: 3px; font-size: 11px;">✓ Responded</span>'
|
|
)
|
|
elif obj.is_read:
|
|
return format_html(
|
|
'<span style="background-color: #17a2b8; color: white; padding: 3px 8px; border-radius: 3px; font-size: 11px;">👁 Read</span>'
|
|
)
|
|
else:
|
|
return format_html(
|
|
'<span style="background-color: #dc3545; color: white; padding: 3px 8px; border-radius: 3px; font-size: 11px; font-weight: bold;">● New</span>'
|
|
)
|
|
|
|
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')
|