4 Commits

  1. 18
      apps/article/migrations/0004_alter_articlecollection_slug.py
  2. 2
      apps/article/models.py
  3. 46
      apps/dobodbi_calendar/migrations/0002_alter_calendaroccasions_title.py
  4. 78
      apps/hadis/migrations/0032_alter_bookauthor_slug_alter_bookreference_slug_and_more.py
  5. 10
      apps/hadis/models/hadis.py
  6. 6
      apps/hadis/models/reference.py
  7. 10
      apps/hadis/models/transmitter.py
  8. 51
      apps/hadis/serializers/serializers_admin.py
  9. 33
      apps/library/migrations/0008_alter_author_slug_alter_book_slug_and_more.py
  10. 8
      apps/library/models.py
  11. 16
      apps/library/serializers_admin.py
  12. 18
      apps/podcast/migrations/0004_alter_podcastcollection_slug.py
  13. 2
      apps/podcast/models.py
  14. 1
      apps/podcast/views.py
  15. 18
      apps/video/migrations/0004_alter_videocollection_slug.py
  16. 2
      apps/video/models.py
  17. 1
      apps/video/views.py

18
apps/article/migrations/0004_alter_articlecollection_slug.py

@ -0,0 +1,18 @@
# Generated by Django 4.2.30 on 2026-07-20 17:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('article', '0003_remove_articlecollection_pin_top'),
]
operations = [
migrations.AlterField(
model_name='articlecollection',
name='slug',
field=models.SlugField(allow_unicode=True, max_length=255, unique=True),
),
]

2
apps/article/models.py

@ -30,7 +30,7 @@ class ArticleCollection(LowercaseSlugMixin, models.Model):
MIDDLE = 'middle', _('Middle Section')
title = models.CharField(max_length=255, help_text="This title will not be displayed anywhere")
slug = models.SlugField(max_length=255, unique=True)
slug = models.SlugField(max_length=255, unique=True, allow_unicode=True)
summary = models.CharField(max_length=512, null=True, blank=True, help_text=_('could be null'))
thumbnail = models.ImageField(upload_to='article/collection/', null=True, blank=True, help_text=_('image allowed'))
order = models.IntegerField(default=0, verbose_name=_('order'))

46
apps/dobodbi_calendar/migrations/0002_alter_calendaroccasions_title.py

