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. 420
      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. 209
      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.admin import ModelAdmin
from unfold.decorators import display, action from unfold.decorators import display, action
from mptt.admin import DraggableMPTTAdmin 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 import json
from utils.admin import dovoodi_admin_site from utils.admin import dovoodi_admin_site
@ -19,70 +20,10 @@ class HadisSectAdminForm(forms.ModelForm):
class Meta: class Meta:
model = HadisSect model = HadisSect
fields = '__all__' 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"]
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
} }
},
"text": {
"type": "string",
"title": "Description Text"
}
},
"required": ["language_code", "text"]
}
}
# Apply JSON editor widgets
self.fields['title'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(title_schema),
'title': 'Titles'
})
self.fields['description'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(description_schema),
'title': 'Descriptions'
})
class HadisCategoryAdminForm(forms.ModelForm): class HadisCategoryAdminForm(forms.ModelForm):
@ -91,70 +32,10 @@ class HadisCategoryAdminForm(forms.ModelForm):
class Meta: class Meta:
model = HadisCategory model = HadisCategory
fields = '__all__' 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"]
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
} }
},
"text": {
"type": "string",
"title": "Description Text"
}
},
"required": ["language_code", "text"]
}
}
# Apply JSON editor widgets
self.fields['title'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(title_schema),
'title': 'Titles'
})
self.fields['description'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(description_schema),
'title': 'Descriptions'
})
class HadisSectAdmin(ModelAdmin): class HadisSectAdmin(ModelAdmin):
@ -174,7 +55,7 @@ class HadisSectAdmin(ModelAdmin):
def display_title(self, obj): def display_title(self, obj):
"""Extracts text from the title JSON list""" """Extracts text from the title JSON list"""
try: try:
return obj.title[0]['text']
return obj.title[0]['title']
except (IndexError, KeyError, TypeError, AttributeError): except (IndexError, KeyError, TypeError, AttributeError):
return "No Title" return "No Title"
@ -205,7 +86,7 @@ class HadisCategoryAdmin(ModelAdmin):
"""Display indented title for tree structure using JSON text""" """Display indented title for tree structure using JSON text"""
try: try:
# Extract text from the first element of the title list # 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): except (IndexError, KeyError, TypeError, AttributeError):
title_text = "No Title" 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.admin import ModelAdmin, TabularInline
from unfold.contrib.forms.widgets import WysiwygWidget from unfold.contrib.forms.widgets import WysiwygWidget
from unfold.decorators import display, action 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 from utils.json_editor_field import JsonEditorWidget
import json import json
from utils.admin import dovoodi_admin_site,dovoodi_admin_site
from utils.admin import dovoodi_admin_site
from ..models import ( from ..models import (
Hadis, HadisReference, HadisTag, HadisStatus, ReferenceImage, Hadis, HadisReference, HadisTag, HadisStatus, ReferenceImage,
HadisCollection, HadisInCollection, HadisCorrection HadisCollection, HadisInCollection, HadisCorrection
@ -22,189 +24,18 @@ class HadisAdminForm(forms.ModelForm):
model = Hadis model = Hadis
fields = '__all__' fields = '__all__'
widgets = { 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): def __init__(self, *args, **kwargs):
super().__init__(*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 = { explanation_schema = {
"type": "array", "type": "array",
"title": "Explanations", "title": "Explanations",
@ -220,7 +51,7 @@ class HadisAdminForm(forms.ModelForm):
"enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"] "enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"]
} }
}, },
"text": {
"title": {
"type": "array", "type": "array",
"title": "Explanation Items", "title": "Explanation Items",
"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' # 'title': 'Explanations'
# }) # })
# self.fields['address_details'].widget = JsonEditorWidget(attrs={
# 'schema': json.dumps(address_details_schema),
# 'title': 'Address Details'
# })
# Custom Forms for JSON Fields # Custom Forms for JSON Fields
@ -355,71 +90,11 @@ class HadisCollectionAdminForm(forms.ModelForm):
class Meta: class Meta:
model = HadisCollection model = HadisCollection
fields = '__all__' 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): class HadisTagAdminForm(forms.ModelForm):
"""Custom form for HadisTag with JSON editor widgets""" """Custom form for HadisTag with JSON editor widgets"""
@ -427,41 +102,10 @@ class HadisTagAdminForm(forms.ModelForm):
class Meta: class Meta:
model = HadisTag model = HadisTag
fields = '__all__' 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): class HadisStatusAdminForm(forms.ModelForm):
"""Custom form for HadisStatus with JSON editor widgets""" """Custom form for HadisStatus with JSON editor widgets"""
@ -469,71 +113,11 @@ class HadisStatusAdminForm(forms.ModelForm):
class Meta: class Meta:
model = HadisStatus model = HadisStatus
fields = '__all__' 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): class HadisReferenceAdminForm(forms.ModelForm):
"""Custom form for HadisReference with JSON editor widgets""" """Custom form for HadisReference with JSON editor widgets"""
@ -541,41 +125,10 @@ class HadisReferenceAdminForm(forms.ModelForm):
class Meta: class Meta:
model = HadisReference model = HadisReference
fields = '__all__' 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 # Inline Admin Classes
class ReferenceImageInline(TabularInline): class ReferenceImageInline(TabularInline):
@ -619,11 +172,11 @@ class HadisTagAdmin(ModelAdmin):
return self._extract_first_text(obj.title) return self._extract_first_text(obj.title)
def _extract_first_text(self, json_data): 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: if json_data and isinstance(json_data, list) and len(json_data) > 0:
first_item = json_data[0] first_item = json_data[0]
if isinstance(first_item, dict): if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-' return '-'
@ -651,11 +204,11 @@ class HadisStatusAdmin(ModelAdmin):
return self._extract_first_text(obj.description) return self._extract_first_text(obj.description)
def _extract_first_text(self, json_data): 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: if json_data and isinstance(json_data, list) and len(json_data) > 0:
first_item = json_data[0] first_item = json_data[0]
if isinstance(first_item, dict): if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-' return '-'
@ -675,7 +228,7 @@ class HadisAdmin(ModelAdmin):
'fields': ('category', 'number', 'title', 'title_narrator', 'status', 'slug') 'fields': ('category', 'number', 'title', 'title_narrator', 'status', 'slug')
}), }),
(_('Content'), { (_('Content'), {
'fields': ('text', 'translation', 'explanation','description')
'fields': ('text', 'translation','description')
}), }),
(_('Status & Classification'), { (_('Status & Classification'), {
'fields': ('hadis_status','tags') 'fields': ('hadis_status','tags')
@ -695,11 +248,11 @@ class HadisAdmin(ModelAdmin):
return self._extract_first_text(obj.title) return self._extract_first_text(obj.title)
def _extract_first_text(self, json_data): 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: if json_data and isinstance(json_data, list) and len(json_data) > 0:
first_item = json_data[0] first_item = json_data[0]
if isinstance(first_item, dict): if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-' return '-'
@ -770,11 +323,11 @@ class HadisCollectionAdmin(ModelAdmin):
return self._extract_first_text(obj.title) return self._extract_first_text(obj.title)
def _extract_first_text(self, json_data): 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: if json_data and isinstance(json_data, list) and len(json_data) > 0:
first_item = json_data[0] first_item = json_data[0]
if isinstance(first_item, dict): if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-' return '-'
@display(description=_("Collection"), header=True) @display(description=_("Collection"), header=True)
@ -824,101 +377,12 @@ class HadisCorrectionAdminForm(forms.ModelForm):
class Meta: class Meta:
model = HadisCorrection model = HadisCorrection
fields = '__all__' 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): class HadisCorrectionAdmin(ModelAdmin):
"""Admin for HadisCorrection model""" """Admin for HadisCorrection model"""
@ -951,11 +415,11 @@ class HadisCorrectionAdmin(ModelAdmin):
return self._extract_first_text(obj.title) return self._extract_first_text(obj.title)
def _extract_first_text(self, json_data): 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: if json_data and isinstance(json_data, list) and len(json_data) > 0:
first_item = json_data[0] first_item = json_data[0]
if isinstance(first_item, dict): if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-' return '-'

349
apps/hadis/admin/reference.py

@ -3,7 +3,8 @@ from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin, TabularInline from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import display 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 json
# Import your custom admin site # Import your custom admin site
@ -29,131 +30,13 @@ class BookReferenceAdminForm(forms.ModelForm):
class Meta: class Meta:
model = BookReference model = BookReference
fields = '__all__' 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),
'language': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'publisher': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
} }
# Schema for language JSON field
language_schema = {
"type": "array",
"title": "Languages",
"items": {
"type": "object",
"title": "Language",
"properties": {
"language_code": {
"type": "string",
"title": "Language Code",
"enum": ["en", "fa", "ar", "ur", "ru"],
"options": {
"enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"]
}
},
"text": {
"type": "string",
"title": "Language Text"
}
},
"required": ["language_code", "text"]
}
}
# Schema for publisher JSON field
publisher_schema = {
"type": "array",
"title": "Publishers",
"items": {
"type": "object",
"title": "Publisher",
"properties": {
"language_code": {
"type": "string",
"title": "Language Code",
"enum": ["en", "fa", "ar", "ur", "ru"],
"options": {
"enum_titles": ["English", "Persian", "Arabic", "Urdu", "Russian"]
}
},
"text": {
"type": "string",
"title": "Publisher Text"
}
},
"required": ["language_code", "text"]
}
}
# Apply JSON editor widgets
self.fields['title'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(title_schema),
'title': 'Titles'
})
self.fields['description'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(description_schema),
'title': 'Descriptions'
})
self.fields['language'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(language_schema),
'title': 'Languages'
})
self.fields['publisher'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(publisher_schema),
'title': 'Publishers'
})
class BookAttributeAdminForm(forms.ModelForm): class BookAttributeAdminForm(forms.ModelForm):
"""Custom form for BookAttribute with JSON editor widgets""" """Custom form for BookAttribute with JSON editor widgets"""
@ -161,71 +44,11 @@ class BookAttributeAdminForm(forms.ModelForm):
class Meta: class Meta:
model = BookAttribute model = BookAttribute
fields = '__all__' 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): class BookAuthorAdminForm(forms.ModelForm):
"""Custom form for BookAuthor with JSON editor widgets""" """Custom form for BookAuthor with JSON editor widgets"""
@ -233,41 +56,10 @@ class BookAuthorAdminForm(forms.ModelForm):
class Meta: class Meta:
model = BookAuthor model = BookAuthor
fields = '__all__' 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): class BookReferenceImageAdminForm(forms.ModelForm):
"""Custom form for BookReferenceImage with JSON editor widgets""" """Custom form for BookReferenceImage with JSON editor widgets"""
@ -275,41 +67,10 @@ class BookReferenceImageAdminForm(forms.ModelForm):
class Meta: class Meta:
model = BookReferenceImage model = BookReferenceImage
fields = '__all__' 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): class BookSubjectAreaAdminForm(forms.ModelForm):
"""Custom form for BookSubjectArea with JSON editor widgets""" """Custom form for BookSubjectArea with JSON editor widgets"""
@ -317,41 +78,10 @@ class BookSubjectAreaAdminForm(forms.ModelForm):
class Meta: class Meta:
model = BookSubjectArea model = BookSubjectArea
fields = '__all__' 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): class BookTypeAdminForm(forms.ModelForm):
"""Custom form for BookType with JSON editor widgets""" """Custom form for BookType with JSON editor widgets"""
@ -359,40 +89,9 @@ class BookTypeAdminForm(forms.ModelForm):
class Meta: class Meta:
model = BookType model = BookType
fields = '__all__' 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"
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
} }
},
"required": ["language_code", "text"]
}
}
# Apply JSON editor widgets
self.fields['title'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(title_schema),
'title': 'Titles'
})
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ -476,11 +175,11 @@ class BookReferenceAdmin(ModelAdmin):
return self._extract_first_text(obj.publisher) return self._extract_first_text(obj.publisher)
def _extract_first_text(self, json_data): 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: if json_data and isinstance(json_data, list) and len(json_data) > 0:
first_item = json_data[0] first_item = json_data[0]
if isinstance(first_item, dict): if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-' return '-'
@ -520,7 +219,7 @@ class BookAuthorAdmin(ModelAdmin):
if obj.name and isinstance(obj.name, list) and len(obj.name) > 0: if obj.name and isinstance(obj.name, list) and len(obj.name) > 0:
first = obj.name[0] first = obj.name[0]
if isinstance(first, dict): if isinstance(first, dict):
return first.get('text', '-')
return first.get('title', '-')
return '-' return '-'
class BookReferenceImageAdmin(ModelAdmin): class BookReferenceImageAdmin(ModelAdmin):
@ -534,10 +233,10 @@ class BookReferenceImageAdmin(ModelAdmin):
list_select_related = ("book_reference",) list_select_related = ("book_reference",)
def display_name(self, obj): 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: try:
# We use safe navigation to prevent admin crashes if data is missing # 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}" return f"{book_title} - Image {obj.order}"
except (AttributeError, IndexError, KeyError, TypeError): except (AttributeError, IndexError, KeyError, TypeError):
# Fallback if the title structure isn't exactly as expected # 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: if json_data and isinstance(json_data, list) and len(json_data) > 0:
first = json_data[0] first = json_data[0]
if isinstance(first, dict): if isinstance(first, dict):
return first.get('text', '-')
return first.get('title', '-')
return '-' return '-'

