Browse Source

feat(api): add optional pagination, category search, reference detail and relation management

master
Mohsen Taba 18 hours ago
parent
commit
7c2d1c106f
  1. 21
      apps/article/views.py
  2. 6
      apps/article/views_admin.py
  3. 28
      apps/hadis/serializers/serializers_admin.py
  4. 54
      apps/hadis/views_admin.py
  5. 16
      apps/library/serializers_admin.py
  6. 4
      apps/library/urls.py
  7. 35
      apps/library/views_admin.py
  8. 4
      apps/podcast/serializers_admin.py
  9. 18
      apps/podcast/views.py
  10. 25
      apps/podcast/views_admin.py
  11. 4
      apps/video/serializers_admin.py
  12. 18
      apps/video/views.py
  13. 25
      apps/video/views_admin.py
  14. 366
      scripts/generate_wise_sayings_hadiths.py
  15. 95
      scripts/resolve_category_hadis_conflicts.py
  16. 234
      scripts/update_readable_slugs.py
  17. 9
      utils/pagination.py

21
apps/article/views.py

@ -1,13 +1,14 @@
from django.db.models import Q
from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.decorators import action
from rest_framework.authentication import TokenAuthentication
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from apps.library.pagination import NoPagination
from rest_framework.permissions import IsAuthenticated
from utils.pagination import StandardResultsSetPagination
@ -284,3 +285,21 @@ class DovodiArticleCollectionViewSet(ModelViewSet):
if search_query:
queryset = queryset.filter(Q(title__icontains=search_query) | Q(slug__icontains=search_query))
return queryset
@action(detail=True, methods=["post"], url_path="assign-items")
def assign_items(self, request, pk=None):
collection = self.get_object()
add_ids = request.data.get("add_ids", []) or request.data.get("item_ids", [])
remove_ids = request.data.get("remove_ids", [])
if add_ids:
new_items = [
ArticleInCollection(article_id=aid, collection_id=collection.id)
for aid in add_ids
]
ArticleInCollection.objects.bulk_create(new_items, ignore_conflicts=True)
if remove_ids:
ArticleInCollection.objects.filter(collection_id=collection.id, article_id__in=remove_ids).delete()
return Response({"status": "success", "attached_count": collection.collection_articles.count()})

6
apps/article/views_admin.py

@ -40,7 +40,7 @@ class AdminArticleViewSet(ModelViewSet):
return AdminArticleDetailSerializer
def get_queryset(self):
queryset = Article.objects.all().prefetch_related("categories")
queryset = Article.objects.all().prefetch_related("categories", "collections")
search_query = self.request.query_params.get("search")
if search_query:
@ -55,6 +55,10 @@ class AdminArticleViewSet(ModelViewSet):
if category_id and category_id != "all":
queryset = queryset.filter(categories__id=category_id)
collection_id = self.request.query_params.get("collection") or self.request.query_params.get("collection_id")
if collection_id and collection_id != "all":
queryset = queryset.filter(collections__id=collection_id)
status_filter = self.request.query_params.get("status")
if status_filter in {"true", "false"}:
queryset = queryset.filter(status=status_filter == "true")

28
apps/hadis/serializers/serializers_admin.py

@ -96,8 +96,16 @@ class AdminHadisTagSerializer(serializers.ModelSerializer):
fields = ["id", "title", "status"]
class AdminHadisCategoryListSerializer(serializers.ModelSerializer):
title = serializers.JSONField()
class Meta:
model = HadisCategory
fields = ["id", "title", "slug", "source_type", "order"]
class AdminHadisListSerializer(serializers.ModelSerializer):
category_detail = AdminHadisCategorySerializer(source="category", read_only=True)
category_detail = AdminHadisCategoryListSerializer(source="category", read_only=True)
hadis_status_detail = AdminHadisStatusSerializer(source="hadis_status", read_only=True)
collections_detail = serializers.SerializerMethodField()
@ -123,8 +131,10 @@ class AdminHadisListSerializer(serializers.ModelSerializer):
read_only_fields = ["id", "slug", "created_at", "updated_at"]
def get_collections_detail(self, obj):
collections = [item.collection for item in obj.collection_items.all()]
return AdminHadisCollectionSerializer(collections, many=True).data
return [
{"id": item.collection_id, "title": item.collection.title, "slug": item.collection.slug}
for item in obj.collection_items.all()
]
class HadisCollectionsField(serializers.PrimaryKeyRelatedField):
@ -263,6 +273,14 @@ class AdminHadisDetailSerializer(serializers.ModelSerializer):
refs = list(instance.references.all())
ref = refs[0] if refs else None
ret["book_reference"] = ref.book_reference_id if ref else None
if ref and ref.book_reference:
ret["book_reference_detail"] = {
"id": ref.book_reference.id,
"title": ref.book_reference.title,
"slug": ref.book_reference.slug,
}
else:
ret["book_reference_detail"] = None
return ret
def get_bookmark_count(self, obj):
@ -966,6 +984,8 @@ class AdminBookReferenceDocumentSerializer(serializers.ModelSerializer):
class AdminBookReferenceListSerializer(serializers.ModelSerializer):
author_id = serializers.IntegerField(source="author.id", read_only=True, allow_null=True)
author_name = serializers.JSONField(source="author.name", read_only=True, allow_null=True)
volume = serializers.SerializerMethodField()
publisher = serializers.SerializerMethodField()
year_of_publication = serializers.SerializerMethodField()
@ -977,6 +997,8 @@ class AdminBookReferenceListSerializer(serializers.ModelSerializer):
"title",
"slug",
"has_editions",
"author_id",
"author_name",
"volume",
"publisher",
"year_of_publication",

54
apps/hadis/views_admin.py

@ -7,10 +7,10 @@ from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from apps.account.permissions import IsSuperAdmin
from utils.pagination import StandardResultsSetPagination
from utils.pagination import StandardResultsSetPagination, OptionalPagination
from .models import (
Hadis, HadisCategory, HadisCollection, HadisCorrection, HadisInterpretation, HadisStatus, HadisTag, HadisSect, HadisTransmitter,
Hadis, HadisCategory, HadisCollection, HadisInCollection, HadisCorrection, HadisInterpretation, HadisStatus, HadisTag, HadisSect, HadisTransmitter,
BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookEdition, BookVolume,
Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, TransmitterOpinion, TransmitterOriginalText
)
@ -51,7 +51,7 @@ class AdminHadisCategoryViewSet(ReadOnlyModelViewSet):
serializer_class = AdminHadisCategorySerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = None
pagination_class = OptionalPagination
def get_queryset(self):
hadis_subquery = Hadis.objects.filter(category=OuterRef("pk"))
@ -63,6 +63,11 @@ class AdminHadisCategoryViewSet(ReadOnlyModelViewSet):
source_type = self.request.query_params.get("source_type")
if source_type:
queryset = queryset.filter(source_type=source_type)
search = self.request.query_params.get("search")
if search:
queryset = queryset.filter(
Q(title__icontains=search) | Q(slug__icontains=search)
)
return queryset.order_by("order", "id")
@ -117,7 +122,7 @@ class AdminHadisStatusViewSet(ModelViewSet):
serializer_class = AdminHadisStatusSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = None
pagination_class = OptionalPagination
parser_classes = (JSONParser, MultiPartParser, FormParser)
def get_queryset(self):
@ -168,6 +173,24 @@ class AdminHadisCollectionViewSet(ModelViewSet):
queryset = queryset.filter(status=status_filter == "true")
return queryset.order_by("order", "id")
@action(detail=True, methods=["post"], url_path="assign-items")
def assign_items(self, request, pk=None):
collection = self.get_object()
add_ids = request.data.get("add_ids", []) or request.data.get("item_ids", [])
remove_ids = request.data.get("remove_ids", [])
if add_ids:
new_items = [
HadisInCollection(hadis_id=hid, collection_id=collection.id)
for hid in add_ids
]
HadisInCollection.objects.bulk_create(new_items, ignore_conflicts=True)
if remove_ids:
HadisInCollection.objects.filter(collection_id=collection.id, hadis_id__in=remove_ids).delete()
return Response({"status": "success", "attached_count": collection.hadis_items.count()})
class AdminTransmitterReliabilityViewSet(ReadOnlyModelViewSet):
serializer_class = AdminTransmitterReliabilitySerializer
@ -295,7 +318,7 @@ class AdminHadisViewSet(ModelViewSet):
def get_queryset(self):
queryset = (
Hadis.objects.all()
.select_related("category__sect", "hadis_status")
.select_related("category", "hadis_status")
.prefetch_related("collection_items__collection")
)
@ -316,6 +339,10 @@ class AdminHadisViewSet(ModelViewSet):
if category_id and category_id != "all":
queryset = queryset.filter(category_id=category_id)
collection_id = self.request.query_params.get("collection") or self.request.query_params.get("collection_id")
if collection_id and collection_id != "all":
queryset = queryset.filter(collection_items__collection_id=collection_id)
source_type = self.request.query_params.get("source_type")
if source_type:
queryset = queryset.filter(category__source_type=source_type)
@ -324,7 +351,7 @@ class AdminHadisViewSet(ModelViewSet):
if status_filter in {"true", "false"}:
queryset = queryset.filter(status=status_filter == "true")
return queryset.distinct().order_by("-id")
return queryset.order_by("-id")
class AdminHadisCorrectionViewSet(ModelViewSet):
@ -482,6 +509,7 @@ class AdminBookAuthorViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = None
parser_classes = (MultiPartParser, FormParser, JSONParser)
def get_queryset(self):
queryset = BookAuthor.objects.all()
@ -490,6 +518,20 @@ class AdminBookAuthorViewSet(ModelViewSet):
queryset = queryset.filter(name__icontains=search_query)
return queryset.order_by("id")
@action(detail=True, methods=["post"], url_path="assign-references")
def assign_references(self, request, pk=None):
author = self.get_object()
add_ids = request.data.get("add_ids", []) or request.data.get("book_ids", []) or request.data.get("item_ids", [])
remove_ids = request.data.get("remove_ids", [])
if add_ids:
BookReference.objects.filter(id__in=add_ids).update(author=author)
if remove_ids:
BookReference.objects.filter(id__in=remove_ids, author=author).update(author=None)
return Response({"status": "success", "references_count": author.book_references.count()})
class AdminBookAttributeViewSet(ModelViewSet):
serializer_class = AdminBookAttributeSerializer

16
apps/library/serializers_admin.py

@ -101,10 +101,23 @@ class AdminAuthorSerializer(serializers.ModelSerializer):
return super().update(instance, validated_data)
class AdminLibraryCategoryBriefSerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["id", "title", "slug", "status"]
class AdminLibraryCollectionBriefSerializer(serializers.ModelSerializer):
class Meta:
model = BookCollection
fields = ["id", "title", "slug", "status"]
class AdminBookListSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True)
book_file = AbsoluteFileField(required=False, allow_null=True)
categories_detail = AdminLibraryCategorySerializer(source="categories", many=True, read_only=True)
categories_detail = AdminLibraryCategoryBriefSerializer(source="categories", many=True, read_only=True)
collections_detail = AdminLibraryCollectionBriefSerializer(source="collections", many=True, read_only=True)
author = serializers.CharField(source='author.full_name', read_only=True)
author_id = serializers.IntegerField(read_only=True)
@ -126,6 +139,7 @@ class AdminBookListSerializer(serializers.ModelSerializer):
"created_at",
"updated_at",
"categories_detail",
"collections_detail",
"author_id",
"book_file",
]

4
apps/library/urls.py

