From b9cf002fe1e8cd47163accf80d5ac0219738ec13 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Tue, 1 Sep 2026 20:51:29 +0330 Subject: [PATCH] narrators sons and fathers added --- .../assign_random_transmitter_families.py | 91 ++++++ .../commands/generate_editions_and_volumes.py | 265 ++++++++++++++++++ apps/hadis/serializers/hadis.py | 64 ++++- apps/hadis/serializers/serializers_admin.py | 71 ++++- 4 files changed, 487 insertions(+), 4 deletions(-) create mode 100644 apps/hadis/management/commands/assign_random_transmitter_families.py create mode 100644 apps/hadis/management/commands/generate_editions_and_volumes.py diff --git a/apps/hadis/management/commands/assign_random_transmitter_families.py b/apps/hadis/management/commands/assign_random_transmitter_families.py new file mode 100644 index 0000000..473f86f --- /dev/null +++ b/apps/hadis/management/commands/assign_random_transmitter_families.py @@ -0,0 +1,91 @@ +import random +from django.core.management.base import BaseCommand +from apps.hadis.models.transmitter import Transmitters, TransmitterRelative, RELATION_TYPE_MAP + + +class Command(BaseCommand): + help = "Assign 1 random father and 2 random sons to every transmitter." + + def add_arguments(self, parser): + parser.add_argument( + "--clear", + action="store_true", + help="Clear existing father/relative children relationships before assigning.", + ) + + def handle(self, *args, **options): + transmitters = list(Transmitters.objects.all()) + total_count = len(transmitters) + + if total_count < 4: + self.stdout.write( + self.style.ERROR( + f"Not enough transmitters found (found {total_count}, need at least 4)." + ) + ) + return + + self.stdout.write( + self.style.NOTICE( + f"Starting random family assignment for {total_count} transmitters..." + ) + ) + + relatives_to_create = [] + updated_count = 0 + + for tr in transmitters: + # Available candidates excluding self + candidates = [t for t in transmitters if t.id != tr.id] + + # 1. Pick 1 random father + father = random.choice(candidates) + tr.father = father + tr.save(update_fields=["father"]) + + # Remove father from candidates for sons + son_candidates = [t for t in candidates if t.id != father.id] + if len(son_candidates) < 2: + son_candidates = candidates + + # 2. Pick 2 random sons + sons = random.sample(son_candidates, 2) + + # Clear old children relatives for this transmitter + TransmitterRelative.objects.filter( + related_to=tr, relation_type="children" + ).delete() + + # Prepare 2 children relatives + for son in sons: + son_name = "" + if son.full_name and isinstance(son.full_name, list) and len(son.full_name) > 0: + first = son.full_name[0] + if isinstance(first, dict): + son_name = first.get("text", "") + if not son_name and hasattr(son, "get_name"): + son_name = son.get_name("en") or son.get_name("ar") or f"Narrator {son.id}" + + relatives_to_create.append( + TransmitterRelative( + related_to=tr, + relation_type="children", + relation_value=RELATION_TYPE_MAP.get("children", []), + name=son_name, + narrator=son, + ) + ) + + updated_count += 1 + if updated_count % 10 == 0: + self.stdout.write(f"Processed {updated_count}/{total_count}...") + + if relatives_to_create: + TransmitterRelative.objects.bulk_create(relatives_to_create) + + self.stdout.write( + self.style.SUCCESS( + f"Successfully assigned 1 father and 2 sons to all {updated_count} transmitters!" + ) + ) + diff --git a/apps/hadis/management/commands/generate_editions_and_volumes.py b/apps/hadis/management/commands/generate_editions_and_volumes.py new file mode 100644 index 0000000..16aff19 --- /dev/null +++ b/apps/hadis/management/commands/generate_editions_and_volumes.py @@ -0,0 +1,265 @@ +import os +import random +import urllib.request +from django.core.management.base import BaseCommand +from django.db import transaction +from django.core.files.base import ContentFile +from apps.hadis.models import ( + BookReference, + BookAuthor, + BookEdition, + BookVolume, + BookResearcher, + BookEditor, +) + +class Command(BaseCommand): + help = 'Generates randomized Editions and Volumes (with sample cover image and PDF file) for all BookReferences.' + + def add_arguments(self, parser): + parser.add_argument( + '--clear', + action='store_true', + default=True, + help='Clear existing BookEditions and BookVolumes before generating (default: True)', + ) + parser.add_argument( + '--keep-existing', + action='store_true', + help='Do not clear existing data, only add/modify', + ) + + def _fetch_sample_assets(self): + """ + Downloads a lightweight Islamic cover image and sample PDF from the web. + Falls back to generated placeholder bytes if network request fails. + """ + self.stdout.write("Downloading sample Islamic book image and sample PDF...") + + image_url = "https://images.unsplash.com/photo-1609599006353-e629aaabfeae?w=500&auto=format&fit=crop&q=80" + image_bytes = None + + try: + req = urllib.request.Request(image_url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}) + with urllib.request.urlopen(req, timeout=15) as resp: + image_bytes = resp.read() + self.stdout.write(self.style.SUCCESS(f"Downloaded sample cover image ({len(image_bytes)} bytes).")) + except Exception as e: + self.stdout.write(self.style.WARNING(f"Failed to download image ({e}), generating a local fallback image...")) + from PIL import Image, ImageDraw + import io + img = Image.new('RGB', (400, 600), color='#1e293b') + draw = ImageDraw.Draw(img) + draw.rectangle([20, 20, 380, 580], outline='#e2e8f0', width=3) + buf = io.BytesIO() + img.save(buf, format='JPEG') + image_bytes = buf.getvalue() + + pdf_url = "https://raw.githubusercontent.com/mozilla/pdf.js/master/examples/learning/helloworld.pdf" + pdf_bytes = None + + try: + req = urllib.request.Request(pdf_url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}) + with urllib.request.urlopen(req, timeout=15) as resp: + pdf_bytes = resp.read() + self.stdout.write(self.style.SUCCESS(f"Downloaded sample PDF document ({len(pdf_bytes)} bytes).")) + except Exception as e: + self.stdout.write(self.style.WARNING(f"Failed to download PDF ({e}), generating a fallback PDF...")) + from reportlab.pdfgen import canvas + import io + buf = io.BytesIO() + p = canvas.Canvas(buf) + p.drawString(100, 750, "Sample Islamic Hadith Reference Document") + p.drawString(100, 720, "This is a lightweight sample volume file.") + p.showPage() + p.save() + pdf_bytes = buf.getvalue() + + return image_bytes, pdf_bytes + + def handle(self, *args, **options): + clear_existing = not options.get('keep_existing', False) + + references = list(BookReference.objects.all()) + authors = list(BookAuthor.objects.all()) + + if not references: + self.stdout.write(self.style.ERROR("No BookReference found in the database.")) + return + + self.stdout.write(f"Found {len(references)} BookReferences and {len(authors)} BookAuthors.") + + if clear_existing: + self.stdout.write(self.style.WARNING("Clearing existing BookEditions and BookVolumes...")) + BookVolume.objects.all().delete() + BookEdition.objects.all().delete() + BookResearcher.objects.all().delete() + BookEditor.objects.all().delete() + + # Download or prepare sample assets + image_bytes, pdf_bytes = self._fetch_sample_assets() + + # Publishers pool + publishers_pool = [ + [ + {"language_code": "fa", "text": "دار الحديث"}, + {"language_code": "ar", "text": "دار الحديث للطباعة والنشر"}, + {"language_code": "en", "text": "Dar al-Hadith Publications"}, + ], + [ + {"language_code": "fa", "text": "مؤسسة آل البيت (ع) لإحياء التراث"}, + {"language_code": "ar", "text": "مؤسسة آل البيت عليهم السلام لإحياء التراث"}, + {"language_code": "en", "text": "Aal al-Bayt Institute for Heritage Revival"}, + ], + [ + {"language_code": "fa", "text": "دار الكتب الإسلامية"}, + {"language_code": "ar", "text": "دار الكتب الإسلامية"}, + {"language_code": "en", "text": "Dar al-Kutub al-Islamiyya"}, + ], + [ + {"language_code": "fa", "text": "منشورات الشريف الرضي"}, + {"language_code": "ar", "text": "منشورات الشريف الرضي"}, + {"language_code": "en", "text": "Al-Sharif al-Razi Publications"}, + ], + [ + {"language_code": "fa", "text": "مؤسسة النشر الإسلامي"}, + {"language_code": "ar", "text": "مؤسسة النشر الإسلامي التابعة لجماعة المدرسين"}, + {"language_code": "en", "text": "Islamic Publishing Foundation"}, + ], + [ + {"language_code": "fa", "text": "مؤسسة البلاغ"}, + {"language_code": "ar", "text": "مؤسسة البلاغ للطباعة والنشر والتوزيع"}, + {"language_code": "en", "text": "Al-Balagh Foundation"}, + ], + ] + + cities_pool = [ + [{"language_code": "fa", "text": "قم"}, {"language_code": "ar", "text": "قم المقدسة"}, {"language_code": "en", "text": "Qom"}], + [{"language_code": "fa", "text": "بیروت"}, {"language_code": "ar", "text": "بيروت"}, {"language_code": "en", "text": "Beirut"}], + [{"language_code": "fa", "text": "نجف"}, {"language_code": "ar", "text": "النجف الأشرف"}, {"language_code": "en", "text": "Najaf"}], + [{"language_code": "fa", "text": "تهران"}, {"language_code": "ar", "text": "طهران"}, {"language_code": "en", "text": "Tehran"}], + [{"language_code": "fa", "text": "مشهد"}, {"language_code": "ar", "text": "مشهد المقدسة"}, {"language_code": "en", "text": "Mashhad"}], + ] + + countries_pool = [ + [{"language_code": "fa", "text": "ایران"}, {"language_code": "ar", "text": "إيران"}, {"language_code": "en", "text": "Iran"}], + [{"language_code": "fa", "text": "لبنان"}, {"language_code": "ar", "text": "لبنان"}, {"language_code": "en", "text": "Lebanon"}], + [{"language_code": "fa", "text": "عراق"}, {"language_code": "ar", "text": "العراق"}, {"language_code": "en", "text": "Iraq"}], + ] + + edition_names = ["چاپ اول", "چاپ دوم", "چاپ سوم", "طبعة ثانية منقحة", "طبعة أولى محققة", "First Revised Edition", "Second Edition"] + years_pool = ["1418 هـ.ق", "1422 هـ.ق", "1429 هـ.ق", "1436 هـ.ق", "1441 هـ.ق", "1385 ش", "1392 ش", "1398 ش", "1402 ش"] + + total_editions_created = 0 + total_volumes_created = 0 + total_researchers_created = 0 + total_editors_created = 0 + + self.stdout.write("Generating Editions and Volumes for BookReferences...") + + with transaction.atomic(): + for ref in references: + # Random choice: whether this reference has editions or only direct volumes + has_editions = random.choice([True, False]) + ref.has_editions = has_editions + ref.save(update_fields=['has_editions']) + + if has_editions: + num_editions = random.randint(1, 3) + for ed_idx in range(1, num_editions + 1): + num_volumes = random.randint(3, 5) + pub = random.choice(publishers_pool) + city = random.choice(cities_pool) + country = random.choice(countries_pool) + ed_num = random.choice(edition_names) + year = random.choice(years_pool) + isbn = f"978-964-{random.randint(100, 999)}-{random.randint(10, 99)}-{random.randint(0, 9)}" + + edition = BookEdition.objects.create( + book_reference=ref, + publisher=pub, + city_of_publication=city, + country_of_publication=country, + edition_number=f"{ed_num} ({ed_idx})", + year_of_publication=year, + number_of_volumes=num_volumes, + isbn=isbn, + notes=[ + {"language_code": "fa", "text": f"نسخه معتبر تصحیح شده با مقابله با نسخ خطی {ed_idx}"}, + {"language_code": "en", "text": f"Verified critical edition compared against manuscripts {ed_idx}"} + ], + source_url=f"https://lib.eshia.ir/{ref.id}/{edition_names[0]}" + ) + total_editions_created += 1 + + # Assign random researchers and editors from existing authors + if authors: + sample_researchers = random.sample(authors, min(len(authors), random.randint(1, 2))) + for author in sample_researchers: + BookResearcher.objects.create( + book_edition=edition, + author=author + ) + total_researchers_created += 1 + + sample_editors = random.sample(authors, min(len(authors), random.randint(1, 2))) + for author in sample_editors: + BookEditor.objects.create( + book_edition=edition, + author=author + ) + total_editors_created += 1 + + # Create 3 to 5 volumes for this edition + for vol_idx in range(1, num_volumes + 1): + vol = BookVolume( + book_reference=ref, + edition=edition, + title=f"جلد {vol_idx}", + ) + vol.image.save( + f"ref_{ref.id}_ed_{edition.id}_vol_{vol_idx}_cover.jpg", + ContentFile(image_bytes), + save=False + ) + vol.file.save( + f"ref_{ref.id}_ed_{edition.id}_vol_{vol_idx}.pdf", + ContentFile(pdf_bytes), + save=False + ) + vol.save() + total_volumes_created += 1 + + else: + # Direct volumes without editions + num_volumes = random.randint(3, 5) + for vol_idx in range(1, num_volumes + 1): + vol = BookVolume( + book_reference=ref, + edition=None, + title=f"جلد {vol_idx}", + ) + vol.image.save( + f"ref_{ref.id}_vol_{vol_idx}_cover.jpg", + ContentFile(image_bytes), + save=False + ) + vol.file.save( + f"ref_{ref.id}_vol_{vol_idx}.pdf", + ContentFile(pdf_bytes), + save=False + ) + vol.save() + total_volumes_created += 1 + + self.stdout.write( + self.style.SUCCESS( + f"\n--- Seeding Completed Successfully ---\n" + f"Processed {len(references)} BookReferences:\n" + f" - Total BookEditions created: {total_editions_created}\n" + f" - Total BookVolumes created: {total_volumes_created}\n" + f" - Total Researchers assigned: {total_researchers_created}\n" + f" - Total Editors assigned: {total_editors_created}\n" + f" - Cover image & PDF attached to all volumes." + ) + ) diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index 200688d..efd5ffe 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -585,6 +585,8 @@ class TransmitterDetailSerializer(serializers.ModelSerializer): description = LocalizedField() reliability = serializers.SerializerMethodField() relatives = serializers.SerializerMethodField() + father = serializers.SerializerMethodField() + sons = serializers.SerializerMethodField() share_link = serializers.CharField(read_only=True) class Meta: @@ -596,9 +598,69 @@ class TransmitterDetailSerializer(serializers.ModelSerializer): 'death_year_hijri','age_at_death','reliability', 'madhhab',"in_sahih_muslim","in_sahih_bukhari", "description",'generation','share_link', - 'tadlis', 'ikhtilat', 'companion_type', 'relatives' + 'tadlis', 'ikhtilat', 'companion_type', 'relatives', + 'father', 'sons' ] + def get_father(self, obj): + """Serialize the father foreign key or relative""" + father_obj = obj.father + request = self.context.get('request') + if not father_obj: + parent_rel = obj.relatives.filter(relation_type__in=['parents', 'stepfather']).first() + if parent_rel: + if parent_rel.narrator: + father_obj = parent_rel.narrator + elif parent_rel.name: + return { + 'id': None, + 'name': parent_rel.name, + 'arabic_name': parent_rel.name, + 'slug': None, + } + if father_obj: + return { + 'id': father_obj.id, + 'name': get_localized_text(father_obj.full_name, request) or (father_obj.get_name('en') if hasattr(father_obj, 'get_name') else None) or '—', + 'arabic_name': get_arabic_localized_text(father_obj.full_name) or (father_obj.get_name('ar') if hasattr(father_obj, 'get_name') else None), + 'slug': father_obj.slug, + } + return None + + def get_sons(self, obj): + """Serialize all sons (from children_as_father FK and TransmitterRelative)""" + request = self.context.get('request') + result = [] + seen_ids = set() + + for child in obj.children_as_father.all(): + seen_ids.add(child.id) + result.append({ + 'id': child.id, + 'name': get_localized_text(child.full_name, request) or (child.get_name('en') if hasattr(child, 'get_name') else None) or '—', + 'arabic_name': get_arabic_localized_text(child.full_name) or (child.get_name('ar') if hasattr(child, 'get_name') else None), + 'slug': child.slug, + }) + + for rel in obj.relatives.filter(relation_type='children'): + if rel.narrator and rel.narrator.id not in seen_ids: + seen_ids.add(rel.narrator.id) + result.append({ + 'id': rel.narrator.id, + 'name': get_localized_text(rel.narrator.full_name, request) or (rel.narrator.get_name('en') if hasattr(rel.narrator, 'get_name') else None) or '—', + 'arabic_name': get_arabic_localized_text(rel.narrator.full_name) or (rel.narrator.get_name('ar') if hasattr(rel.narrator, 'get_name') else None), + 'slug': rel.narrator.slug, + }) + elif not rel.narrator and rel.name: + result.append({ + 'id': None, + 'name': rel.name, + 'arabic_name': rel.name, + 'slug': None, + }) + + return result + def get_reliability(self, obj): """Serialize the reliability foreign key""" if obj.reliability: diff --git a/apps/hadis/serializers/serializers_admin.py b/apps/hadis/serializers/serializers_admin.py index e633dac..b0abaf7 100644 --- a/apps/hadis/serializers/serializers_admin.py +++ b/apps/hadis/serializers/serializers_admin.py @@ -1037,6 +1037,18 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer): queryset=TransmitterReliability.objects.all(), required=False, ) + father = serializers.PrimaryKeyRelatedField( + queryset=Transmitters.objects.all(), + required=False, + allow_null=True, + ) + father_detail = serializers.SerializerMethodField(read_only=True) + sons = serializers.SerializerMethodField(read_only=True) + sons_ids = serializers.ListField( + child=serializers.IntegerField(), + required=False, + write_only=True, + ) share_link = serializers.CharField(read_only=True) class Meta: @@ -1065,11 +1077,46 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer): "description", "thumbnail", "remove_thumbnail", + "father", + "father_detail", + "sons", + "sons_ids", "share_link", "created_at", "updated_at", ] - read_only_fields = ["id", "slug", "share_link", "created_at", "updated_at", "reliability_detail"] + read_only_fields = ["id", "slug", "share_link", "created_at", "updated_at", "reliability_detail", "father_detail", "sons"] + + def get_father_detail(self, obj): + if obj.father: + name = obj.father.get_name('en') if hasattr(obj.father, 'get_name') else None + if not name and obj.father.full_name and isinstance(obj.father.full_name, list) and len(obj.father.full_name) > 0: + first = obj.father.full_name[0] + if isinstance(first, dict): + name = first.get('text', '—') + return { + "id": obj.father.id, + "full_name": obj.father.full_name, + "name": name or '—', + "slug": obj.father.slug, + } + return None + + def get_sons(self, obj): + result = [] + for son in obj.children_as_father.all(): + name = son.get_name('en') if hasattr(son, 'get_name') else None + if not name and son.full_name and isinstance(son.full_name, list) and len(son.full_name) > 0: + first = son.full_name[0] + if isinstance(first, dict): + name = first.get('text', '—') + result.append({ + "id": son.id, + "full_name": son.full_name, + "name": name or '—', + "slug": son.slug, + }) + return result def to_internal_value(self, data): import json @@ -1100,22 +1147,40 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer): "birth_year_hijri", "death_year_hijri", "companion_type", + "father", ]: if field in data and (data[field] == "" or data[field] is None or data[field] == "null" or data[field] == "undefined" or (isinstance(data[field], str) and not data[field].strip())): data[field] = None + if "sons_ids" in data: + val = data["sons_ids"] + if isinstance(val, str): + try: + data["sons_ids"] = json.loads(val) + except ValueError: + pass + return super().to_internal_value(data) def create(self, validated_data): validated_data.pop("remove_thumbnail", False) - return super().create(validated_data) + sons_ids = validated_data.pop("sons_ids", None) + instance = super().create(validated_data) + if sons_ids is not None: + Transmitters.objects.filter(id__in=sons_ids).update(father=instance) + return instance def update(self, instance, validated_data): remove_thumbnail = validated_data.pop("remove_thumbnail", False) + sons_ids = validated_data.pop("sons_ids", None) if remove_thumbnail and instance.thumbnail: instance.thumbnail.delete(save=False) instance.thumbnail = None - return super().update(instance, validated_data) + instance = super().update(instance, validated_data) + if sons_ids is not None: + instance.children_as_father.exclude(id__in=sons_ids).update(father=None) + Transmitters.objects.filter(id__in=sons_ids).update(father=instance) + return instance class AdminBookSubjectAreaSerializer(serializers.ModelSerializer):