420
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.admin import ModelAdmin, TabularInline
from unfold.decorators import display, action from unfold.decorators import display, action
from unfold.contrib.forms.widgets import WysiwygWidget 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 import json
from utils.admin import dovoodi_admin_site from utils.admin import dovoodi_admin_site
@ -20,9 +21,6 @@ class HadisTransmitterInline(TabularInline):
fields = ('hadis', 'order') fields = ('hadis', 'order')
# (TransmitterOpinionInline and TransmitterOriginalTextInline moved after their forms)
# Custom Forms for JSON Fields # Custom Forms for JSON Fields
class TransmittersAdminForm(forms.ModelForm): class TransmittersAdminForm(forms.ModelForm):
"""Custom form for Transmitters with JSON editor widgets""" """Custom form for Transmitters with JSON editor widgets"""
@ -30,89 +28,16 @@ class TransmittersAdminForm(forms.ModelForm):
class Meta: class Meta:
model = Transmitters model = Transmitters
fields = '__all__' 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
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),
} }
},
"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)
# Custom Forms for JSON Fields # Custom Forms for JSON Fields
@ -122,70 +47,10 @@ class NarratorLayerAdminForm(forms.ModelForm):
class Meta: class Meta:
model = NarratorLayer model = NarratorLayer
fields = '__all__' 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"
widgets = {
'name': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'description': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
} }
},
"required": ["language_code", "text"]
}
}
# 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): class TransmitterReliabilityAdminForm(forms.ModelForm):
@ -194,40 +59,9 @@ class TransmitterReliabilityAdminForm(forms.ModelForm):
class Meta: class Meta:
model = TransmitterReliability model = TransmitterReliability
fields = '__all__' 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): class OpinionStatusAdminForm(forms.ModelForm):
@ -236,40 +70,9 @@ class OpinionStatusAdminForm(forms.ModelForm):
class Meta: class Meta:
model = OpinionStatus model = OpinionStatus
fields = '__all__' 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"
widgets = {
'title': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
} }
},
"required": ["language_code", "text"]
}
}
# Apply JSON editor widgets
self.fields['title'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(title_schema),
'title': 'Titles'
})
class TransmitterOpinionAdminForm(forms.ModelForm): class TransmitterOpinionAdminForm(forms.ModelForm):
@ -278,70 +81,10 @@ class TransmitterOpinionAdminForm(forms.ModelForm):
class Meta: class Meta:
model = TransmitterOpinion model = TransmitterOpinion
fields = '__all__' 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"
widgets = {
'scholar_name': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextInputWidget),
'opinion_text': MultiLanguageJSONWidget(input_widget_class=UnfoldAdminTextareaWidget),
} }
},
"required": ["language_code", "text"]
}
}
# 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): class TransmitterOriginalTextAdminForm(forms.ModelForm):
@ -350,100 +93,11 @@ class TransmitterOriginalTextAdminForm(forms.ModelForm):
class Meta: class Meta:
model = TransmitterOriginalText model = TransmitterOriginalText
fields = '__all__' 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): class TransmitterOpinionInline(TabularInline):
@ -490,13 +144,13 @@ class TransmittersAdmin(ModelAdmin):
@display(description=_('Full Name'), ordering='full_name') @display(description=_('Full Name'), ordering='full_name')
def get_full_name_display(self, obj): 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: if obj.full_name and isinstance(obj.full_name, list) and len(obj.full_name) > 0:
# Safely get the first item # Safely get the first item
first_item = obj.full_name[0] first_item = obj.full_name[0]
if isinstance(first_item, dict): if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-' return '-'
@display(description=_("Transmitter"), header=True) @display(description=_("Transmitter"), header=True)
@ -552,11 +206,11 @@ class NarratorLayerAdmin(ModelAdmin):
return self._extract_first_text(obj.name) return self._extract_first_text(obj.name)
def _extract_first_text(self, json_data): 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: if json_data and isinstance(json_data, list) and len(json_data) > 0:
first_item = json_data[0] first_item = json_data[0]
if isinstance(first_item, dict): if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-' return '-'
@ -579,11 +233,11 @@ class TransmitterReliabilityAdmin(ModelAdmin):
return self._extract_first_text(obj.title) return self._extract_first_text(obj.title)
def _extract_first_text(self, json_data): 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: if json_data and isinstance(json_data, list) and len(json_data) > 0:
first_item = json_data[0] first_item = json_data[0]
if isinstance(first_item, dict): if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-' return '-'
@ -606,11 +260,11 @@ class OpinionStatusAdmin(ModelAdmin):
return self._extract_first_text(obj.title) return self._extract_first_text(obj.title)
def _extract_first_text(self, json_data): 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: if json_data and isinstance(json_data, list) and len(json_data) > 0:
first_item = json_data[0] first_item = json_data[0]
if isinstance(first_item, dict): if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-' return '-'
@ -642,11 +296,11 @@ class TransmitterOpinionAdmin(ModelAdmin):
return self._extract_first_text(obj.scholar_name) return self._extract_first_text(obj.scholar_name)
def _extract_first_text(self, json_data): 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: if json_data and isinstance(json_data, list) and len(json_data) > 0:
first_item = json_data[0] first_item = json_data[0]
if isinstance(first_item, dict): if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-' return '-'
@ -679,11 +333,11 @@ class TransmitterOriginalTextAdmin(ModelAdmin):
return self._extract_first_text(obj.title) return self._extract_first_text(obj.title)
def _extract_first_text(self, json_data): 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: if json_data and isinstance(json_data, list) and len(json_data) > 0:
first_item = json_data[0] first_item = json_data[0]
if isinstance(first_item, dict): if isinstance(first_item, dict):
return first_item.get('text', '-')
return first_item.get('title', '-')
return '-' 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')) order = models.IntegerField(default=0, verbose_name=_('order'))
def __str__(self): 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}" return f"{self.sect_type}: {title}"
def get_title(self,lang): def get_title(self,lang):
@ -31,11 +31,11 @@ class HadisSect(models.Model):
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
def get_description(self,lang): def get_description(self,lang):
@ -48,11 +48,11 @@ class HadisSect(models.Model):
for tr in self.description: for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
@ -100,7 +100,7 @@ class HadisCategory(MPTTModel):
if self.title and isinstance(self.title, list) and len(self.title) > 0: if self.title and isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0] first_item = self.title[0]
if isinstance(first_item, dict): 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: if title_text:
base_slug = slugify(title_text, allow_unicode=True) base_slug = slugify(title_text, allow_unicode=True)
slug = base_slug slug = base_slug
@ -175,7 +175,7 @@ class HadisCategory(MPTTModel):
ordering = ('order',) ordering = ('order',)
def __str__(self): 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}" return f"{self.sect.sect_type}: {self.source_type} - {title}"
def get_title(self,lang): def get_title(self,lang):
@ -188,11 +188,11 @@ class HadisCategory(MPTTModel):
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
def get_description(self,lang): def get_description(self,lang):
@ -205,11 +205,11 @@ class HadisCategory(MPTTModel):
for tr in self.description: for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None

