4 Commits

Author SHA1 Message Date
Mohsen Taba 76762bebf8 nginx update 1 month ago
Mohsen Taba 471f410970 language codes updated , address and links json connected to its template 1 month ago
Mohsen Taba 8665eed84d multilanguage widget appearance updated 1 month ago
Mohsen Taba 6c13b951c0 hadis multilanguage fields refactor 1 month ago
  1. 139
      apps/hadis/admin/category.py
  2. 614
      apps/hadis/admin/hadis.py
  3. 349
      apps/hadis/admin/reference.py
  4. 422
      apps/hadis/admin/transmitter.py
  5. 69
      apps/hadis/management/commands/migrate_json_keys.py
  6. 22
      apps/hadis/models/category.py
  7. 46
      apps/hadis/models/hadis.py
  8. 44
      apps/hadis/models/reference.py
  9. 60
      apps/hadis/models/transmitter.py
  10. 16
      apps/hadis/serializers/category.py
  11. 6
      apps/hadis/views/category.py
  12. 18
      nginx/dovodi.conf
  13. 158
      templates/utils/widgets/links_json_widget.html
  14. 240
      templates/utils/widgets/multilang_address_widget.html
  15. 237
      templates/utils/widgets/multilang_json_widget.html
  16. 198
      utils/multilang_json_widget.py

139
apps/hadis/admin/category.py

@ -5,7 +5,8 @@ from django.utils.html import format_html
from unfold.admin import ModelAdmin
from unfold.decorators import display, action
from mptt.admin import DraggableMPTTAdmin
from utils.json_editor_field import JsonEditorWidget
from unfold.widgets import UnfoldAdminTextInputWidget, UnfoldAdminTextareaWidget, UnfoldAdminExpandableTextareaWidget
from utils.multilang_json_widget import MultiLanguageJSONWidget
import json
from utils.admin import dovoodi_admin_site
@ -19,71 +20,11 @@ class HadisSectAdminForm(forms.ModelForm):
class Meta:
model = HadisSect
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"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
}
# 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 HadisCategoryAdminForm(forms.ModelForm):
"""Custom form for HadisCategory with JSON editor widgets"""
@ -91,71 +32,11 @@ class HadisCategoryAdminForm(forms.ModelForm):
class Meta:
model = HadisCategory
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"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
}
# 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 HadisSectAdmin(ModelAdmin):
"""Admin for HadisSect model"""
@ -174,7 +55,7 @@ class HadisSectAdmin(ModelAdmin):
def display_title(self, obj):
"""Extracts text from the title JSON list"""
try:
return obj.title[0]['text']
return obj.title[0]['title']
except (IndexError, KeyError, TypeError, AttributeError):
return "No Title"
@ -205,7 +86,7 @@ class HadisCategoryAdmin(ModelAdmin):
"""Display indented title for tree structure using JSON text"""
try:
# Extract text from the first element of the title list
title_text = instance.title[0]['text']
title_text = instance.title[0]['title']
except (IndexError, KeyError, TypeError, AttributeError):
title_text = "No Title"

614
apps/hadis/admin/hadis.py

@ -4,10 +4,12 @@ from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin, TabularInline
from unfold.contrib.forms.widgets import WysiwygWidget
from unfold.decorators import display, action
from unfold.widgets import UnfoldAdminTextInputWidget, UnfoldAdminTextareaWidget
from utils.multilang_json_widget import MultiLanguageJSONWidget, MultiLanguageAddressWidget, LinksJSONWidget
from utils.json_editor_field import JsonEditorWidget
import json
from utils.admin import dovoodi_admin_site,dovoodi_admin_site
from utils.admin import dovoodi_admin_site
from ..models import (
Hadis, HadisReference, HadisTag, HadisStatus, ReferenceImage,
HadisCollection, HadisInCollection, HadisCorrection
@ -22,189 +24,18 @@ class HadisAdminForm(forms.ModelForm):
model = Hadis
fields = '__all__'
widgets = {
'explanation': WysiwygWidget(),
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'title_narrator': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
'translation': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
'address': MultiLanguageAddressWidget(),
'links': LinksJSONWidget(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Schema for translation JSON field
translation_schema = {
"type": "array",
"title": "Translations",
"items": {
"type": "object",
"title": "Translation",
"properties": {
"language_code": {
"type": "string",
"title": "Language Code",
"enum": ["en", "fa", "ar", "ur"],
"options": {
"enum_titles": ["English", "Persian", "Arabic", "Urdu"]
}
},
"text": { # <‑‑ use text, not title
"type": "string",
"title": "Translation Text"
}
},
"required": ["language_code", "text"] # <‑‑ update required key
}
}
# Schema for links JSON field (array of objects with title and link)
links_schema = {
"type": "array",
"title": "Links",
"items": {
"type": "object",
"title": "Link",
"properties": {
"title": {
"type": "string",
"title": "Link Title"
},
"link": {
"type": "string",
"title": "URL",
"format": "uri"
}
},
"required": ["title", "link"]
}
}
# 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)
# Schema for explanation JSON field (title is an array of objects with title and detail)
explanation_schema = {
"type": "array",
"title": "Explanations",
@ -220,7 +51,7 @@ class HadisAdminForm(forms.ModelForm):
"enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"]
}
},
"text": {
"title": {
"type": "array",
"title": "Explanation Items",
"items": {
@ -241,111 +72,15 @@ class HadisAdminForm(forms.ModelForm):
}
}
},
"required": ["language_code", "text"]
"required": ["language_code", "title"]
}
}
# Schema for explanations JSON field (array of objects with title and description)
# explanations_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"]
# }
# },
# "title": {
# "type": "string",
# "title": "Title"
# },
# "description": {
# "type": "string",
# "title": "Description"
# }
# },
# "required": ["language_code", "title", "description"]
# }
# }
# Schema for address_details JSON field (array of objects with text and priority)
# address_details_schema = {
# "type": "array",
# "title": "Address Details",
# "items": {
# "type": "object",
# "title": "Address Detail",
# "properties": {
# "text": {
# "type": "string",
# "title": "Address Text"
# },
# "priority": {
# "type": "integer",
# "title": "Priority",
# "minimum": 0
# }
# },
# "required": ["text", "priority"]
# }
# }
# Apply JSON editor widgets
self.fields['translation'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(translation_schema),
'title': 'Translations'
})
self.fields['links'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(links_schema),
'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),
# self.fields['explanation'].widget = JsonEditorWidget(attrs={
# 'schema': json.dumps(explanation_schema),
# 'title': 'Explanations'
# })
# self.fields['address_details'].widget = JsonEditorWidget(attrs={
# 'schema': json.dumps(address_details_schema),
# 'title': 'Address Details'
# })
# Custom Forms for JSON Fields
@ -355,71 +90,11 @@ class HadisCollectionAdminForm(forms.ModelForm):
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"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'summary': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
}
# 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"""
@ -427,41 +102,10 @@ class HadisTagAdminForm(forms.ModelForm):
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"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
}
# 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"""
@ -469,71 +113,11 @@ class HadisStatusAdminForm(forms.ModelForm):
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"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
}
# 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"""
@ -541,41 +125,10 @@ class HadisReferenceAdminForm(forms.ModelForm):
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"]
}
widgets = {
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
}
# Apply JSON editor widgets
self.fields['description'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(description_schema),
'title': 'Descriptions'
})
# Inline Admin Classes
class ReferenceImageInline(TabularInline):
@ -619,11 +172,11 @@ class HadisTagAdmin(ModelAdmin):
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"""
"""Helper to safely extract the first 'title' 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 first_item.get('title', '-')
return '-'
@ -651,11 +204,11 @@ class HadisStatusAdmin(ModelAdmin):
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"""
"""Helper to safely extract the first 'title' 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 first_item.get('title', '-')
return '-'
@ -675,7 +228,7 @@ class HadisAdmin(ModelAdmin):
'fields': ('category', 'number', 'title', 'title_narrator', 'status', 'slug')
}),
(_('Content'), {
'fields': ('text', 'translation', 'explanation','description')
'fields': ('text', 'translation','description')
}),
(_('Status & Classification'), {
'fields': ('hadis_status','tags')
@ -695,11 +248,11 @@ class HadisAdmin(ModelAdmin):
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"""
"""Helper to safely extract the first 'title' 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 first_item.get('title', '-')
return '-'
@ -770,11 +323,11 @@ class HadisCollectionAdmin(ModelAdmin):
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"""
"""Helper to safely extract the first 'title' 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 first_item.get('title', '-')
return '-'
@display(description=_("Collection"), header=True)
@ -824,101 +377,12 @@ class HadisCorrectionAdminForm(forms.ModelForm):
class Meta:
model = HadisCorrection
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 translation JSON field
translation_schema = {
"type": "array",
"title": "Translations",
"items": {
"type": "object",
"title": "Translation",
"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": "Translation Text"
}
},
"required": ["language_code", "text"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
'translation': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
}
# 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['translation'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(translation_schema),
'title': 'Translations'
})
class HadisCorrectionAdmin(ModelAdmin):
"""Admin for HadisCorrection model"""
@ -951,11 +415,11 @@ class HadisCorrectionAdmin(ModelAdmin):
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"""
"""Helper to safely extract the first 'title' 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 first_item.get('title', '-')
return '-'

