You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

303 lines
12 KiB

# backend/apps/hadis/management/commands/seed_hadis_chains.py
import random
import re
from django.core.management.base import BaseCommand
from django.core.cache import cache
from django.db import transaction
from apps.hadis.models import (
Hadis,
Transmitters,
HadisTransmitter,
NarratorLayer,
TransmitterReliability,
)
DEFAULT_LAYERS = [
{
"number": 1,
"slug_suffix": "l1-intermediate-regional-transmitters",
"name": [
{"language_code": "en", "text": "Intermediate Regional Transmitters"},
{"language_code": "ru", "text": "Промежуточные региональные передатчики"},
],
"description": [
{
"language_code": "en",
"text": "Scholars and compilers transmitting across regional centers of learning.",
},
{
"language_code": "ru",
"text": "Ученые и составители, передававшие хадисы между региональными центрами знаний.",
},
],
},
{
"number": 2,
"slug_suffix": "l2-primary-chain-early-narrators",
"name": [
{"language_code": "en", "text": "Primary Chain & Early Narrators"},
{"language_code": "ru", "text": "Первичная цепочка и ранние передатчики"},
],
"description": [
{
"language_code": "en",
"text": "Early generation transmitters, companions, and direct students.",
},
{
"language_code": "ru",
"text": "Передатчики раннего поколения, сподвижники и прямые ученики.",
},
],
},
]
class Command(BaseCommand):
help = (
"Reassigns non-hadith transmission chains to chainless hadith arguments, "
"creates chains for any remaining chainless hadiths, removes leftover non-hadith chains, "
"and normalizes each hadith chain to have 1 uncertain node and 2-3 gaps."
)
def handle(self, *args, **options):
rng = random.Random(42)
narrators = list(Transmitters.objects.all().order_by("id"))
reliabilities = list(TransmitterReliability.objects.all().order_by("id"))
if len(narrators) < 6:
self.stdout.write(
self.style.ERROR("Not enough Transmitters found. Need at least 6 narrators.")
)
return
# 1. Identify hadith vs non-hadith arguments
hadith_qs = Hadis.objects.filter(category__source_type="hadith").order_by("id")
non_hadith_with_chains = list(
Hadis.objects.exclude(category__source_type="hadith")
.filter(transmitters__isnull=False)
.distinct()
.order_by("id")
)
chainless_hadiths = list(
hadith_qs.filter(transmitters__isnull=True).distinct().order_by("id")
)
self.stdout.write(
f"Total hadith arguments: {hadith_qs.count()} | "
f"Chainless hadiths: {len(chainless_hadiths)} | "
f"Non-hadith arguments with chains: {len(non_hadith_with_chains)}"
)
with transaction.atomic():
# Step 1: Move chains from non-hadith arguments to chainless hadith arguments (1-to-1) in bulk
pairs_count = min(len(non_hadith_with_chains), len(chainless_hadiths))
src_to_dst = {
non_hadith_with_chains[i].id: chainless_hadiths[i].id
for i in range(pairs_count)
}
if src_to_dst:
dst_ids = list(src_to_dst.values())
src_ids = list(src_to_dst.keys())
# Remove any stray empty layers on dst_hadis if present
NarratorLayer.objects.filter(hadis_id__in=dst_ids).delete()
# Bulk update layers
layers_to_move = list(
NarratorLayer.objects.filter(hadis_id__in=src_ids).order_by("id")
)
for layer in layers_to_move:
old_hadis_id = layer.hadis_id
new_hadis_id = src_to_dst[old_hadis_id]
old_slug = layer.slug or f"hadis-{old_hadis_id}-l{layer.number}"
new_slug = re.sub(
rf"^hadis-{old_hadis_id}-",
f"hadis-{new_hadis_id}-",
old_slug,
)
if new_slug == old_slug:
new_slug = f"hadis-{new_hadis_id}-l{layer.number}-{layer.id}"
layer.hadis_id = new_hadis_id
layer.slug = new_slug
NarratorLayer.objects.bulk_update(
layers_to_move, ["hadis_id", "slug"], batch_size=500
)
# Bulk update transmitters
txs_to_move = list(
HadisTransmitter.objects.filter(hadis_id__in=src_ids).order_by("id")
)
for tx in txs_to_move:
tx.hadis_id = src_to_dst[tx.hadis_id]
HadisTransmitter.objects.bulk_update(
txs_to_move, ["hadis_id"], batch_size=500
)
self.stdout.write(
self.style.SUCCESS(
f"Moved {len(src_to_dst)} chains ({len(layers_to_move)} layers, {len(txs_to_move)} transmitters) "
"from non-hadith arguments to chainless hadith arguments."
)
)
# Step 2: Delete any remaining chains/layers on non-hadith arguments
deleted_tx, _ = (
HadisTransmitter.objects.exclude(hadis__category__source_type="hadith").delete()
)
deleted_layers, _ = (
NarratorLayer.objects.filter(hadis__isnull=False)
.exclude(hadis__category__source_type="hadith")
.delete()
)
if deleted_tx or deleted_layers:
self.stdout.write(
self.style.WARNING(
f"Deleted leftover non-hadith records: {deleted_tx} transmitters, {deleted_layers} layers."
)
)
# Step 3: Seed chains for any still-chainless hadith arguments in bulk
remaining_chainless = list(
hadith_qs.filter(transmitters__isnull=True).distinct().order_by("id")
)
if remaining_chainless:
rem_ids = [h.id for h in remaining_chainless]
NarratorLayer.objects.filter(hadis_id__in=rem_ids).delete()
new_layers = []
for hadis in remaining_chainless:
for ldef in DEFAULT_LAYERS:
new_layers.append(
NarratorLayer(
hadis_id=hadis.id,
number=ldef["number"],
name=ldef["name"],
description=ldef["description"],
slug=f"hadis-{hadis.id}-{ldef['slug_suffix']}",
)
)
created_layers = NarratorLayer.objects.bulk_create(new_layers, batch_size=500)
layers_by_hadis = {}
for l in created_layers:
layers_by_hadis.setdefault(l.hadis_id, []).append(l)
new_txs = []
for hadis in remaining_chainless:
h_layers = sorted(layers_by_hadis[hadis.id], key=lambda x: x.number)
num_nodes = rng.randint(8, 10)
chosen_narrators = rng.sample(narrators, num_nodes)
split_idx = num_nodes // 2
for order_idx, narrator in enumerate(chosen_narrators):
layer_obj = h_layers[0] if order_idx < split_idx else h_layers[1]
status_obj = rng.choice(reliabilities) if reliabilities else None
new_txs.append(
HadisTransmitter(
hadis_id=hadis.id,
transmitter=narrator,
chain_index=0,
order=order_idx,
narrator_layer=layer_obj,
status=status_obj,
is_gap=False,
is_uncertain=False,
)
)
HadisTransmitter.objects.bulk_create(new_txs, batch_size=500)
self.stdout.write(
self.style.SUCCESS(
f"Created new chains for {len(remaining_chainless)} remaining chainless hadiths."
)
)
# Step 4: Normalize all hadith chains so every hadith has:
# - 1 uncertain node (is_uncertain=True with 2 candidate transmitters in uncertain_transmitters) per chain
# - 2 to 3 gaps (is_gap=True) per hadith
ThroughModel = HadisTransmitter.uncertain_transmitters.through
all_hadiths = list(
hadith_qs.prefetch_related("transmitters", "narrator_layers").order_by("id")
)
nodes_to_update = []
hadis_ids = [h.id for h in all_hadiths]
ThroughModel.objects.filter(hadistransmitter__hadis_id__in=hadis_ids).delete()
new_m2m_relations = []
normalized_count = 0
for hadis in all_hadiths:
nodes = sorted(
list(hadis.transmitters.all()),
key=lambda x: (x.chain_index, x.order, x.id),
)
if not nodes:
continue
chains = {}
for n in nodes:
chains.setdefault(n.chain_index, []).append(n)
for n in nodes:
n.is_gap = False
n.is_uncertain = False
uncertain_node_ids = set()
for c_idx, c_nodes in chains.items():
candidate_pool = c_nodes[1:] if len(c_nodes) > 2 else c_nodes
unc_node = rng.choice(candidate_pool)
unc_node.is_uncertain = True
uncertain_node_ids.add(unc_node.id)
used_tx_ids = {n.transmitter_id for n in c_nodes if n.transmitter_id}
available_candidates = [t for t in narrators if t.id not in used_tx_ids]
if len(available_candidates) < 2:
available_candidates = [
t for t in narrators if t.id != unc_node.transmitter_id
]
picked_candidates = rng.sample(
available_candidates, min(2, len(available_candidates))
)
for cand in picked_candidates:
new_m2m_relations.append(
ThroughModel(
hadistransmitter_id=unc_node.id,
transmitters_id=cand.id,
)
)
target_gaps = rng.choice([2, 3])
gap_pool = [
n for n in nodes if n.id not in uncertain_node_ids and n.order > 0
]
if len(gap_pool) < target_gaps:
gap_pool = [n for n in nodes if n.id not in uncertain_node_ids]
if len(gap_pool) < target_gaps:
gap_pool = list(nodes)
chosen_gaps = rng.sample(gap_pool, min(target_gaps, len(gap_pool)))
for g_node in chosen_gaps:
g_node.is_gap = True
nodes_to_update.extend(nodes)
normalized_count += 1
HadisTransmitter.objects.bulk_update(
nodes_to_update, ["is_gap", "is_uncertain"], batch_size=500
)
ThroughModel.objects.bulk_create(new_m2m_relations, batch_size=500)
self.stdout.write(
self.style.SUCCESS(
f"Normalized {normalized_count} hadith chains (1 uncertain node per chain, 2-3 gaps per hadith)."
)
)
try:
cache.clear()
self.stdout.write(self.style.SUCCESS("Cleared Django cache."))
except Exception as e:
self.stdout.write(self.style.WARNING(f"Cache clear warning: {e}"))