Browse Source
refactor(transmitters): convert original text to single text field with data migration and populate reference metadata scripts
master
refactor(transmitters): convert original text to single text field with data migration and populate reference metadata scripts
master
7 changed files with 404 additions and 35 deletions
-
30apps/hadis/admin/transmitter.py
-
100apps/hadis/migrations/0047_transmitteroriginaltext_text_to_textfield.py
-
2apps/hadis/models/transmitter.py
-
4apps/hadis/serializers/hadis.py
-
31apps/hadis/serializers/serializers_admin.py
-
32scripts/inspect_refs.py
-
240scripts/populate_reference_metadata.py
@ -0,0 +1,100 @@ |
|||||
|
# Generated by Django on 2026-10-08 |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
|
||||
|
|
||||
|
def migrate_text_data_forward(apps, schema_editor): |
||||
|
TransmitterOriginalText = apps.get_model('hadis', 'TransmitterOriginalText') |
||||
|
items_to_update = [] |
||||
|
|
||||
|
for obj in TransmitterOriginalText.objects.all(): |
||||
|
raw_text = obj.text |
||||
|
raw_translation = obj.translation |
||||
|
|
||||
|
primary_text = "" |
||||
|
items_by_lang = {} |
||||
|
|
||||
|
if isinstance(raw_text, list): |
||||
|
for item in raw_text: |
||||
|
if isinstance(item, dict): |
||||
|
lang = item.get('language_code') or 'unknown' |
||||
|
txt = (item.get('text') or item.get('title') or item.get('value') or '').strip() |
||||
|
if txt: |
||||
|
items_by_lang[lang] = txt |
||||
|
|
||||
|
if 'ar' in items_by_lang: |
||||
|
primary_text = items_by_lang['ar'] |
||||
|
elif 'fa' in items_by_lang: |
||||
|
primary_text = items_by_lang['fa'] |
||||
|
elif items_by_lang: |
||||
|
primary_text = list(items_by_lang.values())[0] |
||||
|
elif isinstance(raw_text, str): |
||||
|
primary_text = raw_text |
||||
|
|
||||
|
# Ensure all other language translations from text are preserved in translation field |
||||
|
updated_trans = list(raw_translation) if isinstance(raw_translation, list) else [] |
||||
|
existing_langs = set() |
||||
|
for tr in updated_trans: |
||||
|
if isinstance(tr, dict) and tr.get('language_code'): |
||||
|
existing_langs.add(tr.get('language_code')) |
||||
|
|
||||
|
for lang, txt in items_by_lang.items(): |
||||
|
if lang not in existing_langs: |
||||
|
updated_trans.append({ |
||||
|
'language_code': lang, |
||||
|
'title': txt, |
||||
|
'text': txt |
||||
|
}) |
||||
|
existing_langs.add(lang) |
||||
|
|
||||
|
obj.temp_text = primary_text |
||||
|
obj.translation = updated_trans |
||||
|
items_to_update.append(obj) |
||||
|
|
||||
|
if len(items_to_update) >= 200: |
||||
|
TransmitterOriginalText.objects.bulk_update(items_to_update, ['temp_text', 'translation']) |
||||
|
items_to_update = [] |
||||
|
|
||||
|
if items_to_update: |
||||
|
TransmitterOriginalText.objects.bulk_update(items_to_update, ['temp_text', 'translation']) |
||||
|
|
||||
|
|
||||
|
def migrate_text_data_backward(apps, schema_editor): |
||||
|
TransmitterOriginalText = apps.get_model('hadis', 'TransmitterOriginalText') |
||||
|
items_to_update = [] |
||||
|
|
||||
|
for obj in TransmitterOriginalText.objects.all(): |
||||
|
if isinstance(obj.text, str) and obj.text: |
||||
|
obj.temp_text = [{'language_code': 'ar', 'text': obj.text, 'title': obj.text}] |
||||
|
items_to_update.append(obj) |
||||
|
if len(items_to_update) >= 200: |
||||
|
TransmitterOriginalText.objects.bulk_update(items_to_update, ['temp_text']) |
||||
|
items_to_update = [] |
||||
|
|
||||
|
if items_to_update: |
||||
|
TransmitterOriginalText.objects.bulk_update(items_to_update, ['temp_text']) |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('hadis', '0046_originaltextreference_hadith_number'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AddField( |
||||
|
model_name='transmitteroriginaltext', |
||||
|
name='temp_text', |
||||
|
field=models.TextField(blank=True, default='', verbose_name='Text'), |
||||
|
), |
||||
|
migrations.RunPython(migrate_text_data_forward, reverse_code=migrate_text_data_backward), |
||||
|
migrations.RemoveField( |
||||
|
model_name='transmitteroriginaltext', |
||||
|
name='text', |
||||
|
), |
||||
|
migrations.RenameField( |
||||
|
model_name='transmitteroriginaltext', |
||||
|
old_name='temp_text', |
||||
|
new_name='text', |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,32 @@ |
|||||
|
import os |
||||
|
import sys |
||||
|
import django |
||||
|
|
||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
||||
|
sys.stdout.reconfigure(encoding='utf-8') |
||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.base') |
||||
|
django.setup() |
||||
|
|
||||
|
from apps.hadis.models import HadisReference, InterpretationReference, BookReference, BookEdition, BookVolume |
||||
|
from apps.hadis.models.transmitter import OriginalTextReference |
||||
|
|
||||
|
print('=== 5 SAMPLE HADIS REFERENCES ===') |
||||
|
for r in HadisReference.objects.select_related('book_reference', 'edition', 'book_volume')[:5]: |
||||
|
print(f'ID {r.id}: Book={r.book_reference_id}, Edition={r.edition_id}, BookVol={r.book_volume_id}, VolText="{r.volume}", Page="{r.pages}", HadithNo="{r.hadith_number}"') |
||||
|
|
||||
|
print('\n=== 5 SAMPLE INTERPRETATION REFERENCES ===') |
||||
|
for r in InterpretationReference.objects.select_related('book_reference', 'edition', 'book_volume')[:5]: |
||||
|
print(f'ID {r.id}: Book={r.book_reference_id}, Edition={r.edition_id}, BookVol={r.book_volume_id}, VolText="{r.volume}", Page="{r.pages}", HadithNo="{r.hadith_number}"') |
||||
|
|
||||
|
print('\n=== 5 SAMPLE ORIGINAL TEXT REFERENCES ===') |
||||
|
for r in OriginalTextReference.objects.select_related('book_reference', 'edition', 'book_volume')[:5]: |
||||
|
print(f'ID {r.id}: Book={r.book_reference_id}, Edition={r.edition_id}, BookVol={r.book_volume_id}, VolText="{r.volume}", Page="{r.pages}", HadithNo="{r.hadith_number}"') |
||||
|
|
||||
|
# Also check how many BookEditions exist per book |
||||
|
print('\n=== BOOK EDITIONS AND VOLUMES INFO ===') |
||||
|
sample_books = BookReference.objects.all()[:5] |
||||
|
for b in sample_books: |
||||
|
ed_count = b.editions.count() |
||||
|
vol_count = b.volumes.count() |
||||
|
editions = list(b.editions.values('id', 'edition_number', 'publisher')) |
||||
|
print(f"Book ID {b.id} ({b.slug}): {ed_count} editions, {vol_count} volumes. Editions: {editions}") |
||||
@ -0,0 +1,240 @@ |
|||||
|
import os |
||||
|
import sys |
||||
|
import random |
||||
|
import django |
||||
|
|
||||
|
# Setup environment |
||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
||||
|
sys.stdout.reconfigure(encoding='utf-8') |
||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.base') |
||||
|
django.setup() |
||||
|
|
||||
|
from django.db import transaction |
||||
|
from django.db.models.signals import post_save, post_delete, m2m_changed |
||||
|
from apps.hadis.signals import clear_hadis_cache, clear_hadis_cache_on_m2m, invalidate_hadis_cache |
||||
|
from apps.hadis.models import ( |
||||
|
BookReference, BookEdition, BookVolume, |
||||
|
HadisReference, HadisInterpretation, InterpretationReference, |
||||
|
HadisCorrection, CorrectionReference |
||||
|
) |
||||
|
from apps.hadis.models.transmitter import OriginalTextReference |
||||
|
|
||||
|
# Disconnect cache signals during bulk population for high performance |
||||
|
post_save.disconnect(clear_hadis_cache) |
||||
|
post_delete.disconnect(clear_hadis_cache) |
||||
|
m2m_changed.disconnect(clear_hadis_cache_on_m2m) |
||||
|
|
||||
|
SAMPLE_PUBLISHERS = [ |
||||
|
[ |
||||
|
{'text': 'دار الحديث', 'language_code': 'fa'}, |
||||
|
{'text': 'دار الحديث للطباعة والنشر', 'language_code': 'ar'}, |
||||
|
{'text': 'Dar al-Hadith Publications', 'language_code': 'en'} |
||||
|
], |
||||
|
[ |
||||
|
{'text': 'دار الكتب الإسلامية', 'language_code': 'fa'}, |
||||
|
{'text': 'دار الكتب الإسلامية', 'language_code': 'ar'}, |
||||
|
{'text': 'Dar al-Kutub al-Islamiyya', 'language_code': 'en'} |
||||
|
], |
||||
|
[ |
||||
|
{'text': 'مؤسسة النشر الإسلامي', 'language_code': 'fa'}, |
||||
|
{'text': 'مؤسسة النشر الإسلامي التابعة لجماعة المدرسين', 'language_code': 'ar'}, |
||||
|
{'text': 'Islamic Publishing Foundation', 'language_code': 'en'} |
||||
|
], |
||||
|
[ |
||||
|
{'text': 'مؤسسة البلاغ', 'language_code': 'fa'}, |
||||
|
{'text': 'مؤسسة البلاغ للطباعة والنشر والتوزيع', 'language_code': 'ar'}, |
||||
|
{'text': 'Al-Balagh Foundation', 'language_code': 'en'} |
||||
|
], |
||||
|
[ |
||||
|
{'text': 'دار الكتب العلمية', 'language_code': 'fa'}, |
||||
|
{'text': 'دار الكتب العلمية', 'language_code': 'ar'}, |
||||
|
{'text': 'Dar al-Kutub al-Ilmiyya', 'language_code': 'en'} |
||||
|
], |
||||
|
[ |
||||
|
{'text': 'مؤسسة آل البيت (ع) لإحياء التراث', 'language_code': 'fa'}, |
||||
|
{'text': 'مؤسسة آل البيت عليهم السلام لإحياء التراث', 'language_code': 'ar'}, |
||||
|
{'text': 'Aal al-Bayt Institute', 'language_code': 'en'} |
||||
|
], |
||||
|
] |
||||
|
|
||||
|
def is_empty_value(val): |
||||
|
if val is None: |
||||
|
return True |
||||
|
s = str(val).strip() |
||||
|
return s in ['', '0', '00', 'None', 'null', 'nan'] |
||||
|
|
||||
|
def ensure_book_editions_and_volumes(): |
||||
|
"""Ensure every BookReference has at least 1 BookEdition and 1 BookVolume.""" |
||||
|
all_books = list(BookReference.objects.prefetch_related('editions', 'volumes').all()) |
||||
|
print(f"Checking {len(all_books)} books for editions and volumes...", flush=True) |
||||
|
|
||||
|
editions_to_create = [] |
||||
|
books_needing_editions = [] |
||||
|
|
||||
|
for book in all_books: |
||||
|
editions = list(book.editions.all()) |
||||
|
if not editions: |
||||
|
pub = random.choice(SAMPLE_PUBLISHERS) |
||||
|
editions_to_create.append(BookEdition( |
||||
|
book_reference=book, |
||||
|
publisher=pub, |
||||
|
edition_number='طبعة أولى (1)', |
||||
|
year_of_publication='1441', |
||||
|
number_of_volumes=4 |
||||
|
)) |
||||
|
books_needing_editions.append(book) |
||||
|
|
||||
|
if editions_to_create: |
||||
|
created = BookEdition.objects.bulk_create(editions_to_create) |
||||
|
print(f"Created {len(created)} default editions.", flush=True) |
||||
|
else: |
||||
|
print("All books already have editions.", flush=True) |
||||
|
|
||||
|
# Re-fetch or assign volumes |
||||
|
all_books = list(BookReference.objects.prefetch_related('editions', 'volumes').all()) |
||||
|
volumes_to_create = [] |
||||
|
for book in all_books: |
||||
|
volumes = list(book.volumes.all()) |
||||
|
if not volumes: |
||||
|
editions = list(book.editions.all()) |
||||
|
ed = editions[0] if editions else None |
||||
|
for vol_num in range(1, 5): |
||||
|
volumes_to_create.append(BookVolume( |
||||
|
book_reference=book, |
||||
|
edition=ed, |
||||
|
title=f"جلد {vol_num}" |
||||
|
)) |
||||
|
|
||||
|
if volumes_to_create: |
||||
|
created_vols = BookVolume.objects.bulk_create(volumes_to_create) |
||||
|
print(f"Created {len(created_vols)} default volumes.", flush=True) |
||||
|
else: |
||||
|
print("All books already have volumes.", flush=True) |
||||
|
|
||||
|
def process_reference_queryset(model_class, model_name, all_books_map): |
||||
|
"""Update edition, book_volume, volume, pages, and hadith_number on all instances using bulk_update.""" |
||||
|
qs = model_class.objects.select_related('book_reference', 'edition', 'book_volume').all() |
||||
|
total = qs.count() |
||||
|
print(f"\nProcessing {total} records of {model_name}...", flush=True) |
||||
|
|
||||
|
if total == 0: |
||||
|
return |
||||
|
|
||||
|
updated_count = 0 |
||||
|
all_book_ids = list(all_books_map.keys()) |
||||
|
if not all_book_ids: |
||||
|
print("Error: No books available in database!", flush=True) |
||||
|
return |
||||
|
|
||||
|
items_to_update = [] |
||||
|
has_hadith_number = hasattr(model_class, 'hadith_number') |
||||
|
|
||||
|
update_fields = ['book_reference', 'edition', 'book_volume', 'volume', 'pages'] |
||||
|
if has_hadith_number: |
||||
|
update_fields.append('hadith_number') |
||||
|
|
||||
|
for ref in qs: |
||||
|
changed = False |
||||
|
|
||||
|
# 1. Ensure book_reference is attached |
||||
|
book = ref.book_reference |
||||
|
if not book: |
||||
|
chosen_book_id = random.choice(all_book_ids) |
||||
|
book = all_books_map[chosen_book_id]['book'] |
||||
|
ref.book_reference = book |
||||
|
changed = True |
||||
|
|
||||
|
book_data = all_books_map.get(book.id) |
||||
|
if not book_data: |
||||
|
continue |
||||
|
|
||||
|
book_editions = book_data['editions'] |
||||
|
book_volumes = book_data['volumes'] |
||||
|
|
||||
|
# 2. Ensure edition (Publisher) is set |
||||
|
if not ref.edition and book_editions: |
||||
|
ref.edition = random.choice(book_editions) |
||||
|
changed = True |
||||
|
|
||||
|
# 3. Ensure book_volume and volume text are set |
||||
|
if not ref.book_volume and book_volumes: |
||||
|
chosen_vol = random.choice(book_volumes) |
||||
|
ref.book_volume = chosen_vol |
||||
|
ref.volume = chosen_vol.title or f"جلد {random.randint(1, 4)}" |
||||
|
changed = True |
||||
|
elif not ref.volume: |
||||
|
ref.volume = f"جلد {random.randint(1, 4)}" |
||||
|
changed = True |
||||
|
|
||||
|
# 4. Ensure pages is set to a valid random page number |
||||
|
if is_empty_value(ref.pages): |
||||
|
ref.pages = str(random.randint(15, 480)) |
||||
|
changed = True |
||||
|
|
||||
|
# 5. Ensure hadith_number is set to a valid random hadith number |
||||
|
if has_hadith_number: |
||||
|
if is_empty_value(ref.hadith_number): |
||||
|
ref.hadith_number = str(random.randint(45, 3450)) |
||||
|
changed = True |
||||
|
|
||||
|
if changed: |
||||
|
items_to_update.append(ref) |
||||
|
|
||||
|
if len(items_to_update) >= 300: |
||||
|
model_class.objects.bulk_update(items_to_update, update_fields) |
||||
|
updated_count += len(items_to_update) |
||||
|
print(f" - Updated {updated_count}/{total} records...", flush=True) |
||||
|
items_to_update = [] |
||||
|
|
||||
|
if items_to_update: |
||||
|
model_class.objects.bulk_update(items_to_update, update_fields) |
||||
|
updated_count += len(items_to_update) |
||||
|
|
||||
|
print(f"Completed {model_name}: {updated_count} / {total} records updated.", flush=True) |
||||
|
|
||||
|
def main(): |
||||
|
print("==================================================", flush=True) |
||||
|
print("STARTING REFERENCE METADATA POPULATION", flush=True) |
||||
|
print("==================================================", flush=True) |
||||
|
|
||||
|
with transaction.atomic(): |
||||
|
# Step 1: Ensure all books have editions and volumes |
||||
|
ensure_book_editions_and_volumes() |
||||
|
|
||||
|
# Step 2: Cache book relations in memory for fast lookup |
||||
|
all_books_map = {} |
||||
|
for b in BookReference.objects.prefetch_related('editions', 'volumes').all(): |
||||
|
all_books_map[b.id] = { |
||||
|
'book': b, |
||||
|
'editions': list(b.editions.all()), |
||||
|
'volumes': list(b.volumes.all()), |
||||
|
} |
||||
|
|
||||
|
print(f"Cached {len(all_books_map)} books with their editions and volumes.", flush=True) |
||||
|
|
||||
|
# Step 3: Update HadisReference (Arguments) |
||||
|
process_reference_queryset(HadisReference, "HadisReference (Arguments)", all_books_map) |
||||
|
|
||||
|
# Step 4: Update InterpretationReference (Interpretations) |
||||
|
process_reference_queryset(InterpretationReference, "InterpretationReference (Interpretations)", all_books_map) |
||||
|
|
||||
|
# Step 5: Update OriginalTextReference (Original Texts) |
||||
|
process_reference_queryset(OriginalTextReference, "OriginalTextReference (Original Texts)", all_books_map) |
||||
|
|
||||
|
# Step 6: Update CorrectionReference (Corrections) |
||||
|
process_reference_queryset(CorrectionReference, "CorrectionReference (Corrections)", all_books_map) |
||||
|
|
||||
|
# Invalidate cache once at the end |
||||
|
print("\nInvalidating API cache...", flush=True) |
||||
|
try: |
||||
|
invalidate_hadis_cache() |
||||
|
print("API cache invalidated successfully.", flush=True) |
||||
|
except Exception as e: |
||||
|
print(f"Cache invalidation note: {e}", flush=True) |
||||
|
|
||||
|
print("\n==================================================", flush=True) |
||||
|
print("SUCCESS: ALL REFERENCES SUCCESSFULLY UPDATED!", flush=True) |
||||
|
print("==================================================", flush=True) |
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
main() |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue