From 900b54772898440fbb4aaed301817973cc3122fc Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Sun, 18 Oct 2026 11:26:21 +0330 Subject: [PATCH] feat(hadis): add gender option to Transmitters model, serializers, admin, and XMind view - Add GenderChoices (male/female) field to Transmitters model with migration 0052 - Include gender across public and admin transmitter serializers - Expose gender in TransmittersAdmin filter and list display - Propagate transmitter gender in HadisCategoryXMindView flowchart payload - Add split_transmitters_gender management command --- apps/hadis/admin/transmitter.py | 6 +-- .../commands/split_transmitters_gender.py | 40 +++++++++++++++++++ .../migrations/0052_transmitters_gender.py | 18 +++++++++ apps/hadis/models/transmitter.py | 10 +++++ apps/hadis/serializers/hadis.py | 13 +++--- apps/hadis/serializers/serializers_admin.py | 2 + apps/hadis/views/category.py | 3 ++ 7 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 apps/hadis/management/commands/split_transmitters_gender.py create mode 100644 apps/hadis/migrations/0052_transmitters_gender.py diff --git a/apps/hadis/admin/transmitter.py b/apps/hadis/admin/transmitter.py index 595e047..f217178 100644 --- a/apps/hadis/admin/transmitter.py +++ b/apps/hadis/admin/transmitter.py @@ -438,15 +438,15 @@ class TransmitterOriginalTextInline(TabularInline): class TransmittersAdmin(ModelAdmin): """Admin for Transmitters model""" form = TransmittersAdminForm - list_display = ('display_header', 'birth_year_hijri', 'death_year_hijri') - list_filter = ('birth_year_hijri', 'death_year_hijri') + list_display = ('display_header', 'gender', 'birth_year_hijri', 'death_year_hijri') + list_filter = ('gender', 'birth_year_hijri', 'death_year_hijri') search_fields = ('full_name', 'description') readonly_fields = ('created_at', 'updated_at') inlines = [HadisTransmitterInline, TransmitterOpinionInline, TransmitterOriginalTextInline] fieldsets = ( (None, { - 'fields': ('full_name', 'birth_year_hijri', 'death_year_hijri','known_as','nickname') + 'fields': ('full_name', 'gender', 'birth_year_hijri', 'death_year_hijri','known_as','nickname') }), (_('Additional Information'), { 'fields': ('description','origin','lived_in','died_in','kunya'), diff --git a/apps/hadis/management/commands/split_transmitters_gender.py b/apps/hadis/management/commands/split_transmitters_gender.py new file mode 100644 index 0000000..fbed0fb --- /dev/null +++ b/apps/hadis/management/commands/split_transmitters_gender.py @@ -0,0 +1,40 @@ +import random +from django.core.management.base import BaseCommand +from django.core.cache import cache +from apps.hadis.models import Transmitters + + +class Command(BaseCommand): + help = "Randomly split all existing Transmitters 50/50 between male and female for testing." + + def handle(self, *args, **options): + transmitters = list(Transmitters.objects.all().order_by("id")) + total = len(transmitters) + if total == 0: + self.stdout.write(self.style.WARNING("No transmitters found in the database.")) + return + + random.shuffle(transmitters) + half = total // 2 + + female_ids = [t.id for t in transmitters[:half]] + male_ids = [t.id for t in transmitters[half:]] + + female_updated = Transmitters.objects.filter(id__in=female_ids).update( + gender="female" + ) + male_updated = Transmitters.objects.filter(id__in=male_ids).update( + gender="male" + ) + + # Clear cache so XMind and sync endpoints reflect updated genders immediately + try: + cache.clear() + except Exception: + pass + + self.stdout.write( + self.style.SUCCESS( + f"Successfully updated {total} transmitters: {male_updated} male, {female_updated} female." + ) + ) diff --git a/apps/hadis/migrations/0052_transmitters_gender.py b/apps/hadis/migrations/0052_transmitters_gender.py new file mode 100644 index 0000000..7c33932 --- /dev/null +++ b/apps/hadis/migrations/0052_transmitters_gender.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.30 on 2026-10-18 10:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hadis', '0051_add_category_is_topic'), + ] + + operations = [ + migrations.AddField( + model_name='transmitters', + name='gender', + field=models.CharField(choices=[('male', 'Male'), ('female', 'Female')], default='male', max_length=10, verbose_name='Gender'), + ), + ] diff --git a/apps/hadis/models/transmitter.py b/apps/hadis/models/transmitter.py index 9d2d6fa..954e16a 100644 --- a/apps/hadis/models/transmitter.py +++ b/apps/hadis/models/transmitter.py @@ -139,11 +139,21 @@ class Transmitters(LowercaseSlugMixin, models.Model): SHIA = 'shia', _('Shia') SUNNI = 'sunni', _('Sunni') + class GenderChoices(models.TextChoices): + MALE = 'male', _('Male') + FEMALE = 'female', _('Female') + # Basic Information full_name = models.JSONField(default = list , verbose_name=_('Full Name')) 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')) + gender = models.CharField( + max_length=10, + choices=GenderChoices.choices, + default=GenderChoices.MALE, + verbose_name=_('Gender') + ) 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) diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index 9439ddf..fd3544c 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -231,6 +231,7 @@ class HadisSyncSerializer(serializers.ModelSerializer): 'id': ut.id, 'name': get_localized_text(ut.full_name, request), 'slug': ut.slug, + 'gender': ut.gender, 'known_as': get_localized_text(ut.known_as, request), 'nickname': get_localized_text(ut.nickname, request), 'birth_year_hijri': ut.birth_year_hijri, @@ -242,6 +243,7 @@ class HadisSyncSerializer(serializers.ModelSerializer): 'id': t.id if t else None, 'name': get_localized_text(t.full_name, request) if t else None, 'slug': t.slug if t else None, + 'gender': t.gender if t else None, 'known_as': get_localized_text(t.known_as, request) if t else None, 'nickname': get_localized_text(t.nickname, request) if t else None, 'reliability': reliability_block, @@ -450,7 +452,7 @@ class TransmitterSerializer(serializers.ModelSerializer): model = Transmitters fields = [ 'id', 'full_name', 'arabic_full_name', 'slug','birth_year_hijri', 'death_year_hijri', - "known_as", 'arabic_known_as', 'nickname', 'arabic_nickname', 'reliability','madhhab','generation','share_link' + "known_as", 'arabic_known_as', 'nickname', 'arabic_nickname', 'reliability','madhhab','generation','share_link', 'gender' ] def get_reliability(self, obj): @@ -479,7 +481,7 @@ class TransmitterShortSerializer(serializers.ModelSerializer): model = Transmitters fields = [ 'id', 'full_name', 'arabic_full_name', 'slug','birth_year_hijri', 'death_year_hijri', - "known_as", 'arabic_known_as', 'nickname', 'arabic_nickname', 'reliability' + "known_as", 'arabic_known_as', 'nickname', 'arabic_nickname', 'reliability', 'gender' ] def get_reliability(self, obj): @@ -688,7 +690,7 @@ class TransmitterDetailSerializer(serializers.ModelSerializer): 'madhhab',"in_sahih_muslim","in_sahih_bukhari", "description",'generation','share_link', 'tadlis', 'ikhtilat', 'companion_type', 'arabic_companion_type', 'relatives', - 'father', 'sons' + 'father', 'sons', 'gender' ] def get_father(self, obj): @@ -818,7 +820,7 @@ class TransmitterSyncSerializer(serializers.ModelSerializer): class Meta: model = Transmitters fields = [ - 'id', 'full_name', 'arabic_full_name', 'slug', 'biographical', 'scholars_opinions', 'original_texts','hadis','relations' + 'id', 'full_name', 'arabic_full_name', 'slug', 'gender', 'biographical', 'scholars_opinions', 'original_texts','hadis','relations' ] def get_biographical(self, obj): @@ -837,6 +839,7 @@ class TransmitterSyncSerializer(serializers.ModelSerializer): return { 'full_name': get_localized_text(obj.full_name, request), 'arabic_full_name': get_arabic_localized_text(obj.full_name), + 'gender': obj.gender, 'kunya': get_localized_text(obj.kunya, request), 'arabic_kunya': get_arabic_localized_text(obj.kunya), 'known_as': get_localized_text(obj.known_as, request), @@ -1550,7 +1553,7 @@ class NarratorRelationSerializer(serializers.ModelSerializer): class Meta: model = Transmitters - fields = ['id', 'slug', 'full_name', 'known_as', 'birth_year_hijri', 'death_year_hijri','reliability'] + fields = ['id', 'slug', 'full_name', 'known_as', 'birth_year_hijri', 'death_year_hijri','reliability', 'gender'] def get_reliability(self, obj): """Serialize the reliability foreign key""" diff --git a/apps/hadis/serializers/serializers_admin.py b/apps/hadis/serializers/serializers_admin.py index 026b60a..9ec9611 100644 --- a/apps/hadis/serializers/serializers_admin.py +++ b/apps/hadis/serializers/serializers_admin.py @@ -1903,6 +1903,7 @@ class AdminTransmitterListSerializer(serializers.ModelSerializer): "id", "full_name", "slug", + "gender", "birth_year_hijri", "death_year_hijri", "known_as", @@ -1957,6 +1958,7 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer): "kunya", "known_as", "nickname", + "gender", "slug", "legacy_number", "legacy_id", diff --git a/apps/hadis/views/category.py b/apps/hadis/views/category.py index 86fc8c2..3fb574a 100644 --- a/apps/hadis/views/category.py +++ b/apps/hadis/views/category.py @@ -800,6 +800,7 @@ class HadisCategoryXMindView(APIView): "id": ut.id, "transmitter_id": ut.id, "slug": ut.slug, + "gender": ut.gender, "name": self.get_localized(ut.full_name, lang) or ut.slug, "title": self.get_localized(ut.full_name, lang) or ut.slug, "full_name": self.get_localized(ut.full_name, lang), @@ -826,6 +827,7 @@ class HadisCategoryXMindView(APIView): "node_type": node_type_str, "tag": node_type_str, "slug": tr.slug if tr else (uncertain_candidates[0]["slug"] if uncertain_candidates else None), + "gender": tr.gender if tr else None, "title": "Uncertain narrator" if is_node_uncertain else (self.get_localized(tr.full_name, lang) or tr.slug if tr else "Narrator"), "full_name": "Uncertain narrator" if is_node_uncertain else (self.get_localized(tr.full_name, lang) if tr else "Narrator"), "arabic_full_name": self.get_arabic(tr.full_name) if tr else None, @@ -853,6 +855,7 @@ class HadisCategoryXMindView(APIView): "name": self.get_localized(tr.full_name, lang) or tr.slug, "full_name": self.get_localized(tr.full_name, lang), "slug": tr.slug, + "gender": tr.gender, "reliability": rel_data, } if tr else None, "layer": layer_data,