@ -1,8 +1,53 @@
# Generated by Django 4.2.30 on 2026-07-19 12:21
import json
from django.db import migrations, models
def convert_titles_to_json(apps, schema_editor):
CalendarOccasions = apps.get_model('dobodbi_calendar', 'CalendarOccasions')
for occasion in CalendarOccasions.objects.all():
title_str = occasion.title
if not title_str:
occasion.title = '[]'
occasion.save()
continue
# Check if it's already a valid JSON list/dict
try:
parsed = json.loads(title_str)
if isinstance(parsed, (list, dict)):
# Already valid JSON
continue
except Exception:
pass
# Convert plain text to JSON list
new_title = [{"language_code": "fa", "title": title_str}]
occasion.title = json.dumps(new_title, ensure_ascii=False)
occasion.save()
def convert_json_to_titles(apps, schema_editor):
CalendarOccasions = apps.get_model('dobodbi_calendar', 'CalendarOccasions')
for occasion in CalendarOccasions.objects.all():
title_str = occasion.title
try:
parsed = json.loads(title_str)
if isinstance(parsed, list) and len(parsed) > 0:
# Try English first, then fallback
val = ""
for item in parsed:
if isinstance(item, dict):
val = item.get("title") or item.get("text") or val
if item.get("language_code") == "en":
break
occasion.title = val
occasion.save()
except Exception:
pass
class Migration(migrations.Migration):
dependencies = [
@ -10,6 +55,7 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(convert_titles_to_json, convert_json_to_titles),
migrations.AlterField(
model_name='calendaroccasions',
name='title',

78
apps/hadis/migrations/0032_alter_bookauthor_slug_alter_bookreference_slug_and_more.py

@ -0,0 +1,78 @@
# Generated by Django 4.2.30 on 2026-07-20 17:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hadis', '0031_remove_hadiscollection_pin_top_and_more'),
]
operations = [
migrations.AlterField(
model_name='bookauthor',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, unique=True, verbose_name='slug'),
),
migrations.AlterField(
model_name='bookreference',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, unique=True, verbose_name='slug'),
),
migrations.AlterField(
model_name='bookresearcher',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, verbose_name='slug'),
),
migrations.AlterField(
model_name='hadis',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, unique=True, verbose_name='slug'),
),
migrations.AlterField(
model_name='hadiscollection',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, unique=True, verbose_name='slug'),
),
migrations.AlterField(
model_name='hadiscorrection',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, unique=True, verbose_name='slug'),
),
migrations.AlterField(
model_name='hadisinterpretation',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, verbose_name='slug'),
),
migrations.AlterField(
model_name='hadisstatus',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, unique=True, verbose_name='slug'),
),
migrations.AlterField(
model_name='narratorlayer',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, unique=True, verbose_name='slug'),
),
migrations.AlterField(
model_name='opinionstatus',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, verbose_name='slug'),
),
migrations.AlterField(
model_name='transmitteroriginaltext',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, unique=True, verbose_name='slug'),
),
migrations.AlterField(
model_name='transmitterreliability',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, verbose_name='slug'),
),
migrations.AlterField(
model_name='transmitters',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, unique=True, verbose_name='slug'),
),
]

10
apps/hadis/models/hadis.py