46
apps/hadis/models/hadis.py

@ -79,7 +79,7 @@ class HadisCollection(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__(self): 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): def get_title(self,lang):
""" """
@ -91,11 +91,11 @@ class HadisCollection(models.Model):
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang: if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title', '')
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en': if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title', '')
return None return None
def get_summary(self,lang): def get_summary(self,lang):
@ -108,11 +108,11 @@ class HadisCollection(models.Model):
for tr in self.summary: for tr in self.summary:
if isinstance(tr, dict) and tr.get('language_code') == lang: if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title', '')
for tr in self.summary: for tr in self.summary:
if isinstance(tr, dict) and tr.get('language_code') == 'en': if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title', '')
return None return None
class Meta: class Meta:
@ -144,7 +144,7 @@ class HadisTag(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self): 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): def get_title(self,lang):
""" """
@ -156,11 +156,11 @@ class HadisTag(models.Model):
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang: if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('title', '')
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en': if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('title', '')
return None return None
@ -232,7 +232,7 @@ class HadisStatus(ColorPaletteMixin,models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__(self): 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): def get_title(self,lang):
""" """
@ -248,7 +248,7 @@ class HadisStatus(ColorPaletteMixin,models.Model):
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
class Meta: class Meta:
@ -288,7 +288,7 @@ class Hadis(models.Model):
embedded_in = models.JSONField(default=list, blank=True) embedded_in = models.JSONField(default=list, blank=True)
def __str__(self): 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}" return f"{self.number} - {title}"
def save(self, *args, **kwargs): 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: if self.title and isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0] first_item = self.title[0]
if isinstance(first_item, dict): 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 # 2. If we found text, use smart slug
if title_text: if title_text:
@ -355,18 +355,18 @@ class Hadis(models.Model):
# 1) exact language # 1) exact language
for item in value: for item in value:
if isinstance(item, dict) and item.get("language_code") == lang: 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 # 2) fallback language
if fallback and fallback != lang: if fallback and fallback != lang:
for item in value: for item in value:
if isinstance(item, dict) and item.get("language_code") == fallback: 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 # 3) first available
item = value[0] item = value[0]
print(item) 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): def get_translation(self, lang):
return self._get_json_field("translation" , lang) return self._get_json_field("translation" , lang)
@ -448,11 +448,11 @@ class HadisReference(models.Model):
for tr in self.description: for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
def __str__(self): def __str__(self):
@ -507,7 +507,7 @@ class HadisCorrection(models.Model):
ordering = ("-created_at",) ordering = ("-created_at",)
def __str__(self): 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}" return f"{self.hadis.number} - {title}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -517,7 +517,7 @@ class HadisCorrection(models.Model):
if isinstance(self.title, list) and len(self.title) > 0: if isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0] first_item = self.title[0]
if isinstance(first_item, dict): 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: if title_text:
self.slug = generate_smart_slug( self.slug = generate_smart_slug(
@ -549,17 +549,17 @@ class HadisCorrection(models.Model):
def get_title(self, lang): def get_title(self, lang):
if not self.title or not isinstance(self.title, list): return None if not self.title or not isinstance(self.title, list): return None
for tr in self.title: 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: 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 return None
def get_translation(self, lang): def get_translation(self, lang):
if not self.translation or not isinstance(self.translation, list): return None if not self.translation or not isinstance(self.translation, list): return None
for tr in self.translation: 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: 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 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')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self): 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): def get_title(self,lang):
""" """
@ -22,11 +22,11 @@ class BookSubjectArea(models.Model):
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
@ -36,7 +36,7 @@ class BookType(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self): 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): def get_title(self,lang):
""" """
@ -48,11 +48,11 @@ class BookType(models.Model):
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
class BookReference(models.Model): class BookReference(models.Model):
@ -100,7 +100,7 @@ class BookReference(models.Model):
ordering = ('-created_at',) ordering = ('-created_at',)
def __str__(self): 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"): 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 # 1) exact language
for item in value: for item in value:
if isinstance(item, dict) and item.get("language_code") == lang: 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 # 2) fallback language
if fallback and fallback != lang: if fallback and fallback != lang:
for item in value: for item in value:
if isinstance(item, dict) and item.get("language_code") == fallback: 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 # 3) first available
item = value[0] item = value[0]
print(item) 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 @property
def share_link(self): 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: if self.title and isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0] first_item = self.title[0]
if isinstance(first_item, dict): 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: if title_text:
base_slug = slugify(title_text, allow_unicode=True) base_slug = slugify(title_text, allow_unicode=True)
slug = base_slug slug = base_slug
@ -240,11 +240,11 @@ class BookReferenceImage(models.Model):
for tr in self.description: for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
def __str__(self): def __str__(self):
@ -300,7 +300,7 @@ class BookAuthor(models.Model):
ordering = ['name'] ordering = ['name']
def __str__(self): 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): def get_name(self,lang):
""" """
@ -312,11 +312,11 @@ class BookAuthor(models.Model):
for tr in self.name: for tr in self.name:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.name:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
@ -343,8 +343,8 @@ class BookAttribute(models.Model):
ordering = ['title'] ordering = ['title']
def __str__(self): 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}" return f"{title}: {value}"
def get_title(self,lang): def get_title(self,lang):
@ -357,11 +357,11 @@ class BookAttribute(models.Model):
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
def get_value(self,lang): def get_value(self,lang):
@ -374,9 +374,9 @@ class BookAttribute(models.Model):
for tr in self.value: for tr in self.value:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.value:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None