349
apps/hadis/admin/reference.py

@ -3,7 +3,8 @@ 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
from unfold.widgets import UnfoldAdminTextInputWidget, UnfoldAdminTextareaWidget, UnfoldAdminExpandableTextareaWidget
from utils.multilang_json_widget import MultiLanguageJSONWidget
import json
# Import your custom admin site
@ -29,131 +30,13 @@ class BookReferenceAdminForm(forms.ModelForm):
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"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
'language': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'publisher': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
}
# 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"""
@ -161,71 +44,11 @@ class BookAttributeAdminForm(forms.ModelForm):
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"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'value': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
}
# 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'
})
class BookAuthorAdminForm(forms.ModelForm):
"""Custom form for BookAuthor with JSON editor widgets"""
@ -233,41 +56,10 @@ class BookAuthorAdminForm(forms.ModelForm):
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"]
}
widgets = {
'name': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
}
# 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"""
@ -275,41 +67,10 @@ class BookReferenceImageAdminForm(forms.ModelForm):
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"]
}
widgets = {
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
}
# 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"""
@ -317,41 +78,10 @@ class BookSubjectAreaAdminForm(forms.ModelForm):
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"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
}
# 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"""
@ -359,41 +89,10 @@ class BookTypeAdminForm(forms.ModelForm):
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"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
}
# Apply JSON editor widgets
self.fields['title'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(title_schema),
'title': 'Titles'
})
# -----------------------------------------------------------------------------
# 1. Inlines
@ -476,11 +175,11 @@ class BookReferenceAdmin(ModelAdmin):
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"""
"""Helper to safely extract the first 'title' 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 first_item.get('title', '-')
return '-'
@ -520,7 +219,7 @@ class BookAuthorAdmin(ModelAdmin):
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 first.get('title', '-')
return '-'
class BookReferenceImageAdmin(ModelAdmin):
@ -534,10 +233,10 @@ class BookReferenceImageAdmin(ModelAdmin):
list_select_related = ("book_reference",)
def display_name(self, obj):
# Implements: f"{self.book_reference.title[0]['text']} - Image {self.order}"
# Implements: f"{self.book_reference.title[0]['title']} - Image {self.order}"
try:
# We use safe navigation to prevent admin crashes if data is missing
book_title = obj.book_reference.title[0]['text']
book_title = obj.book_reference.title[0]['title']
return f"{book_title} - Image {obj.order}"
except (AttributeError, IndexError, KeyError, TypeError):
# Fallback if the title structure isn't exactly as expected
@ -574,7 +273,7 @@ class BookAttributeAdmin(ModelAdmin):
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 first.get('title', '-')
return '-'

422
apps/hadis/admin/transmitter.py

@ -4,7 +4,8 @@ from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import display, action
from unfold.contrib.forms.widgets import WysiwygWidget
from utils.json_editor_field import JsonEditorWidget
from unfold.widgets import UnfoldAdminTextInputWidget, UnfoldAdminTextareaWidget, UnfoldAdminExpandableTextareaWidget
from utils.multilang_json_widget import MultiLanguageJSONWidget
import json
from utils.admin import dovoodi_admin_site
@ -20,9 +21,6 @@ class HadisTransmitterInline(TabularInline):
fields = ('hadis', 'order')
# (TransmitterOpinionInline and TransmitterOriginalTextInline moved after their forms)
# Custom Forms for JSON Fields
class TransmittersAdminForm(forms.ModelForm):
"""Custom form for Transmitters with JSON editor widgets"""
@ -30,89 +28,16 @@ class TransmittersAdminForm(forms.ModelForm):
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'
})
# (Admins moved down to avoid NameError)
widgets = {
'full_name': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'kunya': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'known_as': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'nickname': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'origin': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'lived_in': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'died_in': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
}
# Custom Forms for JSON Fields
@ -122,71 +47,11 @@ class NarratorLayerAdminForm(forms.ModelForm):
class Meta:
model = NarratorLayer
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"]
}
}
# 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"]
}
widgets = {
'name': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
}
# Apply JSON editor widgets
self.fields['name'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(name_schema),
'title': 'Names'
})
self.fields['description'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(description_schema),
'title': 'Descriptions'
})
class TransmitterReliabilityAdminForm(forms.ModelForm):
"""Custom form for TransmitterReliability with JSON editor widgets"""
@ -194,41 +59,10 @@ class TransmitterReliabilityAdminForm(forms.ModelForm):
class Meta:
model = TransmitterReliability
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"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
}
# Apply JSON editor widgets
self.fields['title'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(title_schema),
'title': 'Titles'
})
class OpinionStatusAdminForm(forms.ModelForm):
"""Custom form for OpinionStatus with JSON editor widgets"""
@ -236,41 +70,10 @@ class OpinionStatusAdminForm(forms.ModelForm):
class Meta:
model = OpinionStatus
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"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
}
# Apply JSON editor widgets
self.fields['title'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(title_schema),
'title': 'Titles'
})
class TransmitterOpinionAdminForm(forms.ModelForm):
"""Custom form for TransmitterOpinion with JSON editor widgets"""
@ -278,71 +81,11 @@ class TransmitterOpinionAdminForm(forms.ModelForm):
class Meta:
model = TransmitterOpinion
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Schema for scholar_name JSON field
scholar_name_schema = {
"type": "array",
"title": "Scholar Names",
"items": {
"type": "object",
"title": "Scholar 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": "Scholar Name Text"
}
},
"required": ["language_code", "text"]
}
}
# Schema for opinion_text JSON field
opinion_text_schema = {
"type": "array",
"title": "Opinion Texts",
"items": {
"type": "object",
"title": "Opinion 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": "Opinion Text"
}
},
"required": ["language_code", "text"]
}
widgets = {
'scholar_name': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'opinion_text': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
}
# Apply JSON editor widgets
self.fields['scholar_name'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(scholar_name_schema),
'title': 'Scholar Names'
})
self.fields['opinion_text'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(opinion_text_schema),
'title': 'Opinion Texts'
})
class TransmitterOriginalTextAdminForm(forms.ModelForm):
"""Custom form for TransmitterOriginalText with JSON editor widgets"""
@ -350,101 +93,12 @@ class TransmitterOriginalTextAdminForm(forms.ModelForm):
class Meta:
model = TransmitterOriginalText
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"]
}
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'text': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
'translation': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
}
# Schema for text JSON field
text_schema = {
"type": "array",
"title": "Texts",
"items": {
"type": "object",
"title": "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": "Text Content"
}
},
"required": ["language_code", "text"]
}
}
# Schema for translation JSON field
translation_schema = {
"type": "array",
"title": "Translations",
"items": {
"type": "object",
"title": "Translation",
"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": "Translation Text"
}
},
"required": ["language_code", "text"]
}
}
# Apply JSON editor widgets
self.fields['title'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(title_schema),
'title': 'Titles'
})
self.fields['text'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(text_schema),
'title': 'Texts'
})
self.fields['translation'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(translation_schema),
'title': 'Translations'
})
class TransmitterOpinionInline(TabularInline):
"""Inline for TransmitterOpinion in Transmitters admin"""
@ -490,13 +144,13 @@ class TransmittersAdmin(ModelAdmin):
@display(description=_('Full Name'), ordering='full_name')
def get_full_name_display(self, obj):
"""
Parses the JSON full_name and returns the first item's text.
Parses the JSON full_name and returns the first item's title.
"""
if obj.full_name and isinstance(obj.full_name, list) and len(obj.full_name) > 0:
# Safely get the first item
first_item = obj.full_name[0]
if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-'
@display(description=_("Transmitter"), header=True)
@ -552,11 +206,11 @@ class NarratorLayerAdmin(ModelAdmin):
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"""
"""Helper to safely extract the first 'title' 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 first_item.get('title', '-')
return '-'
@ -579,11 +233,11 @@ class TransmitterReliabilityAdmin(ModelAdmin):
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"""
"""Helper to safely extract the first 'title' 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 first_item.get('title', '-')
return '-'
@ -606,11 +260,11 @@ class OpinionStatusAdmin(ModelAdmin):
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"""
"""Helper to safely extract the first 'title' 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 first_item.get('title', '-')
return '-'
@ -642,11 +296,11 @@ class TransmitterOpinionAdmin(ModelAdmin):
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"""
"""Helper to safely extract the first 'title' 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 first_item.get('title', '-')
return '-'
@ -679,11 +333,11 @@ class TransmitterOriginalTextAdmin(ModelAdmin):
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"""
"""Helper to safely extract the first 'title' 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 first_item.get('title', '-')
return '-'

