Browse Source

dovodi data import

update models to be sync with dovodi data

built an script to run and store dovodi data in three phases
master
Mohsen Taba 2 weeks ago
parent
commit
1a56bc8a40
  1. 369
      apps/hadis/management/commands/import_legacy_data.py
  2. 181
      apps/hadis/migrations/0009_alter_hadistransmitter_options_and_more.py
  3. 27
      apps/hadis/migrations/0010_remove_hadiscorrection_description_and_more.py
  4. 111
      apps/hadis/models/hadis.py
  5. 28
      apps/hadis/models/reference.py
  6. 42
      apps/hadis/models/transmitter.py
  7. 6
      apps/hadis/serializers/hadis.py

369
apps/hadis/management/commands/import_legacy_data.py

@ -0,0 +1,369 @@
import os
import json
import csv
from django.core.management.base import BaseCommand
from django.core.files import File
from django.db import transaction
from django.conf import settings
# Import all necessary models
from apps.hadis.models import (
HadisCategory, HadisSect, HadisStatus, HadisTag, Hadis,
HadisCorrection, HadisReference, ReferenceImage, HadisTransmitter,
BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookSubjectArea,
Transmitters, NarratorLayer, TransmitterReliability, OpinionStatus,
TransmitterOpinion, TransmitterOriginalText, OriginalTextReference, OriginalTextReferenceImage,
HadisInterpretation, InterpretationReference, InterpretationReferenceImage, CorrectionReference, CorrectionReferenceImage
)
class Command(BaseCommand):
help = 'Import legacy Hadith data from JSON, CSV, and Media folders'
def add_arguments(self, parser):
parser.add_argument('base_dir', type=str, help='Absolute path to the "тестовая база данных" directory')
def wrap_lang(self, text, lang="ru"):
"""Helper to format strings into the [ {'language_code': lang, 'text': text} ] schema.
Always returns a valid dictionary to bypass Django's blank=False validators."""
if text is None:
text = ""
return [{"language_code": lang, "text": str(text).strip()}]
@transaction.atomic
def handle(self, *args, **kwargs):
base_dir = kwargs['base_dir']
if not os.path.exists(base_dir):
self.stderr.write(self.style.ERROR(f'Directory not found: {base_dir}'))
return
self.stdout.write(self.style.SUCCESS(f'Starting import from: {base_dir}'))
# Paths
aut_ui_path = os.path.join(base_dir, 'AUT_UI.csv')
bib_path = os.path.join(base_dir, 'bib.csv')
narrators_path = os.path.join(base_dir, 'narrators.json')
tathir_path = os.path.join(base_dir, 'tathir.json')
# --- PHASE 1: SCHOLARS & BOOKS ---
self.stdout.write(self.style.WARNING('\n--- PHASE 1: Loading Scholars & Books ---'))
scholars_map = {}
if os.path.exists(aut_ui_path):
with open(aut_ui_path, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
if len(row) >= 3:
scholars_map[row[0].strip()] = {
"ar": row[1].strip(),
"ru": row[2].strip()
}
self.stdout.write(f'Loaded {len(scholars_map)} scholars into memory.')
if os.path.exists(bib_path):
with open(bib_path, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
if len(row) < 5: continue
base_legacy_id = row[0].strip()
author_name = row[2].strip()
base_title = row[3].strip()
# Extract total volumes (Column 11 / Index 10)
vol_str = row[10].strip() if len(row) > 10 else ''
try:
total_vols = int(vol_str) if vol_str.isdigit() else 1
except ValueError:
total_vols = 1
# Create a BookReference for EVERY volume
for v in range(1, total_vols + 1):
# Generate unique ID and Title for multi-volume books
is_multi_vol = total_vols > 1
legacy_id = f"{base_legacy_id}-v{v}" if is_multi_vol else base_legacy_id
title_text = f"{base_title} (Vol {v})" if is_multi_vol else base_title
book, _ = BookReference.objects.update_or_create(
legacy_id=legacy_id,
defaults={
'title': self.wrap_lang(title_text),
'number_of_volumes': total_vols,
'volume': str(v),
'year_of_publication': row[9].strip() if len(row) > 9 else '',
'source_url': row[11].strip() if len(row) > 11 else '',
'description': self.wrap_lang(row[12].strip() if len(row) > 12 else ''),
'publisher': self.wrap_lang(row[5].strip() if len(row) > 5 else ''),
'language': self.wrap_lang('')
}
)
# Author
if author_name:
author, _ = BookAuthor.objects.get_or_create(name=self.wrap_lang(author_name))
book.authors.add(author)
# Scan Book Folder for Specific Volume Images and PDFs
book_folder = os.path.join(base_dir, 'books', base_legacy_id)
if os.path.exists(book_folder):
vol_num_str = str(v)
vol_padded_str = str(v).zfill(2) # "1" -> "01"
for root, _, files in os.walk(book_folder):
folder_name = os.path.basename(root)
for file in files:
file_path = os.path.join(root, file)
file_lower = file.lower()
# Attach PDF if it matches "1.pdf" or "01.pdf"
if file_lower.endswith('.pdf'):
if file_lower in [f"{vol_num_str}.pdf", f"{vol_padded_str}.pdf"] or not is_multi_vol:
with open(file_path, 'rb') as doc_f:
doc = BookReferenceDocument(book_reference=book, volume=vol_num_str, title=file)
doc.file.save(file, File(doc_f), save=True)
# Attach Images if they are in folder "1" or "01"
elif file_lower.endswith(('.png', '.jpg', '.jpeg', '.gif')):
if folder_name in [vol_num_str, vol_padded_str] or not is_multi_vol:
with open(file_path, 'rb') as img_f:
img = BookReferenceImage(book_reference=book, volume=vol_num_str)
img.image.save(file, File(img_f), save=True)
self.stdout.write(self.style.SUCCESS('Books (split by volumes) loaded successfully.'))
# --- PHASE 2: NARRATORS ---
self.stdout.write(self.style.WARNING('\n--- PHASE 2: Loading Narrators ---'))
if os.path.exists(narrators_path):
with open(narrators_path, 'r', encoding='utf-8') as f:
n_data_list = json.load(f).get('narrators', [])
for n_data in n_data_list:
legacy_id = n_data.get('id')
legacy_number = int(n_data.get('narrator_number')) if str(n_data.get('narrator_number')).isdigit() else None
info = n_data.get('info', {})
ar_info = info.get('arabic', {})
reliability, _ = TransmitterReliability.objects.get_or_create(
title=self.wrap_lang(n_data.get('reliability', 'Unknown'))
)
generation = int(n_data.get('generation')) if str(n_data.get('generation')).isdigit() else None
if generation:
NarratorLayer.objects.get_or_create(
number=generation,
defaults={
'name': self.wrap_lang(f'Layer {generation}'),
'description': self.wrap_lang('')
}
)
# Create Transmitter
transmitter, _ = Transmitters.objects.update_or_create(
legacy_id=legacy_id,
defaults={
'legacy_number': legacy_number,
'full_name': self.wrap_lang(info.get('name', ''), 'ru') + self.wrap_lang(ar_info.get('name', ''), 'ar'),
'known_as': self.wrap_lang(info.get('known_name', ''), 'ru') + self.wrap_lang(ar_info.get('known_name', ''), 'ar'),
'kunya': self.wrap_lang(info.get('kunya', ''), 'ru') + self.wrap_lang(ar_info.get('kunya', ''), 'ar'),
'nickname': self.wrap_lang(info.get('nickname', ''), 'ru') + self.wrap_lang(ar_info.get('nickname', ''), 'ar'),
'origin': self.wrap_lang(info.get('origin', ''), 'ru') + self.wrap_lang(ar_info.get('origin', ''), 'ar'),
'lived_in': self.wrap_lang(info.get('city_of_residence', ''), 'ru') + self.wrap_lang(ar_info.get('city_of_residence', ''), 'ar'),
'died_in': self.wrap_lang(info.get('city_of_death', ''), 'ru') + self.wrap_lang(ar_info.get('city_of_death', ''), 'ar'),
'description': self.wrap_lang(''),
'generation': generation,
'reliability': reliability,
'in_sahih_bukhari': n_data.get('transmitted_to_bukhari', False),
'in_sahih_muslim': n_data.get('transmitted_to_muslim', False),
'relatives_raw': info.get('relatives', {})
}
)
# Opinions
for op in n_data.get('strengthened_weakened', {}).get('review', []):
author_ui = op.get('author_ui')
scholar_data = scholars_map.get(author_ui, {"ar": author_ui, "ru": author_ui})
TransmitterOpinion.objects.get_or_create(
transmitter=transmitter,
opinion_text=self.wrap_lang(op.get('quote_original', ''), 'ar') + self.wrap_lang(op.get('quote_translated', ''), 'ru'),
scholar_name=self.wrap_lang(scholar_data['ar'], 'ar') + self.wrap_lang(scholar_data['ru'], 'ru')
)
# Original Texts
for text_data in n_data.get('excerpts', []):
orig_text, _ = TransmitterOriginalText.objects.get_or_create(
transmitter=transmitter,
title=self.wrap_lang(text_data.get('title')),
text=self.wrap_lang(text_data.get('text'), 'ar'),
translation=self.wrap_lang(text_data.get('translation'), 'ru')
)
for ed in text_data.get('editions', []):
book_ref = self._get_book_volume(ed.get('book_id'), ed.get('volume'))
ref_obj, _ = OriginalTextReference.objects.get_or_create(
original_text=orig_text, book_reference=book_ref,
volume=ed.get('volume'), page=ed.get('page'), url=ed.get('url')
)
folder = ed.get('screenshots_folder')
if folder:
self._attach_images(os.path.join(base_dir, 'screens_trx', legacy_id, folder), OriginalTextReferenceImage, ref_obj)
self.stdout.write(self.style.SUCCESS('Narrators loaded successfully.'))
# --- PHASE 3: HADITHS (Arguments, Corrections, Interpretations) ---
self.stdout.write(self.style.WARNING('\n--- PHASE 3: Loading Hadiths ---'))
default_sect, _ = HadisSect.objects.get_or_create(
sect_type='sunni',
defaults={
'title': self.wrap_lang('Sunni'),
'description': self.wrap_lang('')
}
)
if os.path.exists(tathir_path):
with open(tathir_path, 'r', encoding='utf-8') as f:
materials = json.load(f).get('materials', [])
# Map corrections to their parent hadiths
correction_to_hadith_map = {}
for item in materials:
if item.get('type') == 'arguments':
for conf_id in item.get('confirmation', []):
correction_to_hadith_map[conf_id] = item.get('id')
for item in materials:
i_type = item.get('type')
# A: BASE HADITHS
if i_type == 'arguments':
cat_str = item.get('category', [''])[0]
category, _ = HadisCategory.objects.get_or_create(
title=self.wrap_lang(cat_str),
defaults={
'sect': default_sect,
'source_type': item.get('subtype', 'hadith') or 'hadith',
'description': self.wrap_lang('')
}
)
status, _ = HadisStatus.objects.get_or_create(
title=self.wrap_lang(item.get('authenticity', '')),
defaults={'description': self.wrap_lang('')}
)
hadis, _ = Hadis.objects.update_or_create(
legacy_id=item.get('id'),
defaults={
'category': category,
'hadis_status': status,
'title': self.wrap_lang(item.get('aliases', [''])[0] if item.get('aliases') else ''),
'title_narrator': self.wrap_lang(item.get('aliases', [''])[0] if item.get('aliases') else ''),
'description': self.wrap_lang(''),
'explanation': self.wrap_lang(''),
'address': self.wrap_lang(''),
'hadis_status_text': self.wrap_lang(''),
'text': item.get('original_text', ''),
'translation': self.wrap_lang(item.get('translation', ''), 'ru')
}
)
raw_chain = item.get('chain', [])
chain_arrays = []
if raw_chain:
# Normalize: If it's a flat list of ints, wrap it in a list so it's a 2D array
if isinstance(raw_chain[0], int):
chain_arrays = [raw_chain]
else:
chain_arrays = raw_chain
for chain_idx, narrator_ids in enumerate(chain_arrays):
for order_idx, n_id in enumerate(narrator_ids):
transmitter = Transmitters.objects.filter(legacy_number=n_id).first()
if transmitter:
layer = NarratorLayer.objects.filter(number=transmitter.generation).first()
HadisTransmitter.objects.get_or_create(
hadis=hadis, transmitter=transmitter, chain_index=chain_idx, order=order_idx,
defaults={'narrator_layer': layer, 'status': transmitter.reliability}
)
# Editions & Images
for ed in item.get('editions', []):
book = self._get_book_volume(ed.get('book_id'), ed.get('volume'))
href, _ = HadisReference.objects.get_or_create(
hadis=hadis, book_reference=book,
defaults={'hadith_number': str(ed.get('hadith_number', '')), 'description': self.wrap_lang('')}
)
if ed.get('screenshots_folder'):
self._attach_images(os.path.join(base_dir, 'screens', item.get('id'), ed.get('screenshots_folder')), ReferenceImage, href , field_name='thumbnail')
# B: CORRECTIONS
elif i_type == 'authenticity_analysis':
parent_id = correction_to_hadith_map.get(item.get('id'))
parent_hadith = Hadis.objects.filter(legacy_id=parent_id).first()
if parent_hadith:
corr, _ = HadisCorrection.objects.get_or_create(
hadis=parent_hadith, legacy_id=item.get('id'),
defaults={
'title': self.wrap_lang(''),
'text': item.get('original_text', ''), # Directly mapped to TextField
'translation': self.wrap_lang(item.get('translation', ''), 'ru')
}
)
for ed in item.get('editions', []):
book = self._get_book_volume(ed.get('book_id'), ed.get('volume'))
cref, _ = CorrectionReference.objects.get_or_create(correction=corr, book_reference=book, defaults={'hadith_number': str(ed.get('hadith_number', ''))})
if ed.get('screenshots_folder'):
self._attach_images(os.path.join(base_dir, 'screens', item.get('id'), ed.get('screenshots_folder')), CorrectionReferenceImage, cref)
# C: INTERPRETATIONS
elif i_type == 'interpretation':
cat_str = item.get('category', [''])[0] if item.get('category') else ''
category = HadisCategory.objects.filter(title__contains=[{'text': cat_str}]).first()
if category:
interp, _ = HadisInterpretation.objects.get_or_create(
category=category, legacy_id=item.get('id'),
defaults={
'title': self.wrap_lang(''),
'text': item.get('original_text', ''),
'translation': self.wrap_lang(item.get('translation', ''), 'ru')
}
)
for ed in item.get('editions', []):
book = self._get_book_volume(ed.get('book_id'), ed.get('volume'))
iref, _ = InterpretationReference.objects.get_or_create(interpretation=interp, book_reference=book, defaults={'hadith_number': str(ed.get('hadith_number', ''))})
if ed.get('screenshots_folder'):
self._attach_images(os.path.join(base_dir, 'screens', item.get('id'), ed.get('screenshots_folder')), InterpretationReferenceImage, iref)
self.stdout.write(self.style.SUCCESS('\nAll Hadiths, Corrections, and Interpretations Imported Successfully!'))
def _get_book_volume(self, book_id, volume_str):
"""Finds the specific volume of a book, with fallbacks."""
if not book_id: return None
# 1. Try to find specific volume (e.g., uuid-v2)
if volume_str:
vol_clean = ''.join(filter(str.isdigit, str(volume_str))) # extracts "2" from "Vol 2"
if vol_clean:
book = BookReference.objects.filter(legacy_id=f"{book_id}-v{vol_clean}").first()
if book: return book
# 2. Fallback: Find the base book (single volume) or the first volume available
return BookReference.objects.filter(legacy_id__startswith=book_id).first()
def _attach_images(self, folder_path, ImageModelClass, reference_instance, field_name='image'):
"""Helper to safely scan a folder and attach images to a specific reference instance."""
if os.path.exists(folder_path):
for i, filename in enumerate(sorted(os.listdir(folder_path))):
if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif')):
file_path = os.path.join(folder_path, filename)
with open(file_path, 'rb') as f:
img_obj = ImageModelClass(reference=reference_instance, priority=i)
# Dynamically grab the correct field ('image' or 'thumbnail')
image_field = getattr(img_obj, field_name)
image_field.save(filename, File(f), save=True)

181
apps/hadis/migrations/0009_alter_hadistransmitter_options_and_more.py

@ -0,0 +1,181 @@
# Generated by Django 5.2.12 on 2026-05-09 10:55
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hadis', '0008_hadiscollection_pin_top'),
]
operations = [
migrations.AlterModelOptions(
name='hadistransmitter',
options={'ordering': ('hadis', 'chain_index', 'order'), 'verbose_name': 'Hadis Transmitter', 'verbose_name_plural': 'Hadis Transmitters'},
),
migrations.AlterUniqueTogether(
name='hadistransmitter',
unique_together=set(),
),
migrations.AddField(
model_name='bookreference',
name='legacy_id',
field=models.CharField(blank=True, db_index=True, max_length=255, null=True, unique=True),
),
migrations.AddField(
model_name='bookreference',
name='source_url',
field=models.URLField(blank=True, max_length=1000, null=True, verbose_name='Source URL'),
),
migrations.AddField(
model_name='bookreferenceimage',
name='volume',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Volume'),
),
migrations.AddField(
model_name='hadis',
name='legacy_id',
field=models.CharField(blank=True, db_index=True, max_length=255, null=True, unique=True),
),
migrations.AddField(
model_name='hadiscorrection',
name='legacy_id',
field=models.CharField(blank=True, db_index=True, max_length=255, null=True, unique=True),
),
migrations.AddField(
model_name='hadisreference',
name='hadith_number',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Hadith Number in Book'),
),
migrations.AddField(
model_name='hadistransmitter',
name='chain_index',
field=models.PositiveIntegerField(default=0, help_text='Which chain/branch this belongs to (0, 1, 2...)', verbose_name='Chain Index'),
),
migrations.AddField(
model_name='transmitters',
name='father',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children_as_father', to='hadis.transmitters', verbose_name='Father'),
),
migrations.AddField(
model_name='transmitters',
name='legacy_id',
field=models.CharField(blank=True, db_index=True, max_length=255, null=True, unique=True),
),
migrations.AddField(
model_name='transmitters',
name='legacy_number',
field=models.PositiveIntegerField(blank=True, db_index=True, null=True, unique=True),
),
migrations.AddField(
model_name='transmitters',
name='mother',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children_as_mother', to='hadis.transmitters', verbose_name='Mother'),
),
migrations.AddField(
model_name='transmitters',
name='relatives_raw',
field=models.JSONField(blank=True, default=dict, verbose_name='Raw Relatives Data'),
),
migrations.AddField(
model_name='transmitters',
name='siblings',
field=models.ManyToManyField(blank=True, to='hadis.transmitters', verbose_name='Siblings'),
),
migrations.AlterUniqueTogether(
name='hadistransmitter',
unique_together={('hadis', 'chain_index', 'order')},
),
migrations.CreateModel(
name='BookReferenceDocument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to='hadis/book_reference_documents/', verbose_name='Document File')),
('volume', models.CharField(blank=True, max_length=50, null=True, verbose_name='Volume')),
('title', models.CharField(blank=True, help_text='e.g., Volume 1, 01.pdf', max_length=255, null=True)),
('order', models.PositiveIntegerField(default=0, verbose_name='order')),
('created_at', models.DateTimeField(auto_now_add=True)),
('book_reference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='hadis.bookreference', verbose_name='book reference')),
],
options={
'verbose_name': 'Book Reference Document',
'verbose_name_plural': 'Book Reference Documents',
'ordering': ['order', '-created_at'],
},
),
migrations.CreateModel(
name='CorrectionReference',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('hadith_number', models.CharField(blank=True, max_length=100, null=True)),
('book_reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hadis.bookreference')),
('correction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='references', to='hadis.hadiscorrection')),
],
),
migrations.CreateModel(
name='CorrectionReferenceImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='hadis/correction_images/')),
('priority', models.IntegerField(default=0)),
('reference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='hadis.correctionreference')),
],
),
migrations.CreateModel(
name='HadisInterpretation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('legacy_id', models.CharField(blank=True, db_index=True, max_length=255, null=True, unique=True)),
('title', models.JSONField(default=list, verbose_name='Title')),
('text', models.TextField(verbose_name='Original Text')),
('translation', models.JSONField(default=list, verbose_name='Translation')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interpretations', to='hadis.hadiscategory', verbose_name='Category')),
],
options={
'verbose_name': 'Hadis Interpretation',
'verbose_name_plural': 'Hadis Interpretations',
},
),
migrations.CreateModel(
name='InterpretationReference',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('hadith_number', models.CharField(blank=True, max_length=100, null=True)),
('book_reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hadis.bookreference')),
('interpretation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='references', to='hadis.hadisinterpretation')),
],
),
migrations.CreateModel(
name='InterpretationReferenceImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='hadis/interpretation_images/')),
('priority', models.IntegerField(default=0)),
('reference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='hadis.interpretationreference')),
],
),
migrations.CreateModel(
name='OriginalTextReference',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('volume', models.CharField(blank=True, max_length=100, null=True)),
('page', models.CharField(blank=True, max_length=100, null=True)),
('url', models.URLField(blank=True, max_length=500, null=True)),
('book_reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hadis.bookreference')),
('original_text', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='references', to='hadis.transmitteroriginaltext')),
],
),
migrations.CreateModel(
name='OriginalTextReferenceImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='hadis/original_text_images/')),
('priority', models.IntegerField(default=0)),
('reference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='hadis.originaltextreference')),
],
),
]

