from django.contrib import admin from django import forms from django.utils.translation import gettext_lazy as _ from unfold.admin import ModelAdmin, TabularInline from unfold.decorators import display from utils.json_editor_field import JsonEditorWidget import json # Import your custom admin site from utils.admin import dovoodi_admin_site # Import your models from ..models import ( BookReference, BookReferenceImage, BookAuthor, BookAttribute, BookSubjectArea, BookType ) # ----------------------------------------------------------------------------- # Custom Forms for JSON Fields # ----------------------------------------------------------------------------- class BookReferenceAdminForm(forms.ModelForm): """Custom form for BookReference with JSON editor widgets""" class Meta: model = BookReference fields = '__all__' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Schema for title JSON field title_schema = { "type": "array", "title": "Titles", "items": { "type": "object", "title": "Title", "properties": { "language_code": { "type": "string", "title": "Language Code", "enum": ["en", "fa", "ar", "ur", "ru"], "options": { "enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"] } }, "text": { "type": "string", "title": "Title Text" } }, "required": ["language_code", "text"] } } # Schema for description JSON field description_schema = { "type": "array", "title": "Descriptions", "items": { "type": "object", "title": "Description", "properties": { "language_code": { "type": "string", "title": "Language Code", "enum": ["en", "fa", "ar", "ur", "ru"], "options": { "enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"] } }, "text": { "type": "string", "title": "Description Text" } }, "required": ["language_code", "text"] } } # Schema for language JSON field language_schema = { "type": "array", "title": "Languages", "items": { "type": "object", "title": "Language", "properties": { "language_code": { "type": "string", "title": "Language Code", "enum": ["en", "fa", "ar", "ur", "ru"], "options": { "enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"] } }, "text": { "type": "string", "title": "Language Text" } }, "required": ["language_code", "text"] } } # Schema for publisher JSON field publisher_schema = { "type": "array", "title": "Publishers", "items": { "type": "object", "title": "Publisher", "properties": { "language_code": { "type": "string", "title": "Language Code", "enum": ["en", "fa", "ar", "ur", "ru"], "options": { "enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"] } }, "text": { "type": "string", "title": "Publisher Text" } }, "required": ["language_code", "text"] } } # Apply JSON editor widgets self.fields['title'].widget = JsonEditorWidget(attrs={ 'schema': json.dumps(title_schema), 'title': 'Titles' }) self.fields['description'].widget = JsonEditorWidget(attrs={ 'schema': json.dumps(description_schema), 'title': 'Descriptions' }) self.fields['language'].widget = JsonEditorWidget(attrs={ 'schema': json.dumps(language_schema), 'title': 'Languages' }) self.fields['publisher'].widget = JsonEditorWidget(attrs={ 'schema': json.dumps(publisher_schema), 'title': 'Publishers' }) class BookAttributeAdminForm(forms.ModelForm): """Custom form for BookAttribute with JSON editor widgets""" class Meta: model = BookAttribute fields = '__all__' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Schema for title JSON field title_schema = { "type": "array", "title": "Titles", "items": { "type": "object", "title": "Title", "properties": { "language_code": { "type": "string", "title": "Language Code", "enum": ["en", "fa", "ar", "ur", "ru"], "options": { "enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"] } }, "text": { "type": "string", "title": "Title Text" } }, "required": ["language_code", "text"] } } # Schema for value JSON field value_schema = { "type": "array", "title": "Values", "items": { "type": "object", "title": "Value", "properties": { "language_code": { "type": "string", "title": "Language Code", "enum": ["en", "fa", "ar", "ur", "ru"], "options": { "enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"] } }, "text": { "type": "string", "title": "Value Text" } }, "required": ["language_code", "text"] } } # Apply JSON editor widgets self.fields['title'].widget = JsonEditorWidget(attrs={ 'schema': json.dumps(title_schema), 'title': 'Titles' }) self.fields['value'].widget = JsonEditorWidget(attrs={ 'schema': json.dumps(value_schema), 'title': 'Values' }) # ----------------------------------------------------------------------------- # 1. Inlines # ----------------------------------------------------------------------------- class BookReferenceImageInline(TabularInline): """ Inline for managing Book Images directly inside the BookReference page. """ model = BookReferenceImage extra = 0 fields = ('image', 'order', 'description') readonly_fields = ('created_at',) class BookAttributeInline(TabularInline): """ Inline for managing Book Attributes (Key-Value pairs) inside BookReference. """ model = BookAttribute extra = 0 fields = ('title', 'value') readonly_fields = ('created_at',) # ----------------------------------------------------------------------------- # 2. Main Admins # ----------------------------------------------------------------------------- class BookReferenceAdmin(ModelAdmin): """Admin for BookReference model""" form = BookReferenceAdminForm # Use custom methods for JSON fields to show readable text list_display = ( 'get_title_display', 'slug', 'get_publisher_display', 'year_of_publication', 'rate', 'created_at' ) list_filter = ('year_of_publication', 'rate', 'created_at', 'type') # Searching by JSON fields via string is limited in Django, # so we focus on standard fields search_fields = ('isbn', 'slug', 'volume') readonly_fields = ('created_at', 'updated_at') # Add the inlines to manage images and attributes on the same page inlines = [BookReferenceImageInline, BookAttributeInline] # Use filter_horizontal for ManyToMany fields to make selection easier filter_horizontal = ('subject_area',) fieldsets = ( (_('Basic Info'), { 'fields': ('title', 'description', 'slug', 'language', 'subject_area') }), (_('Publication Info'), { 'fields': ('publisher', 'isbn', 'year_of_publication', 'number_page', 'volume', 'type') }), (_('Rating & Stats'), { 'fields': ('rate', 'created_at', 'updated_at') }), ) # --- Custom Display Methods --- @display(description=_('Title'), ordering='title') def get_title_display(self, obj): return self._extract_first_text(obj.title) @display(description=_('Publisher')) def get_publisher_display(self, obj): return self._extract_first_text(obj.publisher) def _extract_first_text(self, json_data): """Helper to safely extract the first 'text' from a JSON list""" if json_data and isinstance(json_data, list) and len(json_data) > 0: first_item = json_data[0] if isinstance(first_item, dict): return first_item.get('text', '-') return '-' class BookAuthorAdmin(ModelAdmin): """Admin for BookAuthor model""" list_display = ('get_name_display', 'birth_year_hijri', 'death_year_hijri', 'birth_year_miladi', 'death_year_miladi', 'created_at', 'updated_at') search_fields = ('name',) # Note: Search works best on exact text matches readonly_fields = ('created_at', 'updated_at') list_filter = ('birth_year_hijri', 'death_year_hijri', 'birth_year_miladi', 'death_year_miladi', 'created_at') # Use filter_horizontal for ManyToMany fields to make selection easier filter_horizontal = ('book_references',) fieldsets = ( (_('Basic Info'), { 'fields': ('name',) }), (_('Dates (Hijri)'), { 'fields': ('birth_year_hijri', 'death_year_hijri') }), (_('Dates (Miladi)'), { 'fields': ('birth_year_miladi', 'death_year_miladi') }), (_('References'), { 'fields': ('book_references',) }), (_('Timestamps'), { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }), ) @display(description=_('Name'), ordering='name') def get_name_display(self, obj): if obj.name and isinstance(obj.name, list) and len(obj.name) > 0: first = obj.name[0] if isinstance(first, dict): return first.get('text', '-') return '-' class BookReferenceImageAdmin(ModelAdmin): # Display the custom string, plus the raw order and book link for convenience list_display = ("display_name", "order", "book_reference") # optimize database queries since we are accessing foreign key data (book_reference) list_select_related = ("book_reference",) def display_name(self, obj): # Implements: f"{self.book_reference.title[0]['text']} - Image {self.order}" try: # We use safe navigation to prevent admin crashes if data is missing book_title = obj.book_reference.title[0]['text'] return f"{book_title} - Image {obj.order}" except (AttributeError, IndexError, KeyError, TypeError): # Fallback if the title structure isn't exactly as expected return f"Unknown Book - Image {obj.order}" # Sets the column header name in the admin panel display_name.short_description = "Image Reference" class BookAttributeAdmin(ModelAdmin): """ Admin for managing Attributes independently. Useful if you want to see all attributes across all books. """ form = BookAttributeAdminForm list_display = ('get_title_display', 'get_value_display', 'get_book_display', 'created_at') list_filter = ('created_at',) search_fields = ('book_reference__slug',) @display(description=_('Title'), ordering='title') def get_title_display(self, obj): return self._extract_first_text(obj.title) @display(description=_('Value'), ordering='value') def get_value_display(self, obj): return self._extract_first_text(obj.value) @display(description=_('Book Reference'), ordering='book_reference') def get_book_display(self, obj): if obj.book_reference: return self._extract_first_text(obj.book_reference.title) return '-' def _extract_first_text(self, json_data): if json_data and isinstance(json_data, list) and len(json_data) > 0: first = json_data[0] if isinstance(first, dict): return first.get('text', '-') return '-' class BookSubjectAreaAdmin(ModelAdmin): """Admin for BookSubjectArea model""" list_display = ('get_title_display', 'created_at', 'updated_at') search_fields = ('title',) readonly_fields = ('created_at', 'updated_at') @display(description=_('Title'), ordering='title') def get_title_display(self, obj): return self._extract_first_text(obj.title) def _extract_first_text(self, json_data): """Helper to safely extract the first 'text' from a JSON list""" if json_data and isinstance(json_data, list) and len(json_data) > 0: first_item = json_data[0] if isinstance(first_item, dict): return first_item.get('text', '-') return '-' class BookTypeAdmin(ModelAdmin): """Admin for BookType model""" list_display = ('get_title_display', 'created_at', 'updated_at') search_fields = ('title',) readonly_fields = ('created_at', 'updated_at') @display(description=_('Title'), ordering='title') def get_title_display(self, obj): return self._extract_first_text(obj.title) def _extract_first_text(self, json_data): """Helper to safely extract the first 'text' from a JSON list""" if json_data and isinstance(json_data, list) and len(json_data) > 0: first_item = json_data[0] if isinstance(first_item, dict): return first_item.get('text', '-') return '-' # ----------------------------------------------------------------------------- # 3. Registration # ----------------------------------------------------------------------------- dovoodi_admin_site.register(BookReference, BookReferenceAdmin) dovoodi_admin_site.register(BookAuthor, BookAuthorAdmin) dovoodi_admin_site.register(BookAttribute, BookAttributeAdmin) dovoodi_admin_site.register(BookReferenceImage, BookReferenceImageAdmin) dovoodi_admin_site.register(BookSubjectArea, BookSubjectAreaAdmin) dovoodi_admin_site.register(BookType, BookTypeAdmin)