@ -16,7 +16,7 @@ class HadisCollection(LowercaseSlugMixin, models.Model):
slug_source_field = 'title'
title = models.JSONField(default = list , verbose_name=_('Title'))
slug = models.SlugField(max_length=255, unique=True, verbose_name=_('slug'), blank=True)
slug = models.SlugField(max_length=255, unique=True, verbose_name=_('slug'), blank=True, allow_unicode=True)
summary = models.JSONField(default = list , verbose_name=_('Summary'))
status = models.BooleanField(default=True, verbose_name=_('status'))
display_position = models.CharField(
@ -120,7 +120,7 @@ class HadisStatus(ColorPaletteMixin, LowercaseSlugMixin, models.Model):
slug_source_field = 'title'
title = models.JSONField(default = list , verbose_name=_('Title'))
slug= models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique = True)
slug= models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique = True, allow_unicode=True)
# color = models.CharField(max_length=20, choices=ColorChoices.choices, verbose_name=_('color'))
order = models.IntegerField(default=0, verbose_name=_('order'))
description = models.JSONField(default = list , verbose_name=_('Description'))
@ -158,7 +158,7 @@ class Hadis(LowercaseSlugMixin, models.Model):
category = models.ForeignKey("hadis.HadisCategory", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('category'))
number = models.PositiveIntegerField(verbose_name=_('number'), default=1)
legacy_id = models.CharField(max_length=255, unique=True, null=True, blank=True, db_index=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True, allow_unicode=True)
title_narrator = models.JSONField(default = list , verbose_name=_('Title Narrator'))
title = models.JSONField(default = list , verbose_name=_('Title'))
description = models.JSONField(default = list , verbose_name=_('Description'))
@ -388,7 +388,7 @@ class ReferenceImage(models.Model):
class HadisCorrection(models.Model):
hadis = models.ForeignKey(Hadis, verbose_name=_("hadis correction"), on_delete=models.CASCADE)
title = models.JSONField(default = list , verbose_name=_('Title'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True, allow_unicode=True)
legacy_id = models.CharField(max_length=255, unique=True, null=True, blank=True, db_index=True)
narrator = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Narrator'))
links = models.JSONField(verbose_name=_('links'), null=True, blank=True, default=dict)
@ -447,7 +447,7 @@ class HadisCorrection(models.Model):
class HadisInterpretation(models.Model):
category = models.ForeignKey('HadisCategory', on_delete=models.CASCADE, related_name='interpretations', verbose_name=_('Category'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True, db_index=True) # 👈 فیلد جدید
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True, db_index=True, allow_unicode=True) # 👈 فیلد جدید
legacy_id = models.CharField(max_length=255, unique=True, null=True, blank=True, db_index=True)
narrator = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Narrator'))
links = models.JSONField(verbose_name=_('links'), null=True, blank=True, default=dict)

6
apps/hadis/models/reference.py

@ -95,7 +95,7 @@ class BookReference(LowercaseSlugMixin, models.Model):
description = models.JSONField(default = list , verbose_name=_('Description'))
language = models.JSONField(default = list , verbose_name=_('Language'))
number_page = models.PositiveIntegerField(verbose_name=_('number of pages'), blank=True, null=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True, allow_unicode=True)
subject_area = models.ManyToManyField(
'BookSubjectArea',
verbose_name=_('Subject Area'),
@ -329,7 +329,7 @@ class BookAuthor(LowercaseSlugMixin, models.Model):
Model for book reference authors
"""
name = models.JSONField(default=list, verbose_name=_('Name'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), null=True, blank=True, unique=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), null=True, blank=True, unique=True, allow_unicode=True)
related_narrator = models.OneToOneField(
'hadis.Transmitters',
@ -466,7 +466,7 @@ class BookResearcher(LowercaseSlugMixin, models.Model):
verbose_name=_('Author')
)
name = models.JSONField(default=list, blank=True, verbose_name=_('Name'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), null=True, blank=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), null=True, blank=True, allow_unicode=True)
class Meta:
verbose_name = _('Book Researcher')

10
apps/hadis/models/transmitter.py

@ -20,7 +20,7 @@ class NarratorLayer(LowercaseSlugMixin, models.Model):
name = models.JSONField(default = list , verbose_name=_('Name'))
number = models.PositiveIntegerField(verbose_name=_('layer number'), unique=True)
description = models.JSONField(default = list , verbose_name=_('Description'))
slug = models.SlugField(max_length=255,unique=True, verbose_name=_('slug'), blank=True)
slug = models.SlugField(max_length=255,unique=True, verbose_name=_('slug'), blank=True, allow_unicode=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
@ -127,7 +127,7 @@ class TransmitterReliability(ColorPaletteMixin, LowercaseSlugMixin, models.Model
slug_source_field = 'title'
title = models.JSONField(default = list , verbose_name=_('Title'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,null=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,null=True, allow_unicode=True)
@ -183,7 +183,7 @@ class Transmitters(LowercaseSlugMixin, models.Model):
kunya = models.JSONField(default = list , verbose_name=_('Kunya'))
known_as = models.JSONField(default = list , verbose_name=_('Known as'))
nickname = models.JSONField(default = list , verbose_name=_('Nick Name'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True, allow_unicode=True)
legacy_number = models.PositiveIntegerField(unique=True, null=True, blank=True, db_index=True)
legacy_id = models.CharField(max_length=255, unique=True, null=True, blank=True, db_index=True)
relatives_raw = models.JSONField(default=dict, blank=True, verbose_name=_('Raw Relatives Data'))
@ -389,7 +389,7 @@ class OpinionStatus(ColorPaletteMixin, LowercaseSlugMixin, models.Model):
slug_source_field = 'title'
title = models.JSONField(default = list , verbose_name=_('Title'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,null=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,null=True, allow_unicode=True)
@ -510,7 +510,7 @@ class TransmitterOriginalText(models.Model):
verbose_name=_('transmitter'),
related_name='originaltexts'
)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True, allow_unicode=True)
title = models.JSONField(default = list , verbose_name=_('Title'))
text = models.JSONField(default = list , verbose_name=_('Text'))
translation = models.JSONField(verbose_name=_('translation'), default=list)

51
apps/hadis/serializers/serializers_admin.py

@ -128,7 +128,15 @@ class AdminHadisCollectionSerializer(serializers.ModelSerializer):
read_only_fields = ["id", "created_at", "updated_at"]
def to_internal_value(self, data):
import json
mutable_data = safe_copy_data(data)
for json_field in ["title", "summary"]:
val = mutable_data.get(json_field)
if isinstance(val, str):
try:
mutable_data[json_field] = json.loads(val)
except ValueError:
pass
thumbnail = mutable_data.get("thumbnail") if hasattr(mutable_data, "get") else None
if thumbnail and hasattr(thumbnail, "read"):
compressed_bytes = maybe_compress_uploaded_file(thumbnail)
@ -600,6 +608,18 @@ class AdminHadisCategoryArchiveSerializer(serializers.ModelSerializer):
]
read_only_fields = ["id", "share_link", "xmind_share_link"]
def to_internal_value(self, data):
import json
data = safe_copy_data(data)
for json_field in ["title", "description"]:
val = data.get(json_field)
if isinstance(val, str):
try:
data[json_field] = json.loads(val)
except ValueError:
pass
return super().to_internal_value(data)
def create(self, validated_data):
validated_data.pop("remove_xmind_file", False)
return super().create(validated_data)
@ -715,6 +735,37 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer):
]
read_only_fields = ["id", "slug", "share_link", "created_at", "updated_at", "reliability_detail"]
def to_internal_value(self, data):
import json
data = safe_copy_data(data)
for json_field in [
"full_name",
"kunya",
"known_as",
"nickname",
"origin",
"lived_in",
"died_in",
"description",
]:
val = data.get(json_field)
if isinstance(val, str):
try:
data[json_field] = json.loads(val)
except ValueError:
pass
# Handle empty/null values for optional integer fields
for field in ["legacy_number", "age_at_death", "generation"]:
if field in data and (data[field] == "" or data[field] is None or data[field] == "null" or data[field] == "undefined"):
data[field] = None
return super().to_internal_value(data)
def create(self, validated_data):
validated_data.pop("remove_thumbnail", False)
return super().create(validated_data)
def update(self, instance, validated_data):
remove_thumbnail = validated_data.pop("remove_thumbnail", False)
if remove_thumbnail and instance.thumbnail:

33
apps/library/migrations/0008_alter_author_slug_alter_book_slug_and_more.py

@ -0,0 +1,33 @@
# Generated by Django 4.2.30 on 2026-07-20 17:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0007_remove_bookcollection_pin_top_and_more'),
]
operations = [
migrations.AlterField(
model_name='author',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, unique=True),
),
migrations.AlterField(
model_name='book',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, unique=True),
),
migrations.AlterField(
model_name='bookcollection',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, unique=True),
),
migrations.AlterField(
model_name='category',
name='slug',
field=models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, unique=True),
),
]

8
apps/library/models.py

@ -18,7 +18,7 @@ class BookCollection(LowercaseSlugMixin, models.Model):
# BOTTOM = 'bottom', _('Bottom Section')
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True, allow_unicode=True)
summary = models.CharField(max_length=512, null=True, blank=True, help_text=_('could be null'))
display_position = models.CharField(
max_length=20,
@ -65,7 +65,7 @@ class MiddleBookCollection(BookCollection):
class Category(LowercaseSlugMixin, models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True, allow_unicode=True)
status = models.BooleanField(default=True, verbose_name=_('status'))
# books = models.ManyToManyField('library.Book', related_name='related_categories_books',through="library.Book_categories" ,verbose_name=_('Books'), blank=True)
@ -88,7 +88,7 @@ class Category(LowercaseSlugMixin, models.Model):
class Author(LowercaseSlugMixin, models.Model):
slug_source_field = 'full_name'
full_name = models.CharField(max_length=255, verbose_name=_('Full Name'))
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True, allow_unicode=True)
thumbnail = models.ImageField(
upload_to='author_thumbnails/',
null=True,
@ -140,7 +140,7 @@ class Book(LowercaseSlugMixin, models.Model):
docx = 'docx', 'Docx'
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True, allow_unicode=True)
slogan = models.CharField(max_length=300, blank=True, null=True)
summary_title = models.CharField(max_length=512, null=True, blank=True, help_text=_('Summary Title'))

16
apps/library/serializers_admin.py

@ -192,6 +192,22 @@ class AdminBookDetailSerializer(serializers.ModelSerializer):
]
read_only_fields = ["id", "view_count", "download_count", "bookmark_count", "created_at", "updated_at"]
def to_internal_value(self, data):
import json
if hasattr(data, "copy"):
mutable_data = data.copy()
else:
mutable_data = dict(data)
for json_field in ["main_themes", "notable_works"]:
val = mutable_data.get(json_field)
if isinstance(val, str):
try:
mutable_data[json_field] = json.loads(val)
except ValueError:
pass
return super().to_internal_value(mutable_data)
def get_bookmark_count(self, obj):
from apps.bookmark.models.bookmark import Bookmark
return Bookmark.objects.filter(service=Bookmark.ServiceChoices.LIBRARY, content_id=obj.id, status=True).count()

18
apps/podcast/migrations/0004_alter_podcastcollection_slug.py

@ -0,0 +1,18 @@
# Generated by Django 4.2.30 on 2026-07-20 17:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('podcast', '0003_remove_podcastcollection_pin_top'),
]
operations = [
migrations.AlterField(
model_name='podcastcollection',
name='slug',
field=models.SlugField(allow_unicode=True, max_length=255, unique=True),
),
]

2
apps/podcast/models.py

@ -31,7 +31,7 @@ class PodcastCollection(LowercaseSlugMixin, models.Model):
MIDDLE = 'middle', _('Middle Section')
title = models.CharField(max_length=255, help_text="This title will not be displayed anywhere")
slug = models.SlugField(max_length=255, unique=True)
slug = models.SlugField(max_length=255, unique=True, allow_unicode=True)
summary = models.CharField(max_length=512, null=True, blank=True, help_text=_('could be null'))
thumbnail = models.ImageField(upload_to='podcast/collection/', null=True, blank=True, help_text=_('image allowed'))
order = models.IntegerField(default=0, verbose_name=_('order'))

1
apps/podcast/views.py

@ -1,3 +1,4 @@
from django.db.models import Q
from rest_framework import generics, status
from rest_framework.authentication import TokenAuthentication
from rest_framework.decorators import action

18
apps/video/migrations/0004_alter_videocollection_slug.py

@ -0,0 +1,18 @@
# Generated by Django 4.2.30 on 2026-07-20 17:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('video', '0003_remove_videocollection_pin_top'),
]
operations = [
migrations.AlterField(
model_name='videocollection',
name='slug',
field=models.SlugField(allow_unicode=True, max_length=255, unique=True),
),
]

2
apps/video/models.py

@ -33,7 +33,7 @@ class VideoCollection(LowercaseSlugMixin, models.Model):
MIDDLE = 'middle', _('Middle Section')
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
slug = models.SlugField(max_length=255, unique=True, allow_unicode=True)
summary = models.CharField(max_length=512, null=True, blank=True, help_text=_('could be null'))
thumbnail = models.ImageField(upload_to='video/collection/', null=True, blank=True, help_text=_('image allowed'))
order = models.IntegerField(default=0, verbose_name=_('order'))

1
apps/video/views.py

@ -1,3 +1,4 @@
from django.db.models import Q
from rest_framework import generics, status
from rest_framework.authentication import TokenAuthentication
from rest_framework.decorators import action

Loading…
Cancel
Save