27
apps/hadis/migrations/0010_remove_hadiscorrection_description_and_more.py

@ -0,0 +1,27 @@
# Generated by Django 5.2.12 on 2026-05-09 13:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hadis', '0009_alter_hadistransmitter_options_and_more'),
]
operations = [
migrations.RemoveField(
model_name='hadiscorrection',
name='description',
),
migrations.AddField(
model_name='bookreference',
name='number_of_volumes',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Total Number of Volumes'),
),
migrations.AddField(
model_name='hadiscorrection',
name='text',
field=models.TextField(blank=True, default='', verbose_name='Original Text'),
),
]

111
apps/hadis/models/hadis.py

@ -262,6 +262,7 @@ class HadisStatus(ColorPaletteMixin,models.Model):
class Hadis(models.Model):
category = models.ForeignKey("hadis.HadisCategory", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('category'))
number = models.PositiveIntegerField(verbose_name=_('number'), default=1)
legacy_id = models.CharField(max_length=255, unique=True, null=True, blank=True, db_index=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True)
title_narrator = models.JSONField(default = list , verbose_name=_('Title Narrator'))
title = models.JSONField(default = list , verbose_name=_('Title'))
@ -420,6 +421,7 @@ class HadisReference(models.Model):
verbose_name=_('book reference'),
related_name='hadis_references'
)
hadith_number = models.CharField(max_length=100, null=True, blank=True, verbose_name=_('Hadith Number in Book'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
@ -488,7 +490,8 @@ class HadisCorrection(models.Model):
hadis = models.ForeignKey(Hadis, verbose_name=_("hadis correction"), on_delete=models.CASCADE)
title = models.JSONField(default = list , verbose_name=_('Title'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True)
description =models.JSONField(default = list , verbose_name=_('Description'))
legacy_id = models.CharField(max_length=255, unique=True, null=True, blank=True, db_index=True)
text = models.TextField(verbose_name=_('Original Text'), default="", blank=True)
translation = models.JSONField(verbose_name=_("translation"), default=list)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("updated at"))
@ -505,100 +508,94 @@ class HadisCorrection(models.Model):
return f"{self.hadis.number} - {title}"
def save(self, *args, **kwargs):
"""
Override save to automatically generate smart slugs.
"""
# Generate slug if not already set
# Generate smart slug
if not self.slug and self.title:
# Extract title text
title_text = None
if isinstance(self.title, list) and self.title:
if isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0]
if isinstance(first_item, dict):
title_text = first_item.get("text")
# Generate smart slug
if title_text:
self.slug = generate_smart_slug(
text=title_text,
model_class=HadisCorrection,
max_length=100, # ← Adjust max length here
keep_words=8, # ← Limit to 8 words (your requirement)
instance=self,
text=title_text, model_class=HadisCorrection,
max_length=100, keep_words=8, instance=self,
)
else:
# Fallback if title is empty - use timestamp for uniqueness
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"correction-{self.hadis.slug if self.hadis and self.hadis.slug else 'unknown'}-{suffix}"
# Ensure uniqueness
counter = 1
while HadisCorrection.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"correction-{self.hadis.slug if self.hadis and self.hadis.slug else 'unknown'}-{suffix}-{counter}"
counter += 1
self.slug = base_slug
# Generate/update share_link before saving
if self.slug and self.hadis and self.hadis.slug:
self.share_link = f"{settings.DOVODI_DOMAIN}/arguments/hadith/{self.hadis.slug}/corrections/{self.slug}"
# Reset embedded_in if text or translation changes
# Track 'text' instead of 'description'
if self.pk:
old_instance = HadisCorrection.objects.get(pk=self.pk)
if (old_instance.description != self.description or
if (old_instance.text != self.text or
old_instance.translation != self.translation):
self.embedded_in = [] # Reset!
super().save(*args, **kwargs)
def get_title(self, lang):
"""
Get title for a specific language
"""
if not self.title or not isinstance(self.title, list):
return None
if not self.title or not isinstance(self.title, list): return None
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
if isinstance(tr, dict) and tr.get('language_code') == lang: return tr.get('text', '')
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '')
return None
def get_description(self,lang):
"""
Get title for a specific language
"""
if not self.description or not isinstance(self.description, list):
def get_translation(self, lang):
if not self.translation or not isinstance(self.translation, list): return None
for tr in self.translation:
if isinstance(tr, dict) and tr.get('language_code') == lang: return tr.get('text', '')
for tr in self.translation:
if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '')
return None
for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
for tr in self.description:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return None
# apps/hadis/models/hadis.py (or a new interpretation.py file)
def get_translation(self,lang):
"""
Get title for a specific language
"""
class HadisInterpretation(models.Model):
category = models.ForeignKey('HadisCategory', on_delete=models.CASCADE, related_name='interpretations', verbose_name=_('Category'))
legacy_id = models.CharField(max_length=255, unique=True, null=True, blank=True, db_index=True)
if not self.translation or not isinstance(self.translation, list):
return None
title = models.JSONField(default=list, verbose_name=_('Title'))
text = models.TextField(verbose_name=_('Original Text'))
translation = models.JSONField(verbose_name=_('Translation'), default=list)
for tr in self.translation:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
for tr in self.translation:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return None
class Meta:
verbose_name = _("Hadis Interpretation")
verbose_name_plural = _("Hadis Interpretations")
# --- FOR CORRECTIONS ---
class CorrectionReference(models.Model):
correction = models.ForeignKey(HadisCorrection, on_delete=models.CASCADE, related_name='references')
book_reference = models.ForeignKey(BookReference, on_delete=models.SET_NULL, null=True, blank=True)
hadith_number = models.CharField(max_length=100, null=True, blank=True)
class CorrectionReferenceImage(models.Model):
reference = models.ForeignKey(CorrectionReference, related_name='images', on_delete=models.CASCADE)
image = models.ImageField(upload_to='hadis/correction_images/')
priority = models.IntegerField(default=0)
# --- FOR INTERPRETATIONS ---
class InterpretationReference(models.Model):
interpretation = models.ForeignKey(HadisInterpretation, on_delete=models.CASCADE, related_name='references')
book_reference = models.ForeignKey(BookReference, on_delete=models.SET_NULL, null=True, blank=True)
hadith_number = models.CharField(max_length=100, null=True, blank=True)
class InterpretationReferenceImage(models.Model):
reference = models.ForeignKey(InterpretationReference, related_name='images', on_delete=models.CASCADE)
image = models.ImageField(upload_to='hadis/interpretation_images/')
priority = models.IntegerField(default=0)