@ -30,12 +30,16 @@ admin_router.register(r'dovodi-categories', DovodiAdminLibraryCategoryViewSet, b
admin_router.register(r'collections', AdminLibraryCollectionViewSet, basename='admin-library-collections')
admin_router.register(r'dovodi-collections', DovodiAdminLibraryCollectionViewSet, basename='admin-library-dovodi-collections')
dovodi_router = SimpleRouter()
dovodi_router.register(r'collections', DovodiAdminLibraryCollectionViewSet, basename='dovodi-library-collections')
# Hide admin viewsets from swagger
for prefix, viewset, basename in admin_router.registry:
viewset.swagger_schema = None
urlpatterns = [
path('admin/', include(admin_router.urls)),
path('dovodi/', include(dovodi_router.urls)),
path('categories/', CategoryListView.as_view(), name='category-list'),
path('pinned-collections/', PinnedBookCollectionListView.as_view(), name='pinned-collection-list'),
path('collections/', MiddleBookCollectionListView.as_view(), name='collection-list'),

35
apps/library/views_admin.py

@ -1,7 +1,9 @@
from django.db.models import Count, Q
from rest_framework.authentication import TokenAuthentication
from rest_framework.decorators import action
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from apps.account.permissions import IsSuperAdmin
@ -50,15 +52,30 @@ class DovodiAdminLibraryCategoryViewSet(ModelViewSet):
return queryset.order_by("title", "id")
class AdminLibraryCollectionViewSet(ReadOnlyModelViewSet):
class AdminLibraryCollectionViewSet(ModelViewSet):
serializer_class = AdminLibraryCollectionSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = None
parser_classes = (MultiPartParser, FormParser, JSONParser)
def get_queryset(self):
return BookCollection.objects.all().order_by("display_position", "order", "title")
@action(detail=True, methods=["post"], url_path="assign-items")
def assign_items(self, request, pk=None):
collection = self.get_object()
add_ids = request.data.get("add_ids", []) or request.data.get("item_ids", [])
remove_ids = request.data.get("remove_ids", [])
if add_ids:
collection.related_collections.add(*add_ids)
if remove_ids:
collection.related_collections.remove(*remove_ids)
return Response({"status": "success", "attached_count": collection.related_collections.count()})
class DovodiAdminLibraryCollectionViewSet(ModelViewSet):
serializer_class = AdminLibraryCollectionSerializer
@ -84,6 +101,20 @@ class DovodiAdminLibraryCollectionViewSet(ModelViewSet):
return queryset.order_by("display_position", "order", "title", "id")
@action(detail=True, methods=["post"], url_path="assign-items")
def assign_items(self, request, pk=None):
collection = self.get_object()
add_ids = request.data.get("add_ids", []) or request.data.get("item_ids", [])
remove_ids = request.data.get("remove_ids", [])
if add_ids:
collection.related_collections.add(*add_ids)
if remove_ids:
collection.related_collections.remove(*remove_ids)
return Response({"status": "success", "attached_count": collection.related_collections.count()})
class AdminAuthorViewSet(ModelViewSet):
serializer_class = AdminAuthorSerializer
@ -142,7 +173,7 @@ class AdminBookViewSet(ModelViewSet):
if category_id and category_id != "all":
queryset = queryset.filter(categories__id=category_id)
collection_id = self.request.query_params.get("collection")
collection_id = self.request.query_params.get("collection") or self.request.query_params.get("collection_id")
if collection_id and collection_id != "all":
queryset = queryset.filter(collections__id=collection_id)

4
apps/podcast/serializers_admin.py

@ -208,7 +208,9 @@ class AdminPodcastPlaylistListSerializer(serializers.ModelSerializer):
read_only_fields = ["id", "view_count", "total_time", "created_at", "updated_at", "items_count"]
def get_items_count(self, obj):
return obj.playlist_items.count()
if hasattr(obj, "annotated_items_count"):
return obj.annotated_items_count
return len(obj.playlist_items.all())
class AdminPodcastPlaylistDetailSerializer(serializers.ModelSerializer):

18
apps/podcast/views.py

@ -399,6 +399,24 @@ class DovodiPodcastCollectionViewSet(ModelViewSet):
queryset = queryset.filter(Q(title__icontains=search_query) | Q(slug__icontains=search_query))
return queryset
@action(detail=True, methods=["post"], url_path="assign-items")
def assign_items(self, request, pk=None):
collection = self.get_object()
add_ids = request.data.get("add_ids", []) or request.data.get("item_ids", [])
remove_ids = request.data.get("remove_ids", [])
if add_ids:
new_items = [
PodcastPlaylistInCollection(playlist_id=pid, collection_id=collection.id)
for pid in add_ids
]
PodcastPlaylistInCollection.objects.bulk_create(new_items, ignore_conflicts=True)
if remove_ids:
PodcastPlaylistInCollection.objects.filter(collection_id=collection.id, playlist_id__in=remove_ids).delete()
return Response({"status": "success", "attached_count": collection.collection_playlists.count()})
class DovodiPodcastItemViewSet(ModelViewSet):
serializer_class = DovodiPodcastItemSerializer

25
apps/podcast/views_admin.py

@ -11,7 +11,7 @@ from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from apps.account.permissions import IsSuperAdmin
from utils.pagination import StandardResultsSetPagination
from .models import PlaylistItem, Podcast, PodcastCollection, PodcastCategory, PodcastPlaylist
from .models import PlaylistItem, Podcast, PodcastCollection, PodcastCategory, PodcastPlaylist, PodcastPlaylistInCollection
from .serializers_admin import (
AdminPodcastCategorySerializer,
AdminPodcastCollectionSerializer,
@ -33,15 +33,34 @@ class AdminPodcastCategoryViewSet(ReadOnlyModelViewSet):
return PodcastCategory.objects.all().order_by("title")
class AdminPodcastCollectionViewSet(ReadOnlyModelViewSet):
class AdminPodcastCollectionViewSet(ModelViewSet):
serializer_class = AdminPodcastCollectionSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = None
parser_classes = (MultiPartParser, FormParser, JSONParser)
def get_queryset(self):
return PodcastCollection.objects.all().order_by("display_position", "order", "title")
@action(detail=True, methods=["post"], url_path="assign-items")
def assign_items(self, request, pk=None):
collection = self.get_object()
add_ids = request.data.get("add_ids", []) or request.data.get("item_ids", [])
remove_ids = request.data.get("remove_ids", [])
if add_ids:
new_items = [
PodcastPlaylistInCollection(playlist_id=pid, collection_id=collection.id)
for pid in add_ids
]
PodcastPlaylistInCollection.objects.bulk_create(new_items, ignore_conflicts=True)
if remove_ids:
PodcastPlaylistInCollection.objects.filter(collection_id=collection.id, playlist_id__in=remove_ids).delete()
return Response({"status": "success", "attached_count": collection.collection_playlists.count()})
class AdminPodcastViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, IsSuperAdmin]
@ -115,7 +134,7 @@ class AdminPodcastPlaylistViewSet(ModelViewSet):
if category_id and category_id != "all":
queryset = queryset.filter(categories__id=category_id)
collection_id = self.request.query_params.get("collection")
collection_id = self.request.query_params.get("collection") or self.request.query_params.get("collection_id")
if collection_id and collection_id != "all":
queryset = queryset.filter(collections__id=collection_id)

4
apps/video/serializers_admin.py

@ -209,7 +209,9 @@ class AdminVideoPlaylistListSerializer(serializers.ModelSerializer):
read_only_fields = ["id", "view_count", "total_time", "created_at", "updated_at", "items_count"]
def get_items_count(self, obj):
return obj.playlist_items.count()
if hasattr(obj, "annotated_items_count"):
return obj.annotated_items_count
return len(obj.playlist_items.all())
class AdminVideoPlaylistDetailSerializer(serializers.ModelSerializer):

18
apps/video/views.py

@ -482,6 +482,24 @@ class DovodiVideoCollectionViewSet(ModelViewSet):
queryset = queryset.filter(Q(title__icontains=search_query) | Q(slug__icontains=search_query))
return queryset
@action(detail=True, methods=["post"], url_path="assign-items")
def assign_items(self, request, pk=None):
collection = self.get_object()
add_ids = request.data.get("add_ids", []) or request.data.get("item_ids", [])
remove_ids = request.data.get("remove_ids", [])
if add_ids:
new_items = [
VideoPlaylistInCollection(playlist_id=pid, collection_id=collection.id)
for pid in add_ids
]
VideoPlaylistInCollection.objects.bulk_create(new_items, ignore_conflicts=True)
if remove_ids:
VideoPlaylistInCollection.objects.filter(collection_id=collection.id, playlist_id__in=remove_ids).delete()
return Response({"status": "success", "attached_count": collection.collection_playlists.count()})
class DovodiVideoItemViewSet(ModelViewSet):
serializer_class = DovodiVideoItemSerializer

25
apps/video/views_admin.py

@ -11,7 +11,7 @@ from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from apps.account.permissions import IsSuperAdmin
from utils.pagination import StandardResultsSetPagination
from .models import PlaylistItem, Video, VideoCategory, VideoCollection, VideoPlaylist
from .models import PlaylistItem, Video, VideoCategory, VideoCollection, VideoPlaylist, VideoPlaylistInCollection
from .serializers_admin import (
AdminVideoCategorySerializer,
AdminVideoCollectionSerializer,
@ -33,15 +33,34 @@ class AdminVideoCategoryViewSet(ReadOnlyModelViewSet):
return VideoCategory.objects.all().order_by("title")
class AdminVideoCollectionViewSet(ReadOnlyModelViewSet):
class AdminVideoCollectionViewSet(ModelViewSet):
serializer_class = AdminVideoCollectionSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = None
parser_classes = (MultiPartParser, FormParser, JSONParser)
def get_queryset(self):
return VideoCollection.objects.all().order_by("display_position", "order", "title")
@action(detail=True, methods=["post"], url_path="assign-items")
def assign_items(self, request, pk=None):
collection = self.get_object()
add_ids = request.data.get("add_ids", []) or request.data.get("item_ids", [])
remove_ids = request.data.get("remove_ids", [])
if add_ids:
new_items = [
VideoPlaylistInCollection(playlist_id=pid, collection_id=collection.id)
for pid in add_ids
]
VideoPlaylistInCollection.objects.bulk_create(new_items, ignore_conflicts=True)
if remove_ids:
VideoPlaylistInCollection.objects.filter(collection_id=collection.id, playlist_id__in=remove_ids).delete()
return Response({"status": "success", "attached_count": collection.collection_playlists.count()})
class AdminVideoViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, IsSuperAdmin]
@ -112,7 +131,7 @@ class AdminVideoPlaylistViewSet(ModelViewSet):
if category_id and category_id != "all":
queryset = queryset.filter(categories__id=category_id)
collection_id = self.request.query_params.get("collection")
collection_id = self.request.query_params.get("collection") or self.request.query_params.get("collection_id")
if collection_id and collection_id != "all":
queryset = queryset.filter(collections__id=collection_id)

366
scripts/generate_wise_sayings_hadiths.py

@ -0,0 +1,366 @@
#!/usr/bin/env python3
"""
Script to generate 50 realistic hadiths for the category 'wise-sayings'.
Features:
- Connects to the 'wise-sayings' category (HadisCategory)
- Generates rich multilingual content (RU, EN, FA, AR)
- Attaches transmitters and book references
- Assigns readable unique slugs
- Supports --dry-run flag
"""
import os
import sys
import random
import time
import argparse
from pathlib import Path
from django.utils.text import slugify
# Force UTF-8 stdout for Windows consoles
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(BASE_DIR))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.base')
import django
django.setup()
from django.db import transaction
from apps.hadis.models import (
HadisCategory, Hadis, HadisStatus, HadisTag,
Transmitters, HadisTransmitter, HadisReference
)
from apps.hadis.models.reference import BookReference
# 50 Wisdom-themed Hadith templates
WISDOM_TEMPLATES = [
{
"narrator_ru": "Повелитель верующих Али (мир ему)",
"narrator_en": "Amir al-Mu'minin Ali (peace be upon him)",
"narrator_fa": "امیرالمؤمنین علی (ع)",
"title_ru": "Ценность человека и его знания",
"title_en": "The Worth of a Person and Their Knowledge",
"title_fa": "ارزش هر انسان و دانش او",
"text": "قِيمَةُ كُلِّ امْرِئٍ مَا يُحْسِنُهُ.",
"trans_ru": "Ценность каждого человека заключается в том, что он делает наилучшим образом.",
"trans_en": "The value of every person lies in that which they excel at.",
"trans_fa": "ارزش هر انسانی به اندازه مهارتی است که آن را نیکو انجام می‌دهد.",
"exp_ru": "Этот афоризм подчеркивает значимость практических навыков, знаний и добродетелей.",
"exp_en": "This profound saying emphasizes that true human dignity comes from virtue and knowledge."
},
{
"narrator_ru": "Посланник Аллаха (да благословит Аллах его и род его)",
"narrator_en": "The Messenger of Allah (peace be upon him and his family)",
"narrator_fa": "پیامبر اکرم (ص)",
"title_ru": "Мудрость как потерянное достояние верующего",
"title_en": "Wisdom as the Lost Treasure of the Believer",
"title_fa": "حکمت؛ گمشده مومن",
"text": "الْحِكْمَةُ ضَالَّةُ الْمُؤْمِنِ، فَخُذِ الْحِكْمَةَ وَلَوْ مِنْ أَهْلِ النِّفَاقِ.",
"trans_ru": "Мудрость — потерянное достояние верующего; берите же мудрость, даже если она исходит от лицемера.",
"trans_en": "Wisdom is the lost property of the believer; seek wisdom wherever it may be found.",
"trans_fa": "حکمت گمشده مؤمن است، پس حکمت را فرا گیرید حتی اگر از اهل نفاق باشد.",
"exp_ru": "Истина ценна сама по себе вне зависимости от того, кто ее произносит.",
"exp_en": "Truth and wisdom possess intrinsic value regardless of their source."
},
{
"narrator_ru": "Имам Джафар ас-Садик (мир ему)",
"narrator_en": "Imam Ja'far al-Sadiq (peace be upon him)",
"narrator_fa": "امام جعفر صادق (ع)",
"title_ru": "Разум и опора человека",
"title_en": "Intellect as the Pillar of Humanity",
"title_fa": "عقل؛ ستون انسانیت",
"text": "دِعَامَةُ الإِنْسَانِ الْعَقْلُ، وَالْعَقْلُ مِنْهُ الْفِطْنَةُ وَالْفَهْمُ وَالْحِفْظُ وَالْعِلْمُ.",
"trans_ru": "Опора человека — это разум, а от разума происходят сообразительность, понимание, память и знание.",
"trans_en": "The pillar of a human being is the intellect, from which stem perception, understanding, memory, and knowledge.",
"trans_fa": "ستون و قوام انسان، عقل است؛ و از عقل زیرکی، فهم، حفظ و علم سرچشمه می‌گیرد.",
"exp_ru": "Разум является фундаментом для нравственного и интеллектуального развития личности.",
"exp_en": "Intellect serves as the essential bedrock for spiritual and practical enlightenment."
},
{
"narrator_ru": "Повелитель верующих Али (мир ему)",
"narrator_en": "Amir al-Mu'minin Ali (peace be upon him)",
"narrator_fa": "امیرالمؤمنین علی (ع)",
"title_ru": "Молчание и спасение языка",
"title_en": "Silence and the Guarding of Speech",
"title_fa": "سکوت و پاسداری از زبان",
"text": "إِذَا تَمَّ الْعَقْلُ نَقَصَ الْكَلَامُ.",
"trans_ru": "Когда разум достигает совершенства, речь становится немногословной.",
"trans_en": "When the intellect reaches perfection, speech becomes concise.",
"trans_fa": "هنگامی که خرد به کمال رسد، سخن گفتن کاهش می‌یابد.",
"exp_ru": "Зрелый разум побуждает человека взвешивать каждое слово перед произнесением.",
"exp_en": "A mature mind naturally values purposeful reflection over excessive talking."
},
{
"narrator_ru": "Посланник Аллаха (да благословит Аллах его и род его)",
"narrator_en": "The Messenger of Allah (peace be upon him and his family)",
"narrator_fa": "پیامبر اکرم (ص)",
"title_ru": "Доброе слово как милостыня",
"title_en": "A Good Word is Charity",
"title_fa": "کلام نیکو صدقه است",
"text": "الْكَلِمَةُ الطَّيِّبَةُ صَدَقَةٌ.",
"trans_ru": "Доброе, благое слово — это милостыня (садака).",
"trans_en": "A kind and pleasant word is an act of charity.",
"trans_fa": "سخن پاک و نیکو صدقه است.",
"exp_ru": "Даже доброе отношение и утешительное слово приравниваются к благодеяниям.",
"exp_en": "Kind words uplift hearts and constitute noble spiritual contributions."
},
{
"narrator_ru": "Имам Мухаммад аль-Бакир (мир ему)",
"narrator_en": "Imam Muhammad al-Baqir (peace be upon him)",
"narrator_fa": "امام محمد باقر (ع)",
"title_ru": "Искренность в делах и намерениях",
"title_en": "Sincerity in Actions and Intentions",
"title_fa": "اخلاص در عمل و نیت",
"text": "الإِبْقَاءُ عَلَى الْعَمَلِ حَتَّى يَخْلُصَ أَشَدُّ مِنَ الْعَمَلِ.",
"trans_ru": "Сохранить дело в чистоте и искренности труднее, чем совершить само дело.",
"trans_en": "Preserving the purity of an action is more demanding than the act itself.",
"trans_fa": "پایداری بر اخلاص در عمل سخت‌تر از خود عمل است.",
"exp_ru": "Искренность требует непрерывной внутренней работы и защиты от тщеславия.",
"exp_en": "Sustained purity of intention safeguards noble actions from vanity."
},
{
"narrator_ru": "Повелитель верующих Али (мир ему)",
"narrator_en": "Amir al-Mu'minin Ali (peace be upon him)",
"narrator_fa": "امیرالمؤمنین علی (ع)",
"title_ru": "Терпение как вершина веры",
"title_en": "Patience as the Summit of Faith",
"title_fa": "صبر؛ راس ایمان",
"text": "الصَّبْرُ مِنَ الإِيمَانِ كَالرَّأْسِ مِنَ الْجَسَدِ.",
"trans_ru": "Терпение в вере подобно голове на теле: нет веры у того, у кого нет терпения.",
"trans_en": "Patience is to faith what the head is to the body.",
"trans_fa": "جایگاه صبر در ایمان همانند جایگاه سر نسبت به بدن است.",
"exp_ru": "Без терпения невозможно удержать духовные и нравственные ориентиры в жизни.",
"exp_en": "Patience stabilizes human resolve in the face of life's unpredictable trials."
},
{
"narrator_ru": "Имам Али ар-Рида (мир ему)",
"narrator_en": "Imam Ali al-Rida (peace be upon him)",
"narrator_fa": "امام علی بن موسی الرضا (ع)",
"title_ru": "Дружба с разумом",
"title_en": "Friendship with Intellect",
"title_fa": "دوستی با خرد",
"text": "صَدِيقُ كُلِّ امْرِئٍ عَقْلُهُ، وَعَدُوُّهُ جَهْلُهُ.",
"trans_ru": "Истинный друг каждого человека — его разум, а его заклятый враг — его невежество.",
"trans_en": "Every person's true friend is their intellect, and their greatest enemy is ignorance.",
"trans_fa": "دوست هر فردی خرد اوست و دشمن او نادانی‌اش.",
"exp_ru": "Следование здравому смыслу оберегает человека от пагубных ошибок.",
"exp_en": "Intellectual clarity shields individuals from self-inflicted pitfalls."
},
{
"narrator_ru": "Посланник Аллаха (да благословит Аллах его и род его)",
"narrator_en": "The Messenger of Allah (peace be upon him and his family)",
"narrator_fa": "پیامبر اکرم (ص)",
"title_ru": "Лучшие из людей",
"title_en": "The Best of Mankind",
"title_fa": "بهترین مردم",
"text": "خَيْرُ النَّاسِ أَنْفَعُهُمْ لِلنَّاسِ.",
"trans_ru": "Лучший из людей — тот, кто приносит наибольшую пользу людям.",
"trans_en": "The best among people is the one who brings the most benefit to others.",
"trans_fa": "بهترین مردم سودمندترین آنان برای دیگران است.",
"exp_ru": "Служение обществу и помощь ближним — высшая мера добродетели.",
"exp_en": "Selfless service to humanity is the cornerstone of spiritual distinction."
},
{
"narrator_ru": "Повелитель верующих Али (мир ему)",
"narrator_en": "Amir al-Mu'minin Ali (peace be upon him)",
"narrator_fa": "امیرالمؤمنین علی (ع)",
"title_ru": "Очищение сердца от зависти",
"title_en": "Purifying the Heart from Envy",
"title_fa": "پاکی دل از رشک و حسد",
"text": "الْحَسَدُ يَأْكُلُ الإِيمَانَ كَمَا تَأْكُلُ النَّارُ الْحَطَبَ.",
"trans_ru": "Зависть пожирает веру так же стремительно, как огонь пожирает сухие дрова.",
"trans_en": "Envy consumes faith just as fire consumes dry wood.",
"trans_fa": "حسادت ایمان را فرو می‌خورد، همان‌گونه که آتش هیزم را می‌سوزاند.",
"exp_ru": "Зависть разрушает душевный покой и лишает человека благодати искренних дел.",
"exp_en": "Jealousy erodes inner tranquility and poisons human relationships."
}
]
# Additional 40 diverse wisdom sayings
ADDITIONAL_SAYINGS = [
("Благодарность приумножает блага", "Gratitude Multiplies Blessings", "شکر نعمت، نعمتت افزون کند", "لَئِن شَكَرْتُمْ لأَزِيدَنَّكُمْ.", "Имам Али (а)", "Благодарность открывает двери изобилия."),
("Умеренность в расходах", "Moderation in Expenditure", "میانه‌روی در معیشت", "حُسْنُ التَّدْبِيرِ مَعَ الْكَفَافِ خَيْرٌ مِنَ الْكَثِيرِ مَعَ الإِسْرَافِ.", "Имам Садик (а)", "Экономия и рассудительность сохраняют достаток."),
("Скромность возвышает", "Humility Elevates the Soul", "فروتنی و علو مرتبت", "مَنْ تَوَاضَعَ لِلَّهِ رَفَعَهُ اللَّهُ.", "Пророк Мухаммад (с)", "Истинное величие заключается в смирении."),
("Справедливость — основа правления", "Justice is the Pillar of Governance", "عدالت ستون حاکمیت", "الْعَدْلُ أَسَاسُ الْمُلْكِ.", "Имам Али (а)", "Праведное управление держится на непреклонной справедливости."),
("Поиск знаний от колыбели до могилы", "Seeking Knowledge from Cradle to Grave", "دانشجویی از مهد تا لحد", "اطْلُبُوا الْعِلْمَ مِنَ الْمَهْدِ إِلَى اللَّحْدِ.", "Пророк Мухаммад (с)", "Обучение — это пожизненный долг верующего."),
("Правдивость ведет к спасению", "Truthfulness Leads to Salvation", "راستی مایه رستگاری", "الصِّدْقُ يُنْجِي وَإِنْ خِفْتَهُ.", "Имам Али (а)", "Правда дарует уверенность и очищает совесть."),
("Мягкость в общении", "Gentleness in Discourse", "نرمی و مدارا در گفتگو", "مَا كَانَ الرِّفْقُ فِي شَيْءٍ إِلا زَانَهُ.", "Пророк Мухаммад (с)", "Доброта и мягкость украшают любое дело."),
("Бережливость времени", "The Value of Golden Hours", "غنیمت شمردن فرصت‌ها", "الْفُرْصَةُ تَمُرُّ مَرَّ السَّحَابِ.", "Имам Али (а)", "Время скоротечно, используйте возможности во благо."),
("Чистота помыслов", "Purity of Conscience", "پاکی درون و اندیشه", "إِنَّمَا الأَعْمَالُ بِالنِّيَّاتِ.", "Пророк Мухаммад (с)", "Ценность поступка измеряется чистотой намерения."),
("Уважение к родителям", "Devotion to Parents", "نیکی به پدر و مادر", "بِرُّ الْوَالِدَيْنِ أَفْضَلُ الْقُرُبَاتِ.", "Имам Бакир (а)", "Почитание родителей приносит благодать в оба мира.")
]
def generate_hadiths(count=50, dry_run=False):
print("\n🌟 [Wise Sayings] Starting Hadith Generation...")
print("=" * 70)
# 1. Get the category
category = HadisCategory.objects.filter(slug="wise-sayings").first()
if not category:
category = HadisCategory.objects.filter(title__icontains="Wise Sayings").first()
if not category:
print("❌ Error: Category 'wise-sayings' not found in database!")
return
print(f"📁 Target Category: ID {category.id} | Slug: '{category.slug}' | Sect: {category.sect_id}")
# 2. Get dependencies
statuses = list(HadisStatus.objects.all())
transmitters_pool = list(Transmitters.objects.all())
book_refs = list(BookReference.objects.all())
# Fallback status
default_status = statuses[0] if statuses else None
# Current max number for hadiths
max_num = Hadis.objects.all().order_by("-number").values_list("number", flat=True).first() or 0
print(f"📊 Current Highest Hadis Number: {max_num}")
print(f"📚 Available Transmitters: {len(transmitters_pool)} | Book References: {len(book_refs)}")
# 3. Build 50 hadiths
created_count = 0
hadis_instances = []
transmitter_relations = []
reference_relations = []
all_templates = list(WISDOM_TEMPLATES)
# Expand templates if count > 10
idx = 0
while len(all_templates) < count:
base_t = ADDITIONAL_SAYINGS[idx % len(ADDITIONAL_SAYINGS)]
idx += 1
num_variant = idx + 10
all_templates.append({
"narrator_ru": base_t[4],
"narrator_en": "Infallible Imam (peace be upon him)",
"narrator_fa": "معصوم (ع)",
"title_ru": f"{base_t[0]} (Часть {num_variant})",
"title_en": f"{base_t[1]} (Part {num_variant})",
"title_fa": f"{base_t[2]} (بخش {num_variant})",
"text": base_t[3],
"trans_ru": f"{base_t[0]}: {base_t[5]}",
"trans_en": f"{base_t[1]}: Wisdom regarding character and faith.",
"trans_fa": f"{base_t[2]}: رهنمودی گرانبها در اخلاق و دینداری.",
"exp_ru": base_t[5],
"exp_en": f"Spiritual contemplation on {base_t[1]}."
})
for i in range(count):
tmpl = all_templates[i]
curr_number = max_num + i + 1
title_json = [
{"language_code": "ru", "title": tmpl["title_ru"], "text": tmpl["title_ru"]},
{"language_code": "en", "title": tmpl["title_en"], "text": tmpl["title_en"]},
{"language_code": "fa", "title": tmpl["title_fa"], "text": tmpl["title_fa"]},
]
narrator_json = [
{"language_code": "ru", "title": tmpl["narrator_ru"], "text": tmpl["narrator_ru"]},
{"language_code": "en", "title": tmpl["narrator_en"], "text": tmpl["narrator_en"]},
{"language_code": "fa", "title": tmpl["narrator_fa"], "text": tmpl["narrator_fa"]},
]
translation_json = [
{"language_code": "ru", "title": tmpl["trans_ru"], "text": tmpl["trans_ru"]},
{"language_code": "en", "title": tmpl["trans_en"], "text": tmpl["trans_en"]},
{"language_code": "fa", "title": tmpl["trans_fa"], "text": tmpl["trans_fa"]},
]
explanation_json = [
{"language_code": "ru", "text": tmpl["exp_ru"]},
{"language_code": "en", "text": tmpl["exp_en"]},
]
# Generate unique readable slug
base_slug_text = tmpl["title_en"] or tmpl["title_ru"]
clean_slug = slugify(base_slug_text, allow_unicode=True).strip('-').lower()
if len(clean_slug) > 60:
clean_slug = clean_slug[:60].rstrip('-')
slug_candidate = f"{clean_slug}-{curr_number}"
hadis = Hadis(
category=category,
number=curr_number,
slug=slug_candidate,
title=title_json,
title_narrator=narrator_json,
text=tmpl["text"],
translation=translation_json,
explanation=explanation_json,
hadis_status=random.choice(statuses) if statuses else default_status,
links={"dovodi": "https://dovodi.newhorizonco.uk/"},
status=True,
share_link=f"https://dovodi.newhorizonco.uk/arguments/hadith/{slug_candidate}"
)
hadis_instances.append((hadis, tmpl))
print(f"✨ Prepared {len(hadis_instances)} Hadith records.")
if dry_run:
print("\n[DRY RUN PREVIEW] First 5 Hadiths:")
for h, tmpl in hadis_instances[:5]:
print(f" • Hadis #{h.number:4d} | Slug: '{h.slug}' | Title: '{tmpl['title_en']}'")
print("\n✨ Dry run completed. No changes saved to database.")
return
# Execute DB save
with transaction.atomic():
print("💾 Saving Hadith records to database...")
# Save hadiths
saved_hadiths = []
for h, tmpl in hadis_instances:
h.save()
saved_hadiths.append(h)
# Attach transmitters and references
print("🔗 Linking transmitters and book references...")
for h in saved_hadiths:
# 2 to 4 transmitters
if transmitters_pool:
num_tr = min(len(transmitters_pool), random.randint(2, 4))
sampled_tr = random.sample(transmitters_pool, num_tr)
gap_pos = random.randint(0, num_tr - 1)
for pos, tr in enumerate(sampled_tr):
HadisTransmitter.objects.create(
hadis=h,
transmitter=tr,
chain_index=0,
order=pos,
is_gap=(pos == gap_pos)
)
# 1 Book reference
if book_refs:
ref = random.choice(book_refs)
HadisReference.objects.create(
hadis=h,
book_reference=ref,
volume=str(random.randint(1, 8)),
pages=str(random.randint(15, 450)),
hadith_number=str(h.number)
)
print("=" * 70)
print(f"✅ Successfully created {len(saved_hadiths)} Hadiths for category 'wise-sayings'!")
print(f"📌 Numbers range: #{saved_hadiths[0].number} to #{saved_hadiths[-1].number}")
def main():
parser = argparse.ArgumentParser(description="Generate 50 Hadiths for the category 'wise-sayings'.")
parser.add_argument("--count", type=int, default=50, help="Number of hadiths to generate (default: 50).")
parser.add_argument("--dry-run", action="store_true", help="Preview without saving to DB.")
args = parser.parse_args()
start_time = time.time()
generate_hadiths(count=args.count, dry_run=args.dry_run)
duration = time.time() - start_time
print(f"⏱ Completed in {duration:.2f} seconds.")
if __name__ == "__main__":
main()

95
scripts/resolve_category_hadis_conflicts.py

@ -0,0 +1,95 @@
import os
import sys
import argparse
import django
sys.stdout.reconfigure(encoding='utf-8')
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.develop')
django.setup()
from django.db import transaction
from django.db.models import Count
from apps.hadis.models import HadisCategory, Hadis
def get_text(val):
if isinstance(val, list):
for item in val:
if isinstance(item, dict):
return item.get('text') or item.get('title') or str(item)
elif isinstance(val, dict):
return val.get('text') or val.get('title') or str(val)
return str(val or "")
def resolve_category_conflicts(dry_run=True):
print("=" * 65)
print(" CATEGORY CONFLICT RESOLVER (HADITHS vs SUBCATEGORIES) ")
print(f" Mode: {'DRY RUN (Simulation only, no DB changes)' if dry_run else 'EXECUTE (Applying changes to DB)'}")
print("=" * 65)
conflicted_categories = HadisCategory.objects.annotate(
hadis_count=Count('hadis', distinct=True),
children_count=Count('children', distinct=True)
).filter(hadis_count__gt=0, children_count__gt=0)
total_conflicts = conflicted_categories.count()
print(f"\nFound {total_conflicts} categories violating the rule (having both Hadiths & Subcategories).\n")
if total_conflicts == 0:
print("✅ No conflicts found. All categories adhere to the business rule.")
return
deleted_subcats_count = 0
reassigned_hadis_count = 0
with transaction.atomic():
for idx, parent_cat in enumerate(conflicted_categories, 1):
parent_title = get_text(parent_cat.title)
print(f"[{idx}/{total_conflicts}] Category #{parent_cat.id} ('{parent_title}') | Slug: {parent_cat.slug}")
print(f" - Direct Hadiths: {parent_cat.hadis_count}")
print(f" - Subcategories count: {parent_cat.children_count}")
# Get all descendants
descendants = list(parent_cat.get_descendants().order_by('-level'))
print(f" - Total subcategories/descendants to remove: {len(descendants)}")
for subcat in descendants:
subcat_title = get_text(subcat.title)
sub_hadis = subcat.hadis_set.all()
sub_hadis_count = sub_hadis.count()
if sub_hadis_count > 0:
print(f" ⚠️ Subcategory #{subcat.id} ('{subcat_title}') has {sub_hadis_count} Hadiths!")
print(f" -> Reassigning these {sub_hadis_count} Hadiths to parent Category #{parent_cat.id} before deletion.")
if not dry_run:
sub_hadis.update(category=parent_cat)
reassigned_hadis_count += sub_hadis_count
print(f" 🗑️ Removing Subcategory #{subcat.id} ('{subcat_title}')")
if not dry_run:
subcat.delete()
deleted_subcats_count += 1
if dry_run:
print("\n[DRY RUN SUMMARY]")
print(f" - Categories inspected: {total_conflicts}")
print(f" - Subcategories that would be deleted: {deleted_subcats_count}")
print(f" - Hadiths that would be safeguarded/reassigned: {reassigned_hadis_count}")
print(" -> No changes were written to the database.")
else:
print("\nRebuilding MPTT category tree structure...")
HadisCategory.objects.rebuild()
print("✅ MPTT category tree successfully rebuilt.")
print("\n[EXECUTION SUMMARY]")
print(f" - Conflicted categories resolved: {total_conflicts}")
print(f" - Subcategories deleted: {deleted_subcats_count}")
print(f" - Hadiths safeguarded: {reassigned_hadis_count}")
print("✅ All changes committed successfully.")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Resolve categories having both Hadiths and Subcategories")
parser.add_argument('--execute', action='store_true', help='Execute changes on database (default is dry-run)')
args = parser.parse_args()
resolve_category_conflicts(dry_run=not args.execute)

234
scripts/update_readable_slugs.py

@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""
High-Performance Script to Regenerate and Update Slugs for:
- HadisCategory
- Hadis
- Transmitters
Features:
- Human-readable slugs generated from actual multilingual titles and names
- Fast in-memory deduplication and uniqueness guarantees
- Safe two-phase atomic update using raw SQL temporary assignment + bulk update
- Supports --dry-run flag for inspection before applying
"""
import os
import sys
import re
import time
import argparse
from pathlib import Path
from django.utils.text import slugify
# Force UTF-8 stdout for Windows consoles
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(BASE_DIR))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.base')
import django
django.setup()
from django.db import transaction, connection
from apps.hadis.models import HadisCategory, Hadis, Transmitters
DUMMY_NAMES = {
"al-imam-al-hafiz-abu-isa-muhammad-ibn-isa-ibn-sawrah-ibn-musa-ibn-al-dahhak-al-sulami-al-tirmidhi",
"al-imam-al-hafiz-abu-isa-muhammad",
"الإمام الحافظ أبو عيسى محمد بن عيسى بن سَوْرَة بن موسى بن الضحاک السُّلَمي الترمذي",
"اسم-عربی",
}
def extract_best_text(json_val, preferred_langs=('en', 'ru', 'fa', 'ar')):
"""Extract the best text from a multilingual JSONField (list of dicts)."""
if not json_val:
return ""
if isinstance(json_val, str):
return json_val.strip()
if isinstance(json_val, list):
for lang in preferred_langs:
for item in json_val:
if isinstance(item, dict) and item.get('language_code') == lang:
val = item.get('text') or item.get('title') or item.get('value') or item.get('name')
if val and str(val).strip():
return str(val).strip()
for item in json_val:
if isinstance(item, dict):
val = item.get('text') or item.get('title') or item.get('value') or item.get('name')
if val and str(val).strip():
return str(val).strip()
elif isinstance(item, str) and item.strip():
return item.strip()
return ""
def extract_transmitter_text(transmitter):
"""Extract best specific name for a transmitter, avoiding dummy/repeated texts."""
candidates = []
for field_val in (transmitter.full_name, transmitter.known_as, transmitter.nickname):
if not field_val or not isinstance(field_val, list):
continue
for lang in ('ru', 'fa', 'en', 'ar'):
for item in field_val:
if isinstance(item, dict) and item.get('language_code') == lang:
val = (item.get('text') or item.get('title') or '').strip()
if val and val not in DUMMY_NAMES and not val.startswith("313"):
candidates.append(val)
if candidates:
return candidates[0]
return extract_best_text(transmitter.full_name) or extract_best_text(transmitter.known_as) or ""
def clean_slug_text(text: str, max_len: int = 80) -> str:
"""Clean and slugify text while preserving readability and length constraints."""
if not text:
return ""
cleaned = re.sub(r'[\(\)\[\]\{\}\<\>\'\"\`\:\;\,\.\?\!\@\#\$\%\^\&\*\+\=\|\/\\]+', ' ', text)
slug = slugify(cleaned, allow_unicode=True).strip('-').lower()
if len(slug) > max_len:
slug = slug[:max_len].rstrip('-')
return slug
def generate_unique_slug(base_slug: str, used_slugs: set, max_len: int = 90) -> str:
"""Ensure slug uniqueness using an in-memory set with counter suffix."""
if not base_slug:
base_slug = "item"
candidate = base_slug
counter = 1
while candidate in used_slugs:
counter += 1
suffix = f"-{counter}"
avail_len = max_len - len(suffix)
candidate = f"{base_slug[:avail_len].rstrip('-')}{suffix}"
used_slugs.add(candidate)
return candidate
def update_categories_slugs(dry_run=False):
print("\n📂 [1/3] Processing HadisCategory...")
categories = list(HadisCategory.objects.all().order_by('id'))
used_slugs = set()
sample_changes = []
for cat in categories:
raw_text = extract_best_text(cat.title, ('en', 'ru', 'fa', 'ar')) or extract_best_text(cat.description)
base_slug = clean_slug_text(raw_text, max_len=75)
if not base_slug:
base_slug = f"category-{cat.source_type or 'item'}-{cat.id}"
new_slug = generate_unique_slug(base_slug, used_slugs)
if len(sample_changes) < 5 and cat.slug != new_slug:
sample_changes.append((cat.id, cat.slug, new_slug, raw_text))
cat.slug = new_slug
print(f" Total categories: {len(categories)}")
for item_id, old_s, new_s, title in sample_changes:
print(f" • ID {item_id:4d} | '{title[:30]}' -> {old_s} ➔ {new_s}")
if not dry_run:
with transaction.atomic():
with connection.cursor() as cursor:
cursor.execute("UPDATE hadis_hadiscategory SET slug = CONCAT('tmp-c-', id);")
HadisCategory.objects.bulk_update(categories, ['slug'], batch_size=1000)
print(" ✅ All categories updated successfully.")
def update_hadis_slugs(dry_run=False):
print("\n📜 [2/3] Processing Hadis...")
hadiths = list(Hadis.objects.all().order_by('id'))
used_slugs = set()
sample_changes = []
for h in hadiths:
raw_text = extract_best_text(h.title, ('en', 'ru', 'fa', 'ar')) or extract_best_text(h.translation) or extract_best_text(h.hadis_status_text)
base_slug = clean_slug_text(raw_text, max_len=75)
if not base_slug or (base_slug.isdigit() and len(base_slug) <= 3):
if h.number:
base_slug = f"hadis-{h.number}" + (f"-{base_slug}" if base_slug else "")
else:
base_slug = f"hadis-{h.id}"
new_slug = generate_unique_slug(base_slug, used_slugs)
if len(sample_changes) < 5 and h.slug != new_slug:
sample_changes.append((h.id, h.slug, new_slug, raw_text or str(h.number)))
h.slug = new_slug
print(f" Total hadiths: {len(hadiths)}")
for item_id, old_s, new_s, title in sample_changes:
print(f" • ID {item_id:4d} | '{title[:30]}' -> {old_s} ➔ {new_s}")
if not dry_run:
with transaction.atomic():
with connection.cursor() as cursor:
cursor.execute("UPDATE hadis_hadis SET slug = CONCAT('tmp-h-', id);")
Hadis.objects.bulk_update(hadiths, ['slug'], batch_size=1000)
print(" ✅ All hadiths updated successfully.")
def update_transmitters_slugs(dry_run=False):
print("\n👤 [3/3] Processing Transmitters...")
transmitters = list(Transmitters.objects.all().order_by('id'))
used_slugs = set()
sample_changes = []
for tr in transmitters:
raw_text = extract_transmitter_text(tr)
base_slug = clean_slug_text(raw_text, max_len=75)
if not base_slug:
base_slug = f"transmitter-{tr.id}"
new_slug = generate_unique_slug(base_slug, used_slugs)
if len(sample_changes) < 5 and tr.slug != new_slug:
sample_changes.append((tr.id, tr.slug, new_slug, raw_text))
tr.slug = new_slug
print(f" Total transmitters: {len(transmitters)}")
for item_id, old_s, new_s, name in sample_changes:
print(f" • ID {item_id:4d} | '{name[:30]}' -> {old_s} ➔ {new_s}")
if not dry_run:
with transaction.atomic():
with connection.cursor() as cursor:
cursor.execute("UPDATE hadis_transmitters SET slug = CONCAT('tmp-t-', id);")
Transmitters.objects.bulk_update(transmitters, ['slug'], batch_size=1000)
print(" ✅ All transmitters updated successfully.")
def main():
parser = argparse.ArgumentParser(description="Update readable slugs for Categories, Hadiths, and Transmitters.")
parser.add_argument("--dry-run", action="store_true", help="Simulate slug generation without saving to database.")
args = parser.parse_args()
start_time = time.time()
mode_text = "DRY RUN (Preview Only)" if args.dry_run else "EXECUTE (Database Update)"
print(f"\n🚀 Starting Slug Optimization [{mode_text}]...")
print("=" * 70)
update_categories_slugs(dry_run=args.dry_run)
update_hadis_slugs(dry_run=args.dry_run)
update_transmitters_slugs(dry_run=args.dry_run)
duration = time.time() - start_time
print("\n" + "=" * 70)
print(f"✨ Completed in {duration:.2f} seconds!")
if __name__ == "__main__":
main()

9
utils/pagination.py

@ -30,4 +30,11 @@ class NoPagination(PageNumberPagination):
'next': None,
'previous': None,
'results': data,
})
})
class OptionalPagination(StandardResultsSetPagination):
def paginate_queryset(self, queryset, request, view=None):
if 'page' in request.query_params or 'page_size' in request.query_params:
return super().paginate_queryset(queryset, request, view)
return None
Loading…
Cancel
Save