69
apps/hadis/management/commands/migrate_json_keys.py

@ -0,0 +1,69 @@
from django.core.management.base import BaseCommand
from django.db import transaction
from django.apps import apps
from django.db import models as django_models
class Command(BaseCommand):
help = "Rename 'text' key to 'title' in all multi-language JSON fields across Hadis app models."
@transaction.atomic
def handle(self, *args, **options):
hadis_app = apps.get_app_config('hadis')
models = hadis_app.get_models()
self.stdout.write("Starting key migration in JSON fields...")
for model in models:
json_fields = [
f.name for f in model._meta.get_fields()
if isinstance(f, django_models.JSONField)
]
if not json_fields:
continue
self.stdout.write(f"Checking model {model.__name__} for fields: {json_fields}")
queryset = model.objects.all()
for obj in queryset:
changed = False
for field in json_fields:
value = getattr(obj, field)
if not isinstance(value, list):
continue
new_list = []
field_changed = False
for item in value:
if isinstance(item, dict):
# Check if this item follows the multi-language pattern
# lang_key can be 'language_code', 'lang', or 'code'
lang_val = item.get("language_code") or item.get("lang") or item.get("code")
# text_val can be 'text' or 'title'
text_val = item.get("text")
if lang_val and text_val is not None:
# We found a multi-language item with a 'text' key
new_item = {
"language_code": str(lang_val),
"title": text_val
}
# Copy any other keys
for k, v in item.items():
if k not in ["language_code", "lang", "code", "text", "title"]:
new_item[k] = v
new_list.append(new_item)
field_changed = True
else:
new_list.append(item)
else:
new_list.append(item)
if field_changed:
setattr(obj, field, new_list)
changed = True
if changed:
obj.save()
self.stdout.write(f" Updated keys in {model.__name__} ID: {obj.id}")
self.stdout.write(self.style.SUCCESS("JSON key migration completed!"))

22
apps/hadis/models/category.py

@ -18,7 +18,7 @@ class HadisSect(models.Model):
order = models.IntegerField(default=0, verbose_name=_('order'))
def __str__(self):
title = self.title[0]['text'] if self.title else "Untitled Sect"
title = self.title[0].get('title') or self.title[0].get('text', '') if self.title else "Untitled Sect"
return f"{self.sect_type}: {title}"
def get_title(self,lang):
@ -31,11 +31,11 @@ class HadisSect(models.Model):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
def get_description(self,lang):
@ -48,11 +48,11 @@ class HadisSect(models.Model):
for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
@ -100,7 +100,7 @@ class HadisCategory(MPTTModel):
if self.title and isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0]
if isinstance(first_item, dict):
title_text = first_item.get('text', '').strip()
title_text = (first_item.get('title') or first_item.get('text') or '').strip()
if title_text:
base_slug = slugify(title_text, allow_unicode=True)
slug = base_slug
@ -175,7 +175,7 @@ class HadisCategory(MPTTModel):
ordering = ('order',)
def __str__(self):
title = self.title[0]['text'] if self.title else "Untitled Category"
title = self.title[0].get('title') or self.title[0].get('text', '') if self.title else "Untitled Category"
return f"{self.sect.sect_type}: {self.source_type} - {title}"
def get_title(self,lang):
@ -188,11 +188,11 @@ class HadisCategory(MPTTModel):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
def get_description(self,lang):
@ -205,11 +205,11 @@ class HadisCategory(MPTTModel):
for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None

46
apps/hadis/models/hadis.py

