From 538d0316bc918cbdfa0e2d828b0a37e1ad4df11a Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Sat, 24 Jan 2026 10:23:10 +0330 Subject: [PATCH] Add custom forms for JSON fields in Hadis admin - Introduced custom forms for Hadis, HadisCollection, HadisTag, HadisStatus, HadisReference, and Transmitters models to utilize JSON editor widgets for better data management. - Implemented schemas for various JSON fields including titles, descriptions, and addresses to enhance the admin interface. - Updated list displays in admin classes to show the first text from JSON fields for improved readability. - Added a management command to seed HadisStatus descriptions with multilingual definitions based on keywords. --- apps/hadis/admin/hadis.py | 517 +++++++++++++++++- apps/hadis/admin/reference.py | 243 ++++++-- apps/hadis/admin/transmitter.py | 164 +++++- .../management/commands/seed_hadisstatus.py | 110 ++++ 4 files changed, 983 insertions(+), 51 deletions(-) create mode 100644 apps/hadis/management/commands/seed_hadisstatus.py diff --git a/apps/hadis/admin/hadis.py b/apps/hadis/admin/hadis.py index f3dd9a1..2a382d3 100644 --- a/apps/hadis/admin/hadis.py +++ b/apps/hadis/admin/hadis.py @@ -75,6 +75,176 @@ class HadisAdminForm(forms.ModelForm): } } + # Schema for title_narrator JSON field + title_narrator_schema = { + "type": "array", + "title": "Title Narrators", + "items": { + "type": "object", + "title": "Title Narrator", + "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 Narrator Text" + } + }, + "required": ["language_code", "text"] + } + } + + # 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 hadis_status_text JSON field + hadis_status_text_schema = { + "type": "array", + "title": "Status Texts", + "items": { + "type": "object", + "title": "Status Text", + "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": "Status Text" + } + }, + "required": ["language_code", "text"] + } + } + + # Schema for address JSON field (text is an array of strings) + address_schema = { + "type": "array", + "title": "Addresses", + "items": { + "type": "object", + "title": "Address", + "properties": { + "language_code": { + "type": "string", + "title": "Language Code", + "enum": ["en", "fa", "ar", "ur", "ru"], + "options": { + "enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"] + } + }, + "text": { + "type": "array", + "title": "Address Parts", + "items": { + "type": "string", + "title": "Address Part" + } + } + }, + "required": ["language_code", "text"] + } + } + + # Schema for explanation JSON field (text is an array of objects with title and detail) + explanation_schema = { + "type": "array", + "title": "Explanations", + "items": { + "type": "object", + "title": "Explanation", + "properties": { + "language_code": { + "type": "string", + "title": "Language Code", + "enum": ["en", "fa", "ar", "ur", "ru"], + "options": { + "enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"] + } + }, + "text": { + "type": "array", + "title": "Explanation Items", + "items": { + "type": "object", + "title": "Explanation Item", + "properties": { + "title": { + "type": "string", + "title": "Title" + }, + "detail": { + "type": "string", + "title": "Detail", + "format": "textarea" + } + }, + "required": ["title", "detail"] + } + } + }, + "required": ["language_code", "text"] + } + } + # Schema for explanations JSON field (array of objects with title and description) # explanations_schema = { # "type": "array", @@ -137,6 +307,36 @@ class HadisAdminForm(forms.ModelForm): 'title': 'Links' }) + self.fields['title_narrator'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(title_narrator_schema), + 'title': 'Title Narrators' + }) + + 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['hadis_status_text'].widget = JsonEditorWidget(attrs={ + # 'schema': json.dumps(hadis_status_text_schema), + # 'title': 'Status Texts' + # }) + + self.fields['address'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(address_schema), + 'title': 'Addresses' + }) + + self.fields['explanation'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(explanation_schema), + 'title': 'Explanations' + }) + # self.fields['explanations'].widget = JsonEditorWidget(attrs={ # 'schema': json.dumps(explanations_schema), # 'title': 'Explanations' @@ -148,6 +348,235 @@ class HadisAdminForm(forms.ModelForm): # }) +# Custom Forms for JSON Fields +class HadisCollectionAdminForm(forms.ModelForm): + """Custom form for HadisCollection with JSON editor widgets""" + + class Meta: + model = HadisCollection + 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 summary JSON field + summary_schema = { + "type": "array", + "title": "Summaries", + "items": { + "type": "object", + "title": "Summary", + "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": "Summary Text" + } + }, + "required": ["language_code", "text"] + } + } + + # Apply JSON editor widgets + self.fields['title'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(title_schema), + 'title': 'Titles' + }) + + self.fields['summary'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(summary_schema), + 'title': 'Summaries' + }) + + +class HadisTagAdminForm(forms.ModelForm): + """Custom form for HadisTag with JSON editor widgets""" + + class Meta: + model = HadisTag + 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"] + } + } + + # Apply JSON editor widgets + self.fields['title'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(title_schema), + 'title': 'Titles' + }) + + +class HadisStatusAdminForm(forms.ModelForm): + """Custom form for HadisStatus with JSON editor widgets""" + + class Meta: + model = HadisStatus + 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"] + } + } + + # 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' + }) + + +class HadisReferenceAdminForm(forms.ModelForm): + """Custom form for HadisReference with JSON editor widgets""" + + class Meta: + model = HadisReference + fields = '__all__' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # 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"] + } + } + + # Apply JSON editor widgets + self.fields['description'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(description_schema), + 'title': 'Descriptions' + }) + + # Inline Admin Classes class ReferenceImageInline(TabularInline): """Inline for ReferenceImage in HadisReference admin""" @@ -168,7 +597,8 @@ class HadisReferenceInline(TabularInline): # Main Admin Classes class HadisTagAdmin(ModelAdmin): """Admin for HadisTag model""" - list_display = ('title', 'status', 'created_at') + form = HadisTagAdminForm + list_display = ('get_title_display', 'status', 'created_at') list_filter = ('status', 'created_at') search_fields = ('title',) readonly_fields = ('created_at', 'updated_at') @@ -183,10 +613,23 @@ class HadisTagAdmin(ModelAdmin): }), ) + @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 HadisStatusAdmin(ModelAdmin): """Admin for HadisStatus model""" - list_display = ('title', 'color', 'order', 'description') + form = HadisStatusAdminForm + list_display = ('get_title_display', 'color', 'order', 'get_description_display') list_filter = ('color',) search_fields = ('title', 'description') ordering = ('order',) @@ -198,11 +641,27 @@ class HadisStatusAdmin(ModelAdmin): }), ) + @display(description=_('Title'), ordering='title') + def get_title_display(self, obj): + return self._extract_first_text(obj.title) + + @display(description=_('Description')) + def get_description_display(self, obj): + return self._extract_first_text(obj.description) + + 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 HadisAdmin(ModelAdmin): """Admin for Hadis model""" form = HadisAdminForm - list_display = ('number', 'title', 'category', 'status', 'hadis_status', 'created_at','slug') + list_display = ('number', 'get_title_display', 'category', 'status', 'hadis_status', 'created_at','slug') list_filter = ('status', 'hadis_status', 'category', 'created_at') search_fields = ('title', 'text', 'category__title', 'slug') readonly_fields = ('created_at', 'updated_at', 'share_link', 'slug') @@ -212,13 +671,13 @@ class HadisAdmin(ModelAdmin): fieldsets = ( (None, { - 'fields': ('category', 'number', 'title', 'status', 'slug') + 'fields': ('category', 'number', 'title', 'title_narrator', 'status', 'slug') }), (_('Content'), { - 'fields': ('text', 'translation', 'explanation') + 'fields': ('text', 'translation', 'explanation','description') }), (_('Status & Classification'), { - 'fields': ('hadis_status', 'hadis_status_text', 'tags') + 'fields': ('hadis_status','tags') }), (_('Additional Information'), { 'fields': ('address', 'links', 'share_link'), @@ -230,9 +689,22 @@ class HadisAdmin(ModelAdmin): }), ) + @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 HadisReferenceAdmin(ModelAdmin): """Admin for HadisReference model""" + form = HadisReferenceAdminForm list_display = ('hadis', 'book_reference', 'created_at') list_filter = ('created_at', 'book_reference') search_fields = ('hadis__title', 'book_reference__title') @@ -241,7 +713,7 @@ class HadisReferenceAdmin(ModelAdmin): fieldsets = ( (None, { - 'fields': ('hadis', 'book_reference') + 'fields': ('hadis', 'book_reference','description') }), (_('Timestamps'), { 'fields': ('created_at',), @@ -274,7 +746,8 @@ class HadisInCollectionInline(TabularInline): class HadisCollectionAdmin(ModelAdmin): """Admin for HadisCollection model""" - list_display = ('title', 'slug', 'status', 'order', 'created_at') + form = HadisCollectionAdminForm + list_display = ('get_title_display', 'slug', 'status', 'order', 'created_at') list_filter = ('status', 'created_at') search_fields = ('title', 'slug', 'summary') readonly_fields = ('slug', 'created_at', 'updated_at') @@ -283,7 +756,7 @@ class HadisCollectionAdmin(ModelAdmin): fieldsets = ( (None, { - 'fields': ('title', 'slug', 'summary', 'status', 'order', 'thumbnail') + 'fields': ('title', 'slug', 'summary', 'status', 'order') }), (_('Timestamps'), { 'fields': ('created_at', 'updated_at'), @@ -291,6 +764,18 @@ class HadisCollectionAdmin(ModelAdmin): }), ) + @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 HadisInCollectionAdmin(ModelAdmin): """Admin for HadisInCollection model""" @@ -416,7 +901,7 @@ class HadisCorrectionAdminForm(forms.ModelForm): class HadisCorrectionAdmin(ModelAdmin): """Admin for HadisCorrection model""" form = HadisCorrectionAdminForm - list_display = ('hadis', 'title', 'slug', 'created_at') + list_display = ('hadis', 'get_title_display', 'slug', 'created_at') list_filter = ('created_at', 'hadis__category') search_fields = ('hadis__title', 'title', 'slug') readonly_fields = ('slug', 'created_at', 'updated_at', 'share_link') @@ -439,6 +924,18 @@ class HadisCorrectionAdmin(ModelAdmin): }), ) + @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 '-' + # Register models with dovoodi admin site dovoodi_admin_site.register(HadisTag, HadisTagAdmin) diff --git a/apps/hadis/admin/reference.py b/apps/hadis/admin/reference.py index c7c9ad9..a082f77 100644 --- a/apps/hadis/admin/reference.py +++ b/apps/hadis/admin/reference.py @@ -227,6 +227,174 @@ class BookAttributeAdminForm(forms.ModelForm): }) +class BookAuthorAdminForm(forms.ModelForm): + """Custom form for BookAuthor with JSON editor widgets""" + + class Meta: + model = BookAuthor + fields = '__all__' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Schema for name JSON field + name_schema = { + "type": "array", + "title": "Names", + "items": { + "type": "object", + "title": "Name", + "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": "Name Text" + } + }, + "required": ["language_code", "text"] + } + } + + # Apply JSON editor widgets + self.fields['name'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(name_schema), + 'title': 'Names' + }) + + +class BookReferenceImageAdminForm(forms.ModelForm): + """Custom form for BookReferenceImage with JSON editor widgets""" + + class Meta: + model = BookReferenceImage + fields = '__all__' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # 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"] + } + } + + # Apply JSON editor widgets + self.fields['description'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(description_schema), + 'title': 'Descriptions' + }) + + +class BookSubjectAreaAdminForm(forms.ModelForm): + """Custom form for BookSubjectArea with JSON editor widgets""" + + class Meta: + model = BookSubjectArea + 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"] + } + } + + # Apply JSON editor widgets + self.fields['title'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(title_schema), + 'title': 'Titles' + }) + + +class BookTypeAdminForm(forms.ModelForm): + """Custom form for BookType with JSON editor widgets""" + + class Meta: + model = BookType + 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"] + } + } + + # Apply JSON editor widgets + self.fields['title'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(title_schema), + 'title': 'Titles' + }) + + # ----------------------------------------------------------------------------- # 1. Inlines # ----------------------------------------------------------------------------- @@ -236,6 +404,7 @@ class BookReferenceImageInline(TabularInline): Inline for managing Book Images directly inside the BookReference page. """ model = BookReferenceImage + form = BookReferenceImageAdminForm extra = 0 fields = ('image', 'order', 'description') readonly_fields = ('created_at',) @@ -285,10 +454,10 @@ class BookReferenceAdmin(ModelAdmin): fieldsets = ( (_('Basic Info'), { - 'fields': ('title', 'description', 'slug', 'language', 'subject_area') + 'fields': ('title', 'description', 'slug', 'language') }), (_('Publication Info'), { - 'fields': ('publisher', 'isbn', 'year_of_publication', 'number_page', 'volume', 'type') + 'fields': ('publisher', 'isbn', 'year_of_publication', 'number_page', 'volume') }), (_('Rating & Stats'), { 'fields': ('rate', 'created_at', 'updated_at') @@ -316,6 +485,7 @@ class BookReferenceAdmin(ModelAdmin): class BookAuthorAdmin(ModelAdmin): """Admin for BookAuthor model""" + form = BookAuthorAdminForm 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 @@ -353,6 +523,9 @@ class BookAuthorAdmin(ModelAdmin): return '-' class BookReferenceImageAdmin(ModelAdmin): + """Admin for BookReferenceImage model""" + form = BookReferenceImageAdminForm + # Display the custom string, plus the raw order and book link for convenience list_display = ("display_name", "order", "book_reference") @@ -404,44 +577,46 @@ class BookAttributeAdmin(ModelAdmin): return '-' -class BookSubjectAreaAdmin(ModelAdmin): - """Admin for BookSubjectArea model""" +# class BookSubjectAreaAdmin(ModelAdmin): +# """Admin for BookSubjectArea model""" +# form = BookSubjectAreaAdminForm - list_display = ('get_title_display', 'created_at', 'updated_at') - search_fields = ('title',) - readonly_fields = ('created_at', 'updated_at') +# 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) +# @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 '-' +# 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""" +# class BookTypeAdmin(ModelAdmin): +# """Admin for BookType model""" +# form = BookTypeAdminForm - list_display = ('get_title_display', 'created_at', 'updated_at') - search_fields = ('title',) - readonly_fields = ('created_at', 'updated_at') +# 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) +# @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 '-' +# 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 '-' # ----------------------------------------------------------------------------- @@ -452,5 +627,5 @@ 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) \ No newline at end of file +# dovoodi_admin_site.register(BookSubjectArea, BookSubjectAreaAdmin) +# dovoodi_admin_site.register(BookType, BookTypeAdmin) \ No newline at end of file diff --git a/apps/hadis/admin/transmitter.py b/apps/hadis/admin/transmitter.py index 89e4bde..95597f6 100644 --- a/apps/hadis/admin/transmitter.py +++ b/apps/hadis/admin/transmitter.py @@ -34,8 +34,98 @@ class TransmitterOriginalTextInline(TabularInline): fields = ('title', 'slug') +# Custom Forms for JSON Fields +class TransmittersAdminForm(forms.ModelForm): + """Custom form for Transmitters with JSON editor widgets""" + + class Meta: + model = Transmitters + fields = '__all__' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Helper function to create standard language schema + def create_language_schema(title_name, text_title): + return { + "type": "array", + "title": title_name, + "items": { + "type": "object", + "title": title_name[:-1] if title_name.endswith('s') else title_name, + "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": text_title + } + }, + "required": ["language_code", "text"] + } + } + + # Create schemas for all JSON fields + full_name_schema = create_language_schema("Full Names", "Full Name Text") + kunya_schema = create_language_schema("Kunyas", "Kunya Text") + known_as_schema = create_language_schema("Known As", "Known As Text") + nickname_schema = create_language_schema("Nicknames", "Nickname Text") + origin_schema = create_language_schema("Origins", "Origin Text") + lived_in_schema = create_language_schema("Lived In", "Lived In Text") + died_in_schema = create_language_schema("Died In", "Died In Text") + description_schema = create_language_schema("Descriptions", "Description Text") + + # Apply JSON editor widgets + self.fields['full_name'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(full_name_schema), + 'title': 'Full Names' + }) + + self.fields['kunya'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(kunya_schema), + 'title': 'Kunyas' + }) + + self.fields['known_as'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(known_as_schema), + 'title': 'Known As' + }) + + self.fields['nickname'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(nickname_schema), + 'title': 'Nicknames' + }) + + self.fields['origin'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(origin_schema), + 'title': 'Origins' + }) + + self.fields['lived_in'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(lived_in_schema), + 'title': 'Lived In' + }) + + self.fields['died_in'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(died_in_schema), + 'title': 'Died In' + }) + + self.fields['description'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(description_schema), + 'title': 'Descriptions' + }) + + class TransmittersAdmin(ModelAdmin): """Admin for Transmitters model""" + form = TransmittersAdminForm list_display = ('get_full_name_display', 'birth_year_hijri', 'death_year_hijri') list_filter = ('birth_year_hijri', 'death_year_hijri') search_fields = ('full_name', 'description') @@ -44,10 +134,10 @@ class TransmittersAdmin(ModelAdmin): fieldsets = ( (None, { - 'fields': ('full_name', 'birth_year_hijri', 'death_year_hijri') + 'fields': ('full_name', 'birth_year_hijri', 'death_year_hijri','known_as','nickname') }), (_('Additional Information'), { - 'fields': ('description',), + 'fields': ('description','origin','lived_in','died_in','kunya'), # 'classes': ('collapse',) }), (_('Timestamps'), { @@ -422,7 +512,7 @@ class TransmitterOriginalTextAdminForm(forms.ModelForm): class NarratorLayerAdmin(ModelAdmin): """Admin for NarratorLayer model""" form = NarratorLayerAdminForm - list_display = ('number', 'name', 'slug', 'created_at') + list_display = ('number', 'get_name_display', 'slug', 'created_at') list_filter = ('created_at', 'updated_at') search_fields = ('name', 'slug') readonly_fields = ('slug', 'created_at', 'updated_at') @@ -441,11 +531,23 @@ class NarratorLayerAdmin(ModelAdmin): }), ) + @display(description=_('Name'), ordering='name') + def get_name_display(self, obj): + return self._extract_first_text(obj.name) + + 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 TransmitterReliabilityAdmin(ModelAdmin): """Admin for TransmitterReliability model""" form = TransmitterReliabilityAdminForm - list_display = ('title', 'slug', 'color') + list_display = ('get_title_display', 'slug', 'color') list_filter = ('color',) search_fields = ('title', 'slug') readonly_fields = ('slug',) @@ -456,11 +558,23 @@ class TransmitterReliabilityAdmin(ModelAdmin): }), ) + @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 OpinionStatusAdmin(ModelAdmin): """Admin for OpinionStatus model""" form = OpinionStatusAdminForm - list_display = ('title', 'slug', 'color') + list_display = ('get_title_display', 'slug', 'color') list_filter = ('color',) search_fields = ('title', 'slug') readonly_fields = ('slug',) @@ -471,13 +585,25 @@ class OpinionStatusAdmin(ModelAdmin): }), ) + @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 TransmitterOpinionAdmin(ModelAdmin): """Admin for TransmitterOpinion model""" form = TransmitterOpinionAdminForm - list_display = ('transmitter', 'scholar_name', 'status', 'created_at') + list_display = ('transmitter', 'get_scholar_name_display', 'status', 'created_at') list_filter = ('status', 'created_at', 'transmitter') search_fields = ('transmitter__full_name', 'scholar_name') readonly_fields = ('created_at', 'updated_at') @@ -495,6 +621,18 @@ class TransmitterOpinionAdmin(ModelAdmin): }), ) + @display(description=_('Scholar Name'), ordering='scholar_name') + def get_scholar_name_display(self, obj): + return self._extract_first_text(obj.scholar_name) + + 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 '-' + @@ -502,7 +640,7 @@ class TransmitterOpinionAdmin(ModelAdmin): class TransmitterOriginalTextAdmin(ModelAdmin): """Admin for TransmitterOriginalText model""" form = TransmitterOriginalTextAdminForm - list_display = ('transmitter', 'title', 'slug') + list_display = ('transmitter', 'get_title_display', 'slug') list_filter = ('transmitter',) search_fields = ('transmitter__full_name', 'title', 'slug') readonly_fields = ('slug',) @@ -520,6 +658,18 @@ class TransmitterOriginalTextAdmin(ModelAdmin): }), ) + @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 '-' + # Register models with the custom admin site dovoodi_admin_site.register(Transmitters, TransmittersAdmin) diff --git a/apps/hadis/management/commands/seed_hadisstatus.py b/apps/hadis/management/commands/seed_hadisstatus.py new file mode 100644 index 0000000..e1e0e79 --- /dev/null +++ b/apps/hadis/management/commands/seed_hadisstatus.py @@ -0,0 +1,110 @@ +import json +from django.core.management.base import BaseCommand +from django.db import transaction +# REPLACE 'apps.hadis.models' with your actual app path +from apps.hadis.models import HadisStatus + +class Command(BaseCommand): + help = 'Updates HadisStatus descriptions mapping Russian/English titles to trilingual academic definitions' + + def handle(self, *args, **options): + self.stdout.write("Starting update of HadisStatus descriptions...") + + # 1. DEFINITIONS LIBRARY + # We define the content for each 'Type' of status found in your DB + + # SAHIH (Authentic) -> Matches: "Достоверный", "Authentic / Accepted" + desc_sahih = { + 'en': "A Sahih (Authentic) Hadith represents the highest level of reliability. It possesses a continuous chain of narrators (Isnad) made up of upright (Adl) and retentive (Dabit) transmitters, free from any hidden defects (Illah) or irregularities (Shudhudh). It is universally accepted for deriving Islamic laws.", + 'fa': "حدیث صحیح بالاترین درجه اعتبار را دارد. این حدیث دارای سندی متصل است که راویان آن عادل و دارای ضبط (حافظه قوی) هستند و در آن هیچ‌گونه علت (عیب پنهان) یا شذوذ (مخالفت با راویان معتبرتر) وجود ندارد. این حدیث مبنای استنباط احکام شرعی است.", + 'ru': "Хадис Сахих (Достоверный) представляет собой высший уровень надежности. Он имеет непрерывную цепочку передатчиков (иснад), состоящую из праведных ('адль) и обладающих хорошей памятью (дабит) людей, свободен от скрытых недостатков ('илля) или отклонений (шузуз). Он безоговорочно принимается для вынесения шариатских решений." + } + + # HASAN (Good) -> Matches: "Хороший" + desc_hasan = { + 'en': "A Hasan (Good) Hadith is valid and acceptable for legal rulings. It fulfills all conditions of a Sahih hadith (continuity, uprightness, lack of defects), except that the memory or precision of one or more narrators is slightly below the perfect standard required for Sahih.", + 'fa': "حدیث حسن حدیثی معتبر و قابل استناد در احکام است. این حدیث تمام شرایط حدیث صحیح (اتصال سند، عدالت راوی، عدم عیب) را دارد، با این تفاوت که ضبط (دقت و حافظه) یک یا چند نفر از راویان آن کمی پایین‌تر از حد عالی لازم برای حدیث صحیح است.", + 'ru': "Хадис Хасан (Хороший) является действительным и приемлемым. Он отвечает всем условиям хадиса Сахих (непрерывность, праведность, отсутствие недостатков), за исключением того, что память или точность одного из передатчиков немного ниже идеального стандарта, требуемого для Сахих." + } + + # DA'IF (Weak) -> Matches: "Слабый", "Weak / Needs Review" + desc_weak = { + 'en': "A Da'if (Weak) Hadith fails to reach the rank of Hasan because it lacks one or more essential conditions of authenticity. This could be due to a disconnection in the chain of narrators or a significant weakness in the character or memory of a narrator. It is generally not used for legal rulings.", + 'fa': "حدیث ضعیف حدیثی است که به درجه حسن نمی‌رسد زیرا فاقد یک یا چند شرط اصلی صحت است. این امر می‌تواند ناشی از قطع در سند روایت یا ضعف جدی در شخصیت یا حافظه راوی باشد. این حدیث معمولاً مبنای احکام فقهی قرار نمی‌گیرد.", + 'ru': "Хадис Даиф (Слабый) не достигает уровня Хасан, так как в нем отсутствует одно или несколько условий достоверности. Это может быть связано с разрывом в цепочке передатчиков или значительной слабостью в характере или памяти передатчика. Обычно он не используется для правовых решений." + } + + # MAWDU (Fabricated) -> Matches: "Выдуманный" + desc_fabricated = { + 'en': "A Mawdu' (Fabricated) Hadith is a false narration attributed to the Prophet (PBUH) which he did not say. It is considered rejected (Mardud) and is impermissible to convey except to expose its fabrication and warn the community against it.", + 'fa': "حدیث موضوع (جعلی) روایتی دروغین است که به پیامبر (ص) نسبت داده شده در حالی که ایشان آن را نفرموده‌اند. این حدیث مردود است و نقل آن حرام می‌باشد، مگر برای افشای جعلی بودن و هشدار دادن به جامعه درباره آن.", + 'ru': "Хадис Мавду (Выдуманный) — это ложное предание, приписанное Пророку (мир ему), которого он не говорил. Он считается отвергнутым (Мардуд), и его передача запрещена, кроме как для разоблачения его ложности и предостережения общины." + } + + # BROKEN / INTERRUPTED -> Matches: "Разорванный", "Прерванный" + desc_broken = { + 'en': "This category ('Munqati' or 'Maqtu') refers to a Hadith with a non-continuous chain of transmission. There is a missing link (or links) between the narrators, meaning the person reporting the Hadith did not hear it directly from the source they are quoting.", + 'fa': "این دسته (منقطع یا مقطوع) به حدیثی اشاره دارد که سند آن پیوسته نیست. در زنجیره راویان حلقه‌ای مفقود وجود دارد، به این معنا که نقل‌کننده حدیث، آن را مستقیماً از کسی که از او نقل می‌کند، نشنیده است.", + 'ru': "Эта категория ('Мункати' или 'Макту') относится к хадису с прерывистой цепочкой передачи. Между передатчиками отсутствует одно или несколько звеньев; это означает, что человек, передающий хадис, не слышал его непосредственно от источника, который он цитирует." + } + + # UNKNOWN -> Matches: "Неизвестный" + desc_unknown = { + 'en': "This status ('Majhul') indicates that a narrator in the chain is not identified or their reliability status is unknown to scholars. Without verification of the narrator's identity and trustworthiness, the narration cannot be authenticated.", + 'fa': "این وضعیت (مجهول) نشان می‌دهد که یکی از راویان در سند شناخته شده نیست یا وضعیت اعتبار او برای علما نامشخص است. بدون احراز هویت و وثاقت راوی، روایت قابل تایید و استناد نمی‌باشد.", + 'ru': "Этот статус ('Маджхуль') указывает на то, что передатчик в цепочке не идентифицирован или его статус надежности неизвестен ученым. Без подтверждения личности и благонадежности передатчика предание не может быть признано достоверным." + } + + # 2. MAPPING LOGIC + # We define keywords to look for in the title (searching both English and Russian text) + mapping_rules = [ + (['достоверный', 'authentic', 'accepted'], desc_sahih), + (['хороший', 'good'], desc_hasan), + (['слабый', 'weak', 'needs review'], desc_weak), + (['выдуманный', 'fabricated'], desc_fabricated), + (['разорванный', 'прерванный', 'broken', 'interrupted'], desc_broken), + (['неизвестный', 'unknown'], desc_unknown), + ] + + count = 0 + qs = HadisStatus.objects.all() + + if not qs.exists(): + self.stdout.write(self.style.WARNING("No HadisStatus objects found.")) + return + + for status in qs: + # 1. Extract all text from the title list into a single lowercase search string + # Example title: [{"text": "Достоверный", "language_code": "ru"}] -> "достоверный" + search_text = "" + if isinstance(status.title, list): + search_text = " ".join([item.get('text', '') for item in status.title]).lower() + + # 2. Find the correct definition + selected_desc = None + found_key = "" + + for keywords, description in mapping_rules: + # Check if ANY keyword exists in the search text + if any(k in search_text for k in keywords): + selected_desc = description + found_key = search_text + break + + # 3. Update or Skip + if selected_desc: + # Construct the new JSON format + new_description = [ + {"language_code": "en", "text": selected_desc['en']}, + {"language_code": "fa", "text": selected_desc['fa']}, + {"language_code": "ru", "text": selected_desc['ru']} + ] + + status.description = new_description + status.save() + count += 1 + self.stdout.write(f"Updated ID {status.id} ({found_key}) -> mapped to new description.") + else: + self.stdout.write(self.style.WARNING(f"Skipped ID {status.id}: Title '{search_text}' did not match any known categories.")) + + self.stdout.write(self.style.SUCCESS(f"Successfully updated {count} HadisStatus descriptions.")) \ No newline at end of file