28
apps/hadis/models/reference.py

@ -60,11 +60,14 @@ class BookReference(models.Model):
Model for hadis book references with detailed information
This is different from library books - these are reference books for hadis
"""
legacy_id = models.CharField(max_length=255, unique=True, null=True, blank=True, db_index=True)
source_url = models.URLField(max_length=1000, null=True, blank=True, verbose_name=_('Source URL'))
title = models.JSONField(default = list , verbose_name=_('Title'))
description = models.JSONField(default = list , verbose_name=_('Description'))
language = models.JSONField(default = list , verbose_name=_('Language'))
isbn = models.CharField(max_length=100, verbose_name=_('ISBN'), blank=True, null=True)
volume = models.CharField(max_length=100, verbose_name=_('volume'), blank=True, null=True)
number_of_volumes = models.PositiveIntegerField(verbose_name=_('Total Number of Volumes'), null=True, blank=True)
year_of_publication = models.CharField(max_length=50, verbose_name=_('year of publication'), blank=True, null=True)
number_page = models.PositiveIntegerField(verbose_name=_('number of pages'), blank=True, null=True)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True)
@ -211,6 +214,7 @@ class BookReferenceImage(models.Model):
verbose_name=_('book reference')
)
image = models.ImageField(upload_to='hadis/book_reference_images/', verbose_name=_('image'))
volume = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('Volume'))
order = models.PositiveIntegerField(default=0, verbose_name=_('order'))
description = models.JSONField(default = list , verbose_name=_('Description'))
@ -241,6 +245,30 @@ class BookReferenceImage(models.Model):
def __str__(self):
return f"{self.book_reference} - Image {self.order}"
class BookReferenceDocument(models.Model):
"""
Model for handling multiple PDF files per book (e.g., different volumes).
"""
book_reference = models.ForeignKey(
BookReference,
on_delete=models.CASCADE,
related_name='documents',
verbose_name=_('book reference')
)
file = models.FileField(upload_to='hadis/book_reference_documents/', verbose_name=_('Document File'))
volume = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('Volume'))
title = models.CharField(max_length=255, blank=True, null=True, help_text=_('e.g., Volume 1, 01.pdf'))
order = models.PositiveIntegerField(default=0, verbose_name=_('order'))
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = _('Book Reference Document')
verbose_name_plural = _('Book Reference Documents')
ordering = ['order', '-created_at']
def __str__(self):
return f"{self.book_reference} - {self.title or 'Doc ' + str(self.id)}"
class BookAuthor(models.Model):
"""

42
apps/hadis/models/transmitter.py

@ -235,6 +235,13 @@ class Transmitters(models.Model):
known_as = models.JSONField(default = list , verbose_name=_('Known as'))
nickname = models.JSONField(default = list , verbose_name=_('Nick Name'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=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)
relatives_raw = models.JSONField(default=dict, blank=True, verbose_name=_('Raw Relatives Data'))
father = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children_as_father', verbose_name=_('Father'))
mother = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children_as_mother', verbose_name=_('Mother'))
# ManyToMany for siblings allows bi-directional sibling relationships
siblings = models.ManyToManyField('self', blank=True, verbose_name=_('Siblings'))
# Geographic Information
origin = models.JSONField(default = list , verbose_name=_('Origin'))
@ -423,6 +430,13 @@ class HadisTransmitter(models.Model):
verbose_name=_('transmitter'),
related_name='hadises'
)
chain_index = models.PositiveIntegerField(
default=0,
verbose_name=_('Chain Index'),
help_text=_('Which chain/branch this belongs to (0, 1, 2...)')
)
narrator_layer = models.ForeignKey(
NarratorLayer,
on_delete=models.SET_NULL,
@ -457,12 +471,14 @@ class HadisTransmitter(models.Model):
]
verbose_name = _('Hadis Transmitter')
verbose_name_plural = _('Hadis Transmitters')
ordering = ('hadis', 'order')
unique_together = ('hadis', 'transmitter', 'order')
ordering = ('hadis', 'chain_index', 'order') # Update ordering
# 2. UPDATE UNIQUE TOGETHER
# A specific position in a specific chain of a specific hadith can only have one narrator
unique_together = ('hadis', 'chain_index', 'order')
def __str__(self):
layer_info = f" - {self.narrator_layer}" if self.narrator_layer else ""
return f'{self.hadis.number} - {self.transmitter} ({self.order}){layer_info}'
return f'{self.hadis.number} - Chain {self.chain_index} - {self.transmitter} (Pos: {self.order})'
class OpinionStatus(ColorPaletteMixin, models.Model):
@ -737,3 +753,21 @@ class TransmitterOriginalText(models.Model):
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return None
from .reference import BookReference
class OriginalTextReference(models.Model):
"""Links an Original Text (Excerpt) to a specific Book Edition"""
original_text = models.ForeignKey(
TransmitterOriginalText,
on_delete=models.CASCADE,
related_name='references'
)
book_reference = models.ForeignKey(BookReference, on_delete=models.SET_NULL, null=True, blank=True)
volume = models.CharField(max_length=100, null=True, blank=True)
page = models.CharField(max_length=100, null=True, blank=True)
url = models.URLField(max_length=500, null=True, blank=True)
class OriginalTextReferenceImage(models.Model):
"""Stores the screenshots for a specific Original Text Book Reference"""
reference = models.ForeignKey(OriginalTextReference, related_name='images', on_delete=models.CASCADE)
image = models.ImageField(upload_to='hadis/original_text_images/')
priority = models.IntegerField(default=0)

6
apps/hadis/serializers/hadis.py

@ -205,7 +205,7 @@ class HadisSyncSerializer(serializers.ModelSerializer):
corrections_data.append({
'id': correction.id,
'title': get_localized_text(correction.title, request),
'description': get_localized_text(correction.description, request),
'text': correction.text,
'translation': get_localized_text(correction.translation, request),
'share_link': correction.share_link,
})
@ -706,12 +706,12 @@ class HadisReferenceSerializer(serializers.ModelSerializer):
class HadisCorrectionSerializer(serializers.ModelSerializer):
"""Serializer for HadisCorrection"""
title = LocalizedField()
description = LocalizedField()
text = LocalizedField()
translation = LocalizedField()
bookmark = serializers.SerializerMethodField()
class Meta:
model = HadisCorrection
fields = ['id', 'title','slug', 'description', 'translation','share_link','bookmark']
fields = ['id', 'title','slug', 'text', 'translation','share_link','bookmark']
def get_bookmark(self, obj):
"""Get bookmark information for this correction."""

Loading…
Cancel
Save