@ -79,7 +79,7 @@ class HadisCollection(models.Model):
super().save(*args, **kwargs)
def __str__(self):
return self.title[0]['text'] if self.title else "Untitled Collection"
return self.title[0]['title'] if self.title else "Untitled Collection"
def get_title(self,lang):
"""
@ -91,11 +91,11 @@ class HadisCollection(models.Model):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title', '')
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title', '')
return None
def get_summary(self,lang):
@ -108,11 +108,11 @@ class HadisCollection(models.Model):
for tr in self.summary:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title', '')
for tr in self.summary:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title', '')
return None
class Meta:
@ -144,7 +144,7 @@ class HadisTag(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self):
return self.title[0]['text'] if self.title else "Untagged"
return self.title[0]['title'] if self.title else "Untagged"
def get_title(self,lang):
"""
@ -156,11 +156,11 @@ class HadisTag(models.Model):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title', '')
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title', '')
return None
@ -232,7 +232,7 @@ class HadisStatus(ColorPaletteMixin,models.Model):
super().save(*args, **kwargs)
def __str__(self):
return self.title[0]['text'] if self.title else str(self.id)
return self.title[0]['title'] if self.title else str(self.id)
def get_title(self,lang):
"""
@ -248,7 +248,7 @@ class HadisStatus(ColorPaletteMixin,models.Model):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
class Meta:
@ -288,7 +288,7 @@ class Hadis(models.Model):
embedded_in = models.JSONField(default=list, blank=True)
def __str__(self):
title = self.title[0]['text'] if self.title else f"Hadis {self.number}"
title = self.title[0].get('title') or self.title[0].get('text', '') if self.title else f"Hadis {self.number}"
return f"{self.number} - {title}"
def save(self, *args, **kwargs):
@ -300,7 +300,7 @@ class Hadis(models.Model):
if self.title and isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0]
if isinstance(first_item, dict):
title_text = first_item.get("text")
title_text = first_item.get("title") or first_item.get("text")
# 2. If we found text, use smart slug
if title_text:
@ -355,18 +355,18 @@ class Hadis(models.Model):
# 1) exact language
for item in value:
if isinstance(item, dict) and item.get("language_code") == lang:
return item.get("text", "")
return item.get("title") or item.get("text") or ""
# 2) fallback language
if fallback and fallback != lang:
for item in value:
if isinstance(item, dict) and item.get("language_code") == fallback:
return item.get("text", "")
return item.get("title") or item.get("text") or ""
# 3) first available
item = value[0]
print(item)
return item.get("text", "") if isinstance(item, dict) else None
return item.get("title") or item.get("text") if isinstance(item, dict) else None
def get_translation(self, lang):
return self._get_json_field("translation" , lang)
@ -448,11 +448,11 @@ class HadisReference(models.Model):
for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
def __str__(self):
@ -507,7 +507,7 @@ class HadisCorrection(models.Model):
ordering = ("-created_at",)
def __str__(self):
title = self.title[0]['text'] if self.title else "No Title"
title = self.title[0].get('title') or self.title[0].get('text', '') if self.title else "No Title"
return f"{self.hadis.number} - {title}"
def save(self, *args, **kwargs):
@ -517,7 +517,7 @@ class HadisCorrection(models.Model):
if isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0]
if isinstance(first_item, dict):
title_text = first_item.get("text")
title_text = first_item.get("title") or first_item.get("text")
if title_text:
self.slug = generate_smart_slug(
@ -549,17 +549,17 @@ class HadisCorrection(models.Model):
def get_title(self, lang):
if not self.title or not isinstance(self.title, list): return None
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang: return tr.get('text', '')
if isinstance(tr, dict) and tr.get('language_code') == lang: return tr.get('title') or tr.get('text') or ''
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '')
if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('title') or tr.get('text') or ''
return None
def get_translation(self, lang):
if not self.translation or not isinstance(self.translation, list): return None
for tr in self.translation:
if isinstance(tr, dict) and tr.get('language_code') == lang: return tr.get('text', '')
if isinstance(tr, dict) and tr.get('language_code') == lang: return tr.get('title') or tr.get('text') or ''
for tr in self.translation:
if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '')
if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('title') or tr.get('text') or ''
return None

44
apps/hadis/models/reference.py

@ -10,7 +10,7 @@ class BookSubjectArea(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self):
return self.title[0]['text'] if self.title else str(self.id)
return self.title[0].get('title') or self.title[0].get('text', '') if self.title else str(self.id)
def get_title(self,lang):
"""
@ -22,11 +22,11 @@ class BookSubjectArea(models.Model):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
@ -36,7 +36,7 @@ class BookType(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self):
return self.title[0]['text'] if self.title else str(self.id)
return self.title[0].get('title') or self.title[0].get('text', '') if self.title else str(self.id)
def get_title(self,lang):
"""
@ -48,11 +48,11 @@ class BookType(models.Model):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
class BookReference(models.Model):
@ -100,7 +100,7 @@ class BookReference(models.Model):
ordering = ('-created_at',)
def __str__(self):
return self.title[0]['text'] if self.title else "Untitled Reference"
return self.title[0].get('title') or self.title[0].get('text', '') if self.title else "Untitled Reference"
def _get_json_field(self, field_name: str, lang: Optional[str]=None , fallback: str = "en"):
"""
@ -117,18 +117,18 @@ class BookReference(models.Model):
# 1) exact language
for item in value:
if isinstance(item, dict) and item.get("language_code") == lang:
return item.get("text", "")
return item.get("title") or item.get("text") or ""
# 2) fallback language
if fallback and fallback != lang:
for item in value:
if isinstance(item, dict) and item.get("language_code") == fallback:
return item.get("text", "")
return item.get("title") or item.get("text") or ""
# 3) first available
item = value[0]
print(item)
return item.get("text", "") if isinstance(item, dict) else None
return item.get("title") or item.get("text") if isinstance(item, dict) else None
@property
def share_link(self):
@ -157,7 +157,7 @@ class BookReference(models.Model):
if self.title and isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0]
if isinstance(first_item, dict):
title_text = first_item.get('text', '').strip()
title_text = (first_item.get('title') or first_item.get('text') or '').strip()
if title_text:
base_slug = slugify(title_text, allow_unicode=True)
slug = base_slug
@ -240,11 +240,11 @@ class BookReferenceImage(models.Model):
for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
def __str__(self):
@ -300,7 +300,7 @@ class BookAuthor(models.Model):
ordering = ['name']
def __str__(self):
return self.name[0]['text'] if self.name else "Unknown Author"
return self.name[0].get('title') or self.name[0].get('text', '') if self.name else "Unknown Author"
def get_name(self,lang):
"""
@ -312,11 +312,11 @@ class BookAuthor(models.Model):
for tr in self.name:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.name:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
@ -343,8 +343,8 @@ class BookAttribute(models.Model):
ordering = ['title']
def __str__(self):
title = self.title[0]['text'] if self.title else "No Title"
value = self.value[0]['text'] if self.value else "No Value"
title = self.title[0].get('title') or self.title[0].get('text', '') if self.title else "No Title"
value = self.value[0].get('title') or self.value[0].get('text', '') if self.value else "No Value"
return f"{title}: {value}"
def get_title(self,lang):
@ -357,11 +357,11 @@ class BookAttribute(models.Model):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
def get_value(self,lang):
@ -374,9 +374,9 @@ class BookAttribute(models.Model):
for tr in self.value:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.value:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None

60
apps/hadis/models/transmitter.py

@ -30,7 +30,7 @@ class NarratorLayer(models.Model):
ordering = ['number']
def __str__(self):
name = self.name[0]['text'] if self.name else f"Layer {self.number}"
name = self.name[0].get('title') or self.name[0].get('text', '') if self.name else f"Layer {self.number}"
return f"{_('Layer')} {self.number} - {name}"
def get_description(self,lang):
@ -43,11 +43,11 @@ class NarratorLayer(models.Model):
for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
def get_name(self,lang):
@ -60,11 +60,11 @@ class NarratorLayer(models.Model):
for tr in self.name:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.name:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
def save(self, *args, **kwargs):
@ -74,7 +74,7 @@ class NarratorLayer(models.Model):
if self.name and isinstance(self.name, list) and len(self.name) > 0:
first_item = self.name[0]
if isinstance(first_item, dict):
text = first_item.get('text', '').strip()
text = (first_item.get('title') or first_item.get('text') or '').strip()
if text:
slug = slugify(text)
# Ensure uniqueness
@ -135,7 +135,7 @@ class TransmitterReliability(ColorPaletteMixin, models.Model):
if self.title and isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0]
if isinstance(first_item, dict):
text = first_item.get('text', '').strip()
text = (first_item.get('title') or first_item.get('text') or '').strip()
if text:
slug = slugify(text)
# Ensure uniqueness
@ -185,7 +185,7 @@ class TransmitterReliability(ColorPaletteMixin, models.Model):
super().save(*args, **kwargs)
def __str__(self):
return self.title[0]['text'] if self.title else str(self.id)
return self.title[0].get('title') or self.title[0].get('text', '') if self.title else str(self.id)
def get_title(self,lang):
"""
@ -197,11 +197,11 @@ class TransmitterReliability(ColorPaletteMixin, models.Model):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
class Meta:
@ -314,7 +314,7 @@ class Transmitters(models.Model):
if self.full_name and isinstance(self.full_name, list) and len(self.full_name) > 0:
first_item = self.full_name[0]
if isinstance(first_item, dict):
name_text = first_item.get('text', '').strip()
name_text = (first_item.get('title') or first_item.get('text') or '').strip()
if name_text:
base_slug = slugify(name_text, allow_unicode=True)
slug = base_slug
@ -380,18 +380,18 @@ class Transmitters(models.Model):
# 1) exact language
for item in value:
if isinstance(item, dict) and item.get("language_code") == lang:
return item.get("text", "")
return item.get("title") or item.get("text") or ""
# 2) fallback language
if fallback and fallback != lang:
for item in value:
if isinstance(item, dict) and item.get("language_code") == fallback:
return item.get("text", "")
return item.get("title") or item.get("text") or ""
# 3) first available
item = value[0]
print(item)
return item.get("text", "") if isinstance(item, dict) else None
return item.get("title") or item.get("text") if isinstance(item, dict) else None
def get_full_name(self, lang):
return self._get_json_field("full_name" , lang)
@ -417,7 +417,7 @@ class Transmitters(models.Model):
def __str__(self):
if self.full_name and len(self.full_name) > 0:
return self.full_name[0].get('text', str(self.id))
return self.full_name[0].get('title') or self.full_name[0].get('text', str(self.id))
return str(self.id)
@ -498,7 +498,7 @@ class OpinionStatus(ColorPaletteMixin, models.Model):
if self.title and isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0]
if isinstance(first_item, dict):
text = first_item.get('text', '').strip()
text = (first_item.get('title') or first_item.get('text') or '').strip()
if text:
slug = slugify(text)
# Ensure uniqueness
@ -548,7 +548,7 @@ class OpinionStatus(ColorPaletteMixin, models.Model):
super().save(*args, **kwargs)
def __str__(self):
return self.title[0]['text'] if self.title else str(self.id)
return self.title[0].get('title') or self.title[0].get('text', '') if self.title else str(self.id)
def get_title(self,lang):
"""
@ -560,11 +560,11 @@ class OpinionStatus(ColorPaletteMixin, models.Model):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
class Meta:
@ -611,7 +611,7 @@ class TransmitterOpinion(models.Model):
ordering = ('-created_at',)
def __str__(self):
scholar = self.scholar_name[0]['text'] if self.scholar_name else "Unknown Scholar"
scholar = self.scholar_name[0].get('title') or self.scholar_name[0].get('text', '') if self.scholar_name else "Unknown Scholar"
return f"{scholar}'s opinion on {self.transmitter} ({self.status})"
def get_scholar_name(self,lang):
@ -624,11 +624,11 @@ class TransmitterOpinion(models.Model):
for tr in self.scholar_name:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.scholar_name:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
def get_opinion_tex(self,lang):
@ -641,11 +641,11 @@ class TransmitterOpinion(models.Model):
for tr in self.opinion_text:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.opinion_text:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
class TransmitterOriginalText(models.Model):
@ -671,7 +671,7 @@ class TransmitterOriginalText(models.Model):
verbose_name_plural = _('Transmitter Original Text')
def __str__(self):
title = self.title[0]['text'] if self.title else "Untitled"
title = self.title[0].get('title') or self.title[0].get('text', '') if self.title else "Untitled"
return f"{title} by {self.transmitter}"
def save(self, *args, **kwargs):
@ -686,7 +686,7 @@ class TransmitterOriginalText(models.Model):
if isinstance(self.title, list) and self.title:
first_item = self.title[0]
if isinstance(first_item, dict):
title_text = first_item.get("text")
title_text = first_item.get("title") or first_item.get("text")
# Generate smart slug
if title_text:
@ -734,11 +734,11 @@ class TransmitterOriginalText(models.Model):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
def get_opinion_tex(self,lang):
@ -752,11 +752,11 @@ class TransmitterOriginalText(models.Model):
for tr in self.opinion_text:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
for tr in self.opinion_text:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title') or tr.get('text') or ''
return None
from .reference import BookReference
class OriginalTextReference(models.Model):

16
apps/hadis/serializers/category.py

@ -10,7 +10,7 @@ def get_localized_text(json_list, request=None, fallback_lang="en", language_cod
"""
Extract localized text from a JSON list based on language.
Expects: [{"language_code": "en", "text": "..."}, ...]
Expects: [{"language_code": "en", "title": "..."}, ...]
Returns: Single text string or None
"""
if not json_list or not isinstance(json_list, list):
@ -25,16 +25,16 @@ def get_localized_text(json_list, request=None, fallback_lang="en", language_cod
# 1) Exact match
for item in json_list:
if isinstance(item, dict) and item.get("language_code") == language_code:
return item.get("text")
return item.get("title") or item.get("text")
# 2) Fallback to English
for item in json_list:
if isinstance(item, dict) and item.get("language_code") == "en":
return item.get("text")
return item.get("title") or item.get("text")
# 3) First available
if json_list and isinstance(json_list[0], dict):
return json_list[0].get("text")
return json_list[0].get("title") or json_list[0].get("text")
return None
@ -47,7 +47,7 @@ class LocalizedField(serializers.Field):
"""
def to_representation(self, value):
# Expecting value to be a list of {"language_code": "...", "text": "..."}
# Expecting value to be a list of {"language_code": "...", "title": "..."}
if not value or not isinstance(value, list):
return None
@ -60,16 +60,16 @@ class LocalizedField(serializers.Field):
# 1) Exact match with request language
for item in value:
if item.get("language_code") == language_code:
return item.get("text")
return item.get("title") or item.get("text")
# 2) Fallback to English
for item in value:
if item.get("language_code") == "en":
return item.get("text")
return item.get("title") or item.get("text")
# 3) Fallback to first item
first = value[0]
return first.get("text") if isinstance(first, dict) else None
return first.get("title") or first.get("text") if isinstance(first, dict) else None
class SimpleCategory(serializers.ModelSerializer):
title = LocalizedField()