60
apps/hadis/models/transmitter.py

@ -30,7 +30,7 @@ class NarratorLayer(models.Model):
ordering = ['number'] ordering = ['number']
def __str__(self): 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}" return f"{_('Layer')} {self.number} - {name}"
def get_description(self,lang): def get_description(self,lang):
@ -43,11 +43,11 @@ class NarratorLayer(models.Model):
for tr in self.description: for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
def get_name(self,lang): def get_name(self,lang):
@ -60,11 +60,11 @@ class NarratorLayer(models.Model):
for tr in self.name: for tr in self.name:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.name:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
def save(self, *args, **kwargs): 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: if self.name and isinstance(self.name, list) and len(self.name) > 0:
first_item = self.name[0] first_item = self.name[0]
if isinstance(first_item, dict): 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: if text:
slug = slugify(text) slug = slugify(text)
# Ensure uniqueness # Ensure uniqueness
@ -135,7 +135,7 @@ class TransmitterReliability(ColorPaletteMixin, models.Model):
if self.title and isinstance(self.title, list) and len(self.title) > 0: if self.title and isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0] first_item = self.title[0]
if isinstance(first_item, dict): 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: if text:
slug = slugify(text) slug = slugify(text)
# Ensure uniqueness # Ensure uniqueness
@ -185,7 +185,7 @@ class TransmitterReliability(ColorPaletteMixin, models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__(self): 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): def get_title(self,lang):
""" """
@ -197,11 +197,11 @@ class TransmitterReliability(ColorPaletteMixin, models.Model):
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
class Meta: 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: if self.full_name and isinstance(self.full_name, list) and len(self.full_name) > 0:
first_item = self.full_name[0] first_item = self.full_name[0]
if isinstance(first_item, dict): 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: if name_text:
base_slug = slugify(name_text, allow_unicode=True) base_slug = slugify(name_text, allow_unicode=True)
slug = base_slug slug = base_slug
@ -380,18 +380,18 @@ class Transmitters(models.Model):
# 1) exact language # 1) exact language
for item in value: for item in value:
if isinstance(item, dict) and item.get("language_code") == lang: 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 # 2) fallback language
if fallback and fallback != lang: if fallback and fallback != lang:
for item in value: for item in value:
if isinstance(item, dict) and item.get("language_code") == fallback: 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 # 3) first available
item = value[0] item = value[0]
print(item) 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): def get_full_name(self, lang):
return self._get_json_field("full_name" , lang) return self._get_json_field("full_name" , lang)
@ -417,7 +417,7 @@ class Transmitters(models.Model):
def __str__(self): def __str__(self):
if self.full_name and len(self.full_name) > 0: 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) 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: if self.title and isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0] first_item = self.title[0]
if isinstance(first_item, dict): 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: if text:
slug = slugify(text) slug = slugify(text)
# Ensure uniqueness # Ensure uniqueness
@ -548,7 +548,7 @@ class OpinionStatus(ColorPaletteMixin, models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__(self): 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): def get_title(self,lang):
""" """
@ -560,11 +560,11 @@ class OpinionStatus(ColorPaletteMixin, models.Model):
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
class Meta: class Meta:
@ -611,7 +611,7 @@ class TransmitterOpinion(models.Model):
ordering = ('-created_at',) ordering = ('-created_at',)
def __str__(self): 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})" return f"{scholar}'s opinion on {self.transmitter} ({self.status})"
def get_scholar_name(self,lang): def get_scholar_name(self,lang):
@ -624,11 +624,11 @@ class TransmitterOpinion(models.Model):
for tr in self.scholar_name: for tr in self.scholar_name:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.scholar_name:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
def get_opinion_tex(self,lang): def get_opinion_tex(self,lang):
@ -641,11 +641,11 @@ class TransmitterOpinion(models.Model):
for tr in self.opinion_text: for tr in self.opinion_text:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.opinion_text:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
class TransmitterOriginalText(models.Model): class TransmitterOriginalText(models.Model):
@ -671,7 +671,7 @@ class TransmitterOriginalText(models.Model):
verbose_name_plural = _('Transmitter Original Text') verbose_name_plural = _('Transmitter Original Text')
def __str__(self): 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}" return f"{title} by {self.transmitter}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -686,7 +686,7 @@ class TransmitterOriginalText(models.Model):
if isinstance(self.title, list) and self.title: if isinstance(self.title, list) and self.title:
first_item = self.title[0] first_item = self.title[0]
if isinstance(first_item, dict): 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 # Generate smart slug
if title_text: if title_text:
@ -734,11 +734,11 @@ class TransmitterOriginalText(models.Model):
for tr in self.title: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
def get_opinion_tex(self,lang): def get_opinion_tex(self,lang):
@ -752,11 +752,11 @@ class TransmitterOriginalText(models.Model):
for tr in self.opinion_text: for tr in self.opinion_text:
if isinstance(tr, dict) and tr.get('language_code') == lang: 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: for tr in self.opinion_text:
if isinstance(tr, dict) and tr.get('language_code') == 'en': 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 return None
from .reference import BookReference from .reference import BookReference
class OriginalTextReference(models.Model): 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. 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 Returns: Single text string or None
""" """
if not json_list or not isinstance(json_list, list): 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 # 1) Exact match
for item in json_list: for item in json_list:
if isinstance(item, dict) and item.get("language_code") == language_code: 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 # 2) Fallback to English
for item in json_list: for item in json_list:
if isinstance(item, dict) and item.get("language_code") == "en": 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 # 3) First available
if json_list and isinstance(json_list[0], dict): 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 return None
@ -47,7 +47,7 @@ class LocalizedField(serializers.Field):
""" """
def to_representation(self, value): 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): if not value or not isinstance(value, list):
return None return None
@ -60,16 +60,16 @@ class LocalizedField(serializers.Field):
# 1) Exact match with request language # 1) Exact match with request language
for item in value: for item in value:
if item.get("language_code") == language_code: if item.get("language_code") == language_code:
return item.get("text")
return item.get("title") or item.get("text")
# 2) Fallback to English # 2) Fallback to English
for item in value: for item in value:
if item.get("language_code") == "en": if item.get("language_code") == "en":
return item.get("text")
return item.get("title") or item.get("text")
# 3) Fallback to first item # 3) Fallback to first item
first = value[0] 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): class SimpleCategory(serializers.ModelSerializer):
title = LocalizedField() title = LocalizedField()

