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