6
apps/hadis/views/category.py

@ -429,16 +429,16 @@ class HadisCategoryXMindView(APIView):
# 1. Try specific language
for item in json_field:
if item.get('language_code') == lang:
return item.get('text', '')
return item.get('title') or item.get('text', '')
# 2. Fallback to English
for item in json_field:
if item.get('language_code') == 'en':
return item.get('text', '')
return item.get('title') or item.get('text', '')
# 3. Fallback to first available
if len(json_field) > 0:
return json_field[0].get('text', '')
return json_field[0].get('title') or json_field[0].get('text', '')
return "Unknown"

18
nginx/dovodi.conf

@ -17,7 +17,7 @@ server {
# با prefix زبانی
location /en/dovoodi/ {
proxy_pass http://88.99.212.243:8010;
proxy_pass http://88.99.212.243:8024;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@ -26,7 +26,7 @@ server {
}
location /fa/dovoodi/ {
proxy_pass http://88.99.212.243:8010;
proxy_pass http://88.99.212.243:8024;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@ -36,7 +36,7 @@ server {
# بدون prefix زبانی (fallback)
location /dovoodi/ {
proxy_pass http://88.99.212.243:8010;
proxy_pass http://88.99.212.243:8024;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@ -51,7 +51,7 @@ server {
}
location /admin/ {
proxy_pass http://88.99.212.243:8010;
proxy_pass http://88.99.212.243:8024;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@ -65,7 +65,7 @@ server {
# مسیر عمومی admin (سازگاری با config قدیمی)
location /en/admin {
proxy_pass http://88.99.212.243:8010;
proxy_pass http://88.99.212.243:8024;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
client_max_body_size 1200M;
@ -74,7 +74,7 @@ server {
# ========== Django Services ==========
location /api {
proxy_pass http://88.99.212.243:8010;
proxy_pass http://88.99.212.243:8024;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_method $request_method;
@ -88,7 +88,7 @@ server {
}
location /en/swagger {
proxy_pass http://88.99.212.243:8010;
proxy_pass http://88.99.212.243:8024;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header Authorization $http_authorization;
@ -96,13 +96,13 @@ server {
}
location /en/redoc {
proxy_pass http://88.99.212.243:8010;
proxy_pass http://88.99.212.243:8024;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
}
location /i18n/ {
proxy_pass http://88.99.212.243:8010;
proxy_pass http://88.99.212.243:8024;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}

158
templates/utils/widgets/links_json_widget.html

@ -0,0 +1,158 @@
{% load i18n %}
<div class="space-y-3 max-w-2xl" data-links-json data-field-name="{{ widget.field_name }}">
<div class="space-y-2" data-links-container>
{% for link in widget.links %}
<div class="flex items-center gap-2" data-link-row>
<div class="w-2/5">
<input type="text" value="{{ link.title }}" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl link-title" placeholder="{% trans 'Link Title (e.g. Source)' %}">
</div>
<div class="w-3/5">
<input type="url" value="{{ link.link }}" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl link-url" placeholder="{% trans 'URL (e.g. https://...)' %}">
</div>
<button type="button" class="remove-link-btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-2 text-sm transition-colors duration-150" title="Remove Link">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
{% empty %}
<div class="flex items-center gap-2" data-link-row>
<div class="w-2/5">
<input type="text" value="" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl link-title" placeholder="{% trans 'Link Title (e.g. Source)' %}">
</div>
<div class="w-3/5">
<input type="url" value="" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl link-url" placeholder="{% trans 'URL (e.g. https://...)' %}">
</div>
<button type="button" class="remove-link-btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-2 text-sm transition-colors duration-150" title="Remove Link">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
{% endfor %}
</div>
<button type="button" class="add-link-btn mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary-50 dark:hover:bg-primary-950/30 rounded border border-primary-200 dark:border-primary-800 transition-colors duration-150" data-add-link>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
{% trans "Add Link" %}
</button>
<input type="hidden" name="{{ widget.field_name }}" value='{{ widget.serialized|escapejs }}'>
</div>
<script>
(function () {
function init(root) {
if (root.getAttribute("data-initialized") === "true") return;
root.setAttribute("data-initialized", "true");
var fieldName = root.getAttribute("data-field-name");
if (!fieldName) return;
var container = root.querySelector("[data-links-container]");
var hidden = root.querySelector('input[type="hidden"][name="' + fieldName + '"]');
if (!container || !hidden) return;
function serialize() {
var result = [];
container.querySelectorAll('[data-link-row]').forEach(function (row) {
var titleInput = row.querySelector('.link-title');
var urlInput = row.querySelector('.link-url');
var titleVal = titleInput ? titleInput.value.trim() : '';
var urlVal = urlInput ? urlInput.value.trim() : '';
if (titleVal !== '' || urlVal !== '') {
result.push({ title: titleVal, link: urlVal });
}
});
try {
hidden.value = JSON.stringify(result);
} catch (e) {
console.error('JSON stringify error:', e);
}
}
function bindInputEvents(input) {
input.addEventListener('input', serialize);
}
container.querySelectorAll('input').forEach(bindInputEvents);
var addBtn = root.querySelector('[data-add-link]');
if (addBtn) {
addBtn.addEventListener('click', function () {
var row = document.createElement('div');
row.className = 'flex items-center gap-2 mt-2';
row.setAttribute('data-link-row', '');
var col1 = document.createElement('div');
col1.className = 'w-2/5';
var inputTitle = document.createElement('input');
inputTitle.type = 'text';
inputTitle.className = 'border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl link-title';
inputTitle.placeholder = 'Link Title (e.g. Source)';
bindInputEvents(inputTitle);
col1.appendChild(inputTitle);
var col2 = document.createElement('div');
col2.className = 'w-3/5';
var inputUrl = document.createElement('input');
inputUrl.type = 'url';
inputUrl.className = 'border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl link-url';
inputUrl.placeholder = 'URL (e.g. https://...)';
bindInputEvents(inputUrl);
col2.appendChild(inputUrl);
var delBtn = document.createElement('button');
delBtn.type = 'button';
delBtn.className = 'remove-link-btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-2 text-sm transition-colors duration-150';
delBtn.title = 'Remove Link';
delBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" /></svg>';
delBtn.addEventListener('click', function () {
row.remove();
serialize();
});
row.appendChild(col1);
row.appendChild(col2);
row.appendChild(delBtn);
container.appendChild(row);
inputTitle.focus();
serialize();
});
}
container.querySelectorAll('.remove-link-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var row = btn.closest('[data-link-row]');
if (row) {
row.remove();
serialize();
}
});
});
}
function initializeWidgets() {
document.querySelectorAll('[data-links-json]').forEach(init);
}
if (document.readyState === 'loading') {
document.addEventListener("DOMContentLoaded", initializeWidgets);
} else {
initializeWidgets();
}
setTimeout(initializeWidgets, 100);
document.addEventListener("formset:added", function (event) {
var newFormset = event.detail.formsetRow;
if (newFormset) {
newFormset.querySelectorAll('[data-links-json]').forEach(init);
}
});
})();
</script>

240
templates/utils/widgets/multilang_address_widget.html

@ -0,0 +1,240 @@
{% load i18n %}
<div class="space-y-3 max-w-2xl" data-multilang-address data-field-name="{{ widget.field_name }}">
<div>
<div class="w-full max-w-2xl pr-2">
<div class="flex items-center gap-2 py-1.5">
<span class="text-xs font-semibold text-font-subtle-light dark:text-font-subtle-dark uppercase tracking-wider">
{% trans "Select Language" %}:
</span>
<select class="lang-select border border-base-200 bg-white font-semibold min-w-[120px] rounded shadow-sm text-font-default-light text-xs focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 px-3 py-1.5" data-lang-select>
{% for code in widget.languages %}
<option value="{{ code }}">
{{ code|upper }}{% if widget.has_value_codes and code in widget.has_value_codes %} ✓{% endif %}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="space-y-3" data-inputs>
{% for item in widget.rendered_languages %}
<div class="hidden" data-input-wrapper data-lang-code="{{ item.code }}">
<div class="flex items-center gap-2 mb-1">
<span class="text-xs font-medium text-font-subtle-light dark:text-font-subtle-dark">
{{ item.code|upper }}
</span>
</div>
<div class="space-y-2" data-parts-container>
{% for part in item.parts %}
<div class="flex items-center gap-2" data-part-row>
<input type="text" value="{{ part }}" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl" placeholder="Address part (e.g. Book 1, Chapter 9)">
<button type="button" class="remove-part-btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-2 text-sm transition-colors duration-150" title="Remove part">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
{% empty %}
<div class="flex items-center gap-2" data-part-row>
<input type="text" value="" class="border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl" placeholder="Address part (e.g. Book 1, Chapter 9)">
<button type="button" class="remove-part-btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-2 text-sm transition-colors duration-150" title="Remove part">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
{% endfor %}
</div>
<button type="button" class="add-part-btn mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary-50 dark:hover:bg-primary-950/30 rounded border border-primary-200 dark:border-primary-800 transition-colors duration-150" data-add-part>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
{% trans "Add Part" %}
</button>
</div>
{% endfor %}
<input type="hidden" name="{{ widget.field_name }}" value='{{ widget.serialized|escapejs }}'>
</div>
<div class="text-xs text-font-subtle-light dark:text-font-subtle-dark">
{% trans "Click a language code to manage its address parts." %}
</div>
</div>
<style>
/* Reuse styling rules from main widget to maintain consistency */
[data-multilang-address] [data-input-wrapper].hidden {
display: none !important;
}
[data-multilang-address] [data-input-wrapper]:not(.hidden) {
display: block !important;
}
.lang-select {
cursor: pointer;
appearance: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E") !important;
background-repeat: no-repeat !important;
background-position: right 0.5rem center !important;
background-size: 1.25rem !important;
padding-right: 1.75rem !important;
}
[dir="rtl"] .lang-select {
background-position: left 0.5rem center !important;
padding-right: 0.75rem !important;
padding-left: 1.75rem !important;
}
</style>
<script>
(function () {
function init(root) {
if (root.getAttribute("data-initialized") === "true") return;
root.setAttribute("data-initialized", "true");
var fieldName = root.getAttribute("data-field-name");
if (!fieldName) return;
var select = root.querySelector("[data-lang-select]");
var inputsRoot = root.querySelector("[data-inputs]");
if (!inputsRoot || !select) return;
// First, hide all wrappers
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
w.classList.add("hidden");
});
var selectedCode = select.value;
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + selectedCode + '"]');
if (wrapper) {
wrapper.classList.remove("hidden");
}
select.addEventListener("change", function () {
var code = select.value;
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + code + '"]');
if (!wrapper) return;
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
w.classList.add("hidden");
});
wrapper.classList.remove("hidden");
var input = wrapper.querySelector('input[type="text"]');
if (input) {
setTimeout(function(){ input.focus(); }, 50);
}
});
var hidden = root.querySelector('input[type="hidden"][name="' + fieldName + '"]');
function serialize() {
if (!hidden) return;
var result = [];
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
var code = w.getAttribute('data-lang-code');
var parts = [];
w.querySelectorAll('input[type="text"]').forEach(function (inp) {
if (inp.value && inp.value.trim() !== '') {
parts.push(inp.value.trim());
}
});
var option = select.querySelector('option[value="' + code + '"]');
var baseText = code.toUpperCase();
if (parts.length > 0) {
result.push({ language_code: code, title: parts });
if (option) option.textContent = baseText + ' ✓';
} else {
if (option) option.textContent = baseText;
}
});
try {
hidden.value = JSON.stringify(result);
} catch (e) {
console.error('JSON stringify error:', e);
}
}
function bindInputEvents(input) {
input.addEventListener('input', serialize);
}
inputsRoot.querySelectorAll('input[type="text"]').forEach(bindInputEvents);
inputsRoot.querySelectorAll('[data-add-part]').forEach(function (addBtn) {
addBtn.addEventListener('click', function () {
var wrapper = addBtn.closest('[data-input-wrapper]');
var container = wrapper.querySelector('[data-parts-container]');
if (!container) return;
var row = document.createElement('div');
row.className = 'flex items-center gap-2 mt-2';
row.setAttribute('data-part-row', '');
var input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Address part (e.g. Book 1, Chapter 9)';
input.className = 'border border-base-200 bg-white font-medium min-w-20 placeholder-base-400 rounded shadow-sm text-font-default-light text-sm focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none group-[.errors]:border-red-600 group-[.errors]:focus:ring-red-200 dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 dark:group-[.errors]:border-red-500 dark:group-[.errors]:focus:ring-red-600/40 px-3 py-2 w-full max-w-2xl';
bindInputEvents(input);
var delBtn = document.createElement('button');
delBtn.type = 'button';
delBtn.className = 'remove-part-btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-2 text-sm transition-colors duration-150';
delBtn.title = 'Remove part';
delBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" /></svg>';
delBtn.addEventListener('click', function () {
row.remove();
serialize();
});
row.appendChild(input);
row.appendChild(delBtn);
container.appendChild(row);
input.focus();
serialize();
});
});
inputsRoot.querySelectorAll('.remove-part-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var row = btn.closest('[data-part-row]');
if (row) {
row.remove();
serialize();
}
});
});
}
function initializeWidgets() {
document.querySelectorAll('[data-multilang-address]').forEach(init);
}
if (document.readyState === 'loading') {
document.addEventListener("DOMContentLoaded", initializeWidgets);
} else {
initializeWidgets();
}
setTimeout(initializeWidgets, 100);
document.addEventListener("formset:added", function (event) {
var newFormset = event.detail.formsetRow;
if (newFormset) {
newFormset.querySelectorAll('[data-multilang-address]').forEach(init);
}
});
})();
</script>

237
templates/utils/widgets/multilang_json_widget.html

@ -1,17 +1,18 @@
{% load i18n %}
<div class="space-y-3 max-w-2xl" data-multilang-json data-field-name="{{ widget.field_name }}">
<div class="relative">
<div class="w-full max-w-2xl overflow-x-auto scrollbar-hover pr-2">
<div class="inline-flex flex-nowrap items-center gap-1 whitespace-nowrap py-1 min-w-max" data-lang-bar>
{% for code in widget.languages %}
<button type="button"
class="lang-btn px-3 py-1.5 rounded-md border transition-all duration-150 text-xs font-medium
border-gray-200 text-gray-700 hover:bg-gray-50 hover:border-gray-300
dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:border-gray-600{% if widget.has_value_codes and code in widget.has_value_codes %} has-value{% endif %}"
data-lang-code="{{ code }}">
{{ code|upper }}
</button>
{% endfor %}
<div>
<div class="w-full max-w-2xl pr-2">
<div class="flex items-center gap-2 py-1.5">
<span class="text-xs font-semibold text-font-subtle-light dark:text-font-subtle-dark uppercase tracking-wider">
{% trans "Select Language" %}:
</span>
<select class="lang-select border border-base-200 bg-white font-semibold min-w-[120px] rounded shadow-sm text-font-default-light text-xs focus:ring focus:ring-primary-300 focus:border-primary-600 focus:outline-none dark:bg-base-900 dark:border-base-700 dark:text-font-default-dark dark:focus:border-primary-600 dark:focus:ring-primary-700 dark:focus:ring-opacity-50 px-3 py-1.5" data-lang-select>
{% for code in widget.languages %}
<option value="{{ code }}">
{{ code|upper }}{% if widget.has_value_codes and code in widget.has_value_codes %} ✓{% endif %}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
@ -36,64 +37,6 @@
</div>
<style>
.scrollbar-hover {
--scrollbar-track: rgb(var(--color-base-100));
--scrollbar-thumb: rgb(var(--color-base-300));
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
scrollbar-width: none;
}
.scrollbar-hover:hover { scrollbar-width: thin; }
.dark .scrollbar-hover { --scrollbar-track: rgb(var(--color-base-800)); --scrollbar-thumb: rgb(var(--color-base-600)); }
.scrollbar-hover::-webkit-scrollbar { height: 0; }
.scrollbar-hover:hover::-webkit-scrollbar { height: 6px; }
.scrollbar-hover::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 3px; }
.scrollbar-hover::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; }
.scrollbar-hover::-webkit-scrollbar-corner { background: var(--scrollbar-track); }
.scrollbar-hover:hover::-webkit-scrollbar-thumb:hover { background: rgb(var(--color-base-400)); }
.dark .scrollbar-hover:hover::-webkit-scrollbar-thumb:hover { background: rgb(var(--color-base-500)); }
.lang-btn {
background: transparent;
cursor: pointer;
transition: all 0.15s ease;
}
.lang-btn:hover {
background: #f8f9fa;
border-color: #dee2e6;
}
.dark .lang-btn:hover {
background: #343a40;
border-color: #6c757d;
}
.lang-btn.is-active {
border-color: #3b82f6;
background: #eff6ff;
color: #1d4ed8;
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.25);
}
.dark .lang-btn.is-active {
border-color: #2563eb;
background: #1e3a8a;
color: #dbeafe;
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.5);
}
.lang-btn.has-value {
border-color: #3b82f6;
color: #1d4ed8;
}
.dark .lang-btn.has-value {
border-color: #3b82f6;
color: #dbeafe;
}
.lang-btn.is-active:hover {
background: #eff6ff;
border-color: #3b82f6;
}
.dark .lang-btn.is-active:hover {
background: #1e3a8a;
border-color: #2563eb;
}
/* Ensure hidden class works properly */
[data-input-wrapper].hidden {
display: none !important;
@ -102,125 +45,64 @@
[data-input-wrapper]:not(.hidden) {
display: block !important;
}
.lang-select {
cursor: pointer;
appearance: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E") !important;
background-repeat: no-repeat !important;
background-position: right 0.5rem center !important;
background-size: 1.25rem !important;
padding-right: 1.75rem !important;
}
[dir="rtl"] .lang-select {
background-position: left 0.5rem center !important;
padding-right: 0.75rem !important;
padding-left: 1.75rem !important;
}
</style>
<script>
(function () {
function init(root) {
console.log('Initializing multilang widget:', root);
if (root.getAttribute("data-initialized") === "true") return;
root.setAttribute("data-initialized", "true");
var fieldName = root.getAttribute("data-field-name");
if (!fieldName) {
console.log('No field name found');
return;
}
if (!fieldName) return;
var buttons = root.querySelectorAll(".lang-btn[data-lang-code]");
var select = root.querySelector("[data-lang-select]");
var inputsRoot = root.querySelector("[data-inputs]");
if (!inputsRoot) {
console.log('No inputs root found');
return;
}
console.log('Found', buttons.length, 'language buttons');
if (!inputsRoot || !select) return;
// First, hide all wrappers
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
w.classList.add("hidden");
});
var hasActiveLanguage = false;
var withValue = [];
var withoutValue = [];
buttons.forEach(function (btn) {
var code = btn.getAttribute("data-lang-code");
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + code + '"]');
var hasValue = false;
if (wrapper) {
var input = wrapper.querySelector('input[name="' + fieldName + '__' + code + '"], textarea[name="' + fieldName + '__' + code + '"], input[id*="' + fieldName + '__' + code + '"]');
hasValue = !!(input && input.value && input.value.trim() !== "");
}
if (hasValue) {
btn.classList.add("has-value");
withValue.push(btn);
if (!hasActiveLanguage && wrapper) {
btn.classList.add("is-active");
wrapper.classList.remove("hidden");
hasActiveLanguage = true;
console.log('Initializing with active language:', code);
}
} else {
withoutValue.push(btn);
}
});
if (!hasActiveLanguage && buttons.length) {
var firstBtn = (withValue[0] || buttons[0]);
var firstCode = firstBtn.getAttribute("data-lang-code");
var firstWrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + firstCode + '"]');
if (firstWrapper) {
firstBtn.classList.add("is-active");
firstWrapper.classList.remove("hidden");
console.log('Initializing with first language:', firstCode);
}
var selectedCode = select.value;
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + selectedCode + '"]');
if (wrapper) {
wrapper.classList.remove("hidden");
}
var bar = root.querySelector('[data-lang-bar]');
if (bar) {
withValue.concat(withoutValue).forEach(function (btn) {
bar.appendChild(btn);
});
}
buttons.forEach(function (btn) {
btn.addEventListener("click", function () {
console.log('Language button clicked:', btn.getAttribute("data-lang-code"));
var code = btn.getAttribute("data-lang-code");
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + code + '"]');
if (!wrapper) {
console.log('No wrapper found for code:', code);
return;
}
select.addEventListener("change", function () {
var code = select.value;
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + code + '"]');
if (!wrapper) return;
var isActive = btn.classList.contains("is-active");
console.log('Button is active:', isActive);
// Remove active class from all buttons
buttons.forEach(function (b) { b.classList.remove("is-active"); });
// Hide all input wrappers
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
w.classList.add("hidden");
console.log('Hiding wrapper for:', w.getAttribute('data-lang-code'));
});
// Always show the clicked wrapper and hide others
btn.classList.add("is-active");
wrapper.classList.remove("hidden");
console.log('Showing wrapper for:', code);
var input = wrapper.querySelector('input, textarea');
if (input) {
setTimeout(function(){ input.focus(); }, 50);
}
var hidden = root.querySelector('input[type="hidden"][name="' + fieldName + '"]');
if (hidden) {
var result = [];
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
var c = w.getAttribute('data-lang-code');
var inp = w.querySelector('input, textarea');
if (inp && inp.value && inp.value.trim() !== '') {
result.push({ language_code: c, title: inp.value });
}
});
try {
hidden.value = JSON.stringify(result);
console.log('Updated hidden field value:', hidden.value);
} catch (e) {
console.error('JSON stringify error:', e);
}
}
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
w.classList.add("hidden");
});
wrapper.classList.remove("hidden");
var input = wrapper.querySelector('input, textarea');
if (input) {
setTimeout(function(){ input.focus(); }, 50);
}
});
var hidden = root.querySelector('input[type="hidden"][name="' + fieldName + '"]');
@ -231,13 +113,14 @@
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
var c = w.getAttribute('data-lang-code');
var i = w.querySelector('input, textarea');
var option = select.querySelector('option[value="' + c + '"]');
var baseText = c.toUpperCase();
if (i && i.value && i.value.trim() !== '') {
result.push({ language_code: c, title: i.value });
var btn = root.querySelector('.lang-btn[data-lang-code="' + c + '"]');
if (btn) btn.classList.add('has-value');
if (option) option.textContent = baseText + ' ✓';
} else {
var btn2 = root.querySelector('.lang-btn[data-lang-code="' + c + '"]');
if (btn2) btn2.classList.remove('has-value');
if (option) option.textContent = baseText;
}
});
try { hidden.value = JSON.stringify(result); } catch (e) { console.error('JSON stringify error:', e); }
@ -247,19 +130,15 @@
}
function initializeWidgets() {
console.log('Initializing all multilang widgets');
document.querySelectorAll('[data-multilang-json]').forEach(init);
}
// Try multiple initialization methods
if (document.readyState === 'loading') {
document.addEventListener("DOMContentLoaded", initializeWidgets);
} else {
// DOM is already loaded
initializeWidgets();
}
// Also try after a short delay to ensure everything is ready
setTimeout(initializeWidgets, 100);
document.addEventListener("formset:added", function (event) {

198
utils/multilang_json_widget.py

@ -57,22 +57,26 @@ class MultiLanguageJSONWidget(Widget):
return child_media
def _get_active_language_codes(self) -> list[str]:
codes: list[str] = []
if Language is not None:
try:
codes = list(
Language.objects.filter(status=True).values_list("code", flat=True) # type: ignore[attr-defined]
)
except Exception:
try:
codes = list(Language.objects.values_list("code", flat=True))
except Exception:
codes = []
if not codes:
codes = [code for code, _ in getattr(settings, "LANGUAGES", [("en", "English")])]
return list(dict.fromkeys(codes))
return [
"ar", # Arabic
"tr", # Turkish
"fa", # Persian (Farsi)
"ur", # Urdu
"bn", # Bengali
"id", # Indonesian
"fr", # French
"ru", # Russian
"uz", # Uzbek
"ky", # Kyrgyz
"tg", # Tajik
"az", # Azerbaijani
"en", # English
"de", # German
"zh", # Mandarin Chinese
"ha", # Hausa
"sw", # Swahili
"es", # Spanish
]
def _normalize_value(self, value: Any) -> dict[str, Any]:
mapping: dict[str, Any] = {}
@ -185,6 +189,168 @@ class MultiLanguageJSONWidget(Widget):
return template.render(context)
class MultiLanguageAddressWidget(MultiLanguageJSONWidget):
"""
Unfold-styled widget for JSONField storing localized address parts.
Each language contains a list of address parts (e.g. book, chapter, verse).
"""
template_name = "utils/widgets/multilang_address_widget.html"
def __init__(self, attrs: Optional[dict[str, Any]] = None) -> None:
super().__init__(input_widget_class=None, attrs=attrs)
def _normalize_value(self, value: Any) -> dict[str, list[str]]:
mapping: dict[str, list[str]] = {}
if not value:
return mapping
if isinstance(value, str):
try:
value = json.loads(value)
except Exception:
return mapping
if isinstance(value, list):
for item in value:
if not isinstance(item, dict):
continue
code = (
item.get("language_code")
or item.get("code")
or item.get("lang")
or item.get("language")
)
title = item.get("title")
if code and isinstance(title, list):
mapping[str(code)] = [str(x) for x in title]
elif isinstance(value, dict):
for code, title in value.items():
if isinstance(title, list):
mapping[str(code)] = [str(x) for x in title]
else:
mapping[str(code)] = [str(title)]
return mapping
def get_context(self, name: str, value: Any, attrs: Optional[dict[str, Any]]):
context = Widget.get_context(self, name, value, attrs)
languages = self._get_active_language_codes()
values_map = self._normalize_value(value)
# Ensure languages include any language codes present in value
for code in values_map.keys():
if code not in languages:
languages.append(code)
# Reorder: languages with existing values first
codes_with_values = [code for code in languages if values_map.get(code)]
codes_without_values = [code for code in languages if code not in codes_with_values]
languages = [*codes_with_values, *codes_without_values]
rendered_languages: list[dict[str, Any]] = []
for code in languages:
parts = values_map.get(code) or []
rendered_languages.append({
"code": code,
"parts": parts,
})
# Prepare serialized hidden value (JSON string)
serialized_list: list[dict[str, Any]] = []
for code in languages:
parts = values_map.get(code)
if parts:
filtered_parts = [str(x).strip() for x in parts if str(x).strip()]
if filtered_parts:
serialized_list.append({"language_code": code, "title": filtered_parts})
context["widget"].update(
{
"languages": languages,
"rendered_languages": rendered_languages,
"field_name": name,
"serialized": json.dumps(serialized_list, ensure_ascii=False),
"has_value_codes": codes_with_values,
}
)
return context
def value_from_datadict(self, data, files, name):
hidden_value = data.get(name)
if hidden_value not in (None, ""):
return hidden_value
return "[]"
def value_omitted_from_data(self, data, files, name):
return name not in data
class LinksJSONWidget(Widget):
"""
Unfold-styled widget for JSONField storing a list of links (title, link).
"""
template_name = "utils/widgets/links_json_widget.html"
def __init__(self, attrs: Optional[dict[str, Any]] = None) -> None:
super().__init__(attrs)
def _normalize_value(self, value: Any) -> list[dict[str, str]]:
if not value:
return []
if isinstance(value, str):
try:
value = json.loads(value)
except Exception:
return []
if isinstance(value, list):
results = []
for item in value:
if isinstance(item, dict):
title = item.get("title", "")
link = item.get("link", "")
results.append({
"title": str(title),
"link": str(link)
})
return results
return []
def get_context(self, name: str, value: Any, attrs: Optional[dict[str, Any]]):
context = super().get_context(name, value, attrs)
links = self._normalize_value(value)
# Prepare serialized hidden value (JSON string)
serialized_list = [l for l in links if l.get("title") or l.get("link")]
context["widget"].update({
"field_name": name,
"links": links,
"serialized": json.dumps(serialized_list, ensure_ascii=False),
})
return context
def value_from_datadict(self, data, files, name):
hidden_value = data.get(name)
if hidden_value not in (None, ""):
return hidden_value
return "[]"
def value_omitted_from_data(self, data, files, name):
return name not in data
def render(self, name, value, attrs=None, renderer=None):
if value is None:
value = ''
context = self.get_context(name, value, attrs)
template = get_template(self.template_name)
return template.render(context)

Loading…
Cancel
Save