6
apps/hadis/views/category.py

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

18
nginx/dovodi.conf

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

209
templates/utils/widgets/multilang_json_widget.html

@ -1,17 +1,18 @@
{% load i18n %} {% load i18n %}
<div class="space-y-3 max-w-2xl" data-multilang-json data-field-name="{{ widget.field_name }}"> <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>
<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 %} {% 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>
<option value="{{ code }}">
{{ code|upper }}{% if widget.has_value_codes and code in widget.has_value_codes %} ✓{% endif %}
</option>
{% endfor %} {% endfor %}
</select>
</div> </div>
</div> </div>
</div> </div>
@ -36,64 +37,6 @@
</div> </div>
<style> <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 */ /* Ensure hidden class works properly */
[data-input-wrapper].hidden { [data-input-wrapper].hidden {
display: none !important; display: none !important;
@ -102,125 +45,64 @@
[data-input-wrapper]:not(.hidden) { [data-input-wrapper]:not(.hidden) {
display: block !important; 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> </style>
<script> <script>
(function () { (function () {
function init(root) { 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"); 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]"); 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 // First, hide all wrappers
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) { inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
w.classList.add("hidden"); 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;
var selectedCode = select.value;
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + selectedCode + '"]');
if (wrapper) { 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"); 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 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");
select.addEventListener("change", function () {
var code = select.value;
var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + code + '"]'); var wrapper = inputsRoot.querySelector('[data-input-wrapper][data-lang-code="' + code + '"]');
if (!wrapper) {
console.log('No wrapper found for code:', code);
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"); });
if (!wrapper) return;
// Hide all input wrappers
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) { inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
w.classList.add("hidden"); 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"); wrapper.classList.remove("hidden");
console.log('Showing wrapper for:', code);
var input = wrapper.querySelector('input, textarea'); var input = wrapper.querySelector('input, textarea');
if (input) { if (input) {
setTimeout(function(){ input.focus(); }, 50); 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);
}
}
});
}); });
var hidden = root.querySelector('input[type="hidden"][name="' + fieldName + '"]'); var hidden = root.querySelector('input[type="hidden"][name="' + fieldName + '"]');
@ -231,13 +113,14 @@
inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) { inputsRoot.querySelectorAll('[data-input-wrapper]').forEach(function (w) {
var c = w.getAttribute('data-lang-code'); var c = w.getAttribute('data-lang-code');
var i = w.querySelector('input, textarea'); var i = w.querySelector('input, textarea');
var option = select.querySelector('option[value="' + c + '"]');
var baseText = c.toUpperCase();
if (i && i.value && i.value.trim() !== '') { if (i && i.value && i.value.trim() !== '') {
result.push({ language_code: c, title: i.value }); 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 { } 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); } try { hidden.value = JSON.stringify(result); } catch (e) { console.error('JSON stringify error:', e); }
@ -247,19 +130,15 @@
} }
function initializeWidgets() { function initializeWidgets() {
console.log('Initializing all multilang widgets');
document.querySelectorAll('[data-multilang-json]').forEach(init); document.querySelectorAll('[data-multilang-json]').forEach(init);
} }
// Try multiple initialization methods
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener("DOMContentLoaded", initializeWidgets); document.addEventListener("DOMContentLoaded", initializeWidgets);
} else { } else {
// DOM is already loaded
initializeWidgets(); initializeWidgets();
} }
// Also try after a short delay to ensure everything is ready
setTimeout(initializeWidgets, 100); setTimeout(initializeWidgets, 100);
document.addEventListener("formset:added", function (event) { document.addEventListener("formset:added", function (event) {

198
utils/multilang_json_widget.py

@ -57,22 +57,26 @@ class MultiLanguageJSONWidget(Widget):
return child_media return child_media
def _get_active_language_codes(self) -> list[str]: 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]: def _normalize_value(self, value: Any) -> dict[str, Any]:
mapping: dict[str, Any] = {} mapping: dict[str, Any] = {}
@ -185,6 +189,168 @@ class MultiLanguageJSONWidget(Widget):
return template.render(context) 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