Browse Source

data script enhancement and deta schema update

book creation is now only creates if a edition or folder mentioned, not blindly all the volumes !

script deletes the old books

some models updated with new values
master
Mohsen Taba 2 weeks ago
parent
commit
f790b3252e
  1. 1
      apps/course/admin/course.py
  2. 251
      apps/hadis/management/commands/import_legacy_data.py
  3. 58
      apps/hadis/migrations/0011_bookreference_city_of_publication_and_more.py
  4. 68
      apps/hadis/migrations/0012_correctionreferenceimage_pages_and_more.py
  5. 40
      apps/hadis/migrations/0013_remove_correctionreferenceimage_pages_and_more.py
  6. 9
      apps/hadis/models/hadis.py
  7. 5
      apps/hadis/models/reference.py
  8. 9
      apps/hadis/models/transmitter.py
  9. 8
      apps/hadis/serializers/hadis.py

1
apps/course/admin/course.py

@ -1,3 +1,4 @@
from utils.admin import admin_url_generator
import os
import hashlib

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

@ -1,12 +1,12 @@
import os
import json
import csv
import re
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,
@ -23,16 +23,12 @@ class Command(BaseCommand):
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 = ""
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
@ -45,6 +41,28 @@ class Command(BaseCommand):
narrators_path = os.path.join(base_dir, 'narrators.json')
tathir_path = os.path.join(base_dir, 'tathir.json')
# --- PRE-SCAN TATHIR.JSON FOR CITED VOLUMES ---
cited_book_volumes = {}
if os.path.exists(tathir_path):
with open(tathir_path, 'r', encoding='utf-8') as f:
t_data = json.load(f).get('materials', [])
for item in t_data:
for ed in item.get('editions', []):
b_id = ed.get('book_id')
b_vol = str(ed.get('volume')).strip() if ed.get('volume') is not None else ''
if b_vol.lower() == 'none': b_vol = ''
if b_id:
if b_id not in cited_book_volumes:
cited_book_volumes[b_id] = set()
if b_vol:
try: cited_book_volumes[b_id].add(str(int(b_vol)))
except ValueError: cited_book_volumes[b_id].add(b_vol)
# --- PRE-FLIGHT CLEANUP ---
self.stdout.write(self.style.WARNING('\n--- PRE-FLIGHT: Cleaning up old legacy books ---'))
BookReference.objects.exclude(legacy_id__isnull=True).exclude(legacy_id__exact='').delete()
# --- PHASE 1: SCHOLARS & BOOKS ---
self.stdout.write(self.style.WARNING('\n--- PHASE 1: Loading Scholars & Books ---'))
@ -54,11 +72,7 @@ class Command(BaseCommand):
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.')
scholars_map[row[0].strip()] = {"ar": row[1].strip(), "ru": row[2].strip()}
if os.path.exists(bib_path):
with open(bib_path, 'r', encoding='utf-8') as f:
@ -70,67 +84,83 @@ class Command(BaseCommand):
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
try: total_vols_int = int(vol_str) if vol_str.isdigit() else 1
except ValueError: total_vols_int = 1
existing_vols = set()
book_folder = os.path.join(base_dir, 'books', base_legacy_id)
if os.path.exists(book_folder):
for item in os.listdir(book_folder):
if os.path.isdir(os.path.join(book_folder, item)):
try: existing_vols.add(str(int(item)))
except ValueError: existing_vols.add(item)
volumes_to_create = existing_vols.union(cited_book_volumes.get(base_legacy_id, set()))
if not volumes_to_create: volumes_to_create = {''}
for v in volumes_to_create:
legacy_id = f"{base_legacy_id}-v{v}" if v else base_legacy_id
title_text = f"{base_title} (Vol {v})" if v 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),
'order': int(row[1]) if len(row) > 1 and row[1].isdigit() else 0,
'researcher': self.wrap_lang(row[4].strip() if len(row) > 4 else ''),
'publisher': self.wrap_lang(row[5].strip() if len(row) > 5 else ''),
'city_of_publication': self.wrap_lang(row[6].strip() if len(row) > 6 else ''),
'country_of_publication': self.wrap_lang(row[7].strip() if len(row) > 7 else ''),
'edition_number': row[8].strip() if len(row) > 8 else '',
'year_of_publication': row[9].strip() if len(row) > 9 else '',
'number_of_volumes': total_vols_int,
'volume': v,
'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"
# Map Book Tags
if len(row) > 13 and row[13].strip():
for t in row[13].split(','):
if t.strip():
btag, _ = BookSubjectArea.objects.get_or_create(title=self.wrap_lang(t.strip()))
book.subject_area.add(btag)
# Attach Media
if os.path.exists(book_folder):
for root, _, files in os.walk(book_folder):
folder_name = os.path.basename(root)
is_root = (root == book_folder)
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"
file_path = os.path.join(root, file)
if file_lower.endswith('.pdf'):
if file_lower in [f"{vol_num_str}.pdf", f"{vol_padded_str}.pdf"] or not is_multi_vol:
if v and file_lower in [f"{v}.pdf", f"{v.zfill(2)}.pdf"]:
with open(file_path, 'rb') as doc_f:
doc = BookReferenceDocument(book_reference=book, volume=vol_num_str, title=file)
doc = BookReferenceDocument(book_reference=book, volume=v, title=file)
doc.file.save(file, File(doc_f), save=True)
elif not v and is_root and not file_lower[0].isdigit():
with open(file_path, 'rb') as doc_f:
doc = BookReferenceDocument(book_reference=book, volume=v, 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:
if v and not is_root and folder_name.lstrip('0') == v.lstrip('0'):
with open(file_path, 'rb') as img_f:
img = BookReferenceImage(book_reference=book, volume=v)
img.image.save(file, File(img_f), save=True)
elif not v and is_root:
with open(file_path, 'rb') as img_f:
img = BookReferenceImage(book_reference=book, volume=vol_num_str)
img = BookReferenceImage(book_reference=book, volume=v)
img.image.save(file, File(img_f), save=True)
self.stdout.write(self.style.SUCCESS('Books (split by volumes) loaded successfully.'))
self.stdout.write(self.style.SUCCESS('Books loaded successfully.'))
# --- PHASE 2: NARRATORS ---
@ -142,25 +172,29 @@ class Command(BaseCommand):
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'))
)
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('')
}
)
NarratorLayer.objects.get_or_create(number=generation, defaults={'name': self.wrap_lang(f'Layer {generation}'), 'description': self.wrap_lang('')})
# Safe Age Extraction
age_str = info.get('age', '')
age_nums = re.findall(r'\d+', str(age_str))
age_val = int(age_nums[0]) if age_nums else None
# Madhhab Translation
madhhab_list = n_data.get('madhab', [])
madhhab_val = Transmitters.MadhhabChoices.UNKNOWN
if madhhab_list:
m_str = str(madhhab_list[0]).lower()
if 'шиит' in m_str: madhhab_val = Transmitters.MadhhabChoices.SHIA
elif 'суннит' in m_str: madhhab_val = Transmitters.MadhhabChoices.SUNNI
else: madhhab_val = Transmitters.MadhhabChoices.OTHER
# Create Transmitter
transmitter, _ = Transmitters.objects.update_or_create(
legacy_id=legacy_id,
defaults={
@ -177,11 +211,20 @@ class Command(BaseCommand):
'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', {})
'relatives_raw': info.get('relatives', {}),
# NEW FIELDS MAPPED
'freed_slave_of': self.wrap_lang(info.get('freed_slave_of', ''), 'ru') + self.wrap_lang(ar_info.get('freed_slave_of', ''), 'ar'),
'occupation': self.wrap_lang(info.get('occupation', ''), 'ru') + self.wrap_lang(ar_info.get('occupation', ''), 'ar'),
'features': self.wrap_lang(info.get('features', ''), 'ru') + self.wrap_lang(ar_info.get('features', ''), 'ar'),
'birth_year_hijri': str(info.get('birth_year', '')),
'death_year_hijri': str(info.get('death_year', '')),
'age_at_death': age_val,
'tags': n_data.get('tags', []),
'madhhab': madhhab_val,
}
)
# 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})
@ -191,7 +234,6 @@ class Command(BaseCommand):
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,
@ -199,14 +241,16 @@ class Command(BaseCommand):
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(
ref_obj, _ = OriginalTextReference.objects.update_or_create(
original_text=orig_text, book_reference=book_ref,
volume=ed.get('volume'), page=ed.get('page'), url=ed.get('url')
defaults={
'volume': str(ed.get('volume', '')),
'page': str(ed.get('pages', '')), # Fixed from '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)
@ -214,21 +258,17 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS('Narrators loaded successfully.'))
# --- PHASE 3: HADITHS (Arguments, Corrections, Interpretations) ---
# --- PHASE 3: HADITHS ---
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('')
}
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':
@ -243,11 +283,7 @@ class Command(BaseCommand):
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('')
}
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', '')),
@ -257,8 +293,7 @@ class Command(BaseCommand):
hadis, _ = Hadis.objects.update_or_create(
legacy_id=item.get('id'),
defaults={
'category': category,
'hadis_status': status,
'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(''),
@ -270,15 +305,17 @@ class Command(BaseCommand):
}
)
# Map Hadith Tags
hadis.tags.clear()
for tag_str in item.get('tags', []):
htag, _ = HadisTag.objects.get_or_create(title=self.wrap_lang(tag_str))
hadis.tags.add(htag)
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
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):
@ -289,33 +326,47 @@ class Command(BaseCommand):
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(
href, _ = HadisReference.objects.update_or_create(
hadis=hadis, book_reference=book,
defaults={'hadith_number': str(ed.get('hadith_number', '')), 'description': self.wrap_lang('')}
defaults={
'hadith_number': str(ed.get('hadith_number', '')),
'description': self.wrap_lang(''),
'volume': str(ed.get('volume', '')),
'pages': str(ed.get('pages', '')),
'url': ed.get('url', '')
}
)
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')
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(
# CHANGE TO update_or_create HERE:
corr, _ = HadisCorrection.objects.update_or_create(
hadis=parent_hadith, legacy_id=item.get('id'),
defaults={
'title': self.wrap_lang(''),
'text': item.get('original_text', ''), # Directly mapped to TextField
'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'))
cref, _ = CorrectionReference.objects.get_or_create(correction=corr, book_reference=book, defaults={'hadith_number': str(ed.get('hadith_number', ''))})
cref, _ = CorrectionReference.objects.update_or_create(
correction=corr, book_reference=book,
defaults={
'hadith_number': str(ed.get('hadith_number', '')),
'volume': str(ed.get('volume', '')),
'pages': str(ed.get('pages', '')),
'url': ed.get('url', '')
}
)
if ed.get('screenshots_folder'):
self._attach_images(os.path.join(base_dir, 'screens', item.get('id'), ed.get('screenshots_folder')), CorrectionReferenceImage, cref)
@ -323,9 +374,9 @@ class Command(BaseCommand):
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(
# CHANGE TO update_or_create HERE:
interp, _ = HadisInterpretation.objects.update_or_create(
category=category, legacy_id=item.get('id'),
defaults={
'title': self.wrap_lang(''),
@ -335,7 +386,15 @@ class Command(BaseCommand):
)
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', ''))})
iref, _ = InterpretationReference.objects.update_or_create(
interpretation=interp, book_reference=book,
defaults={
'hadith_number': str(ed.get('hadith_number', '')),
'volume': str(ed.get('volume', '')),
'pages': str(ed.get('pages', '')),
'url': ed.get('url', '')
}
)
if ed.get('screenshots_folder'):
self._attach_images(os.path.join(base_dir, 'screens', item.get('id'), ed.get('screenshots_folder')), InterpretationReferenceImage, iref)
@ -344,15 +403,11 @@ class Command(BaseCommand):
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"
vol_clean = ''.join(filter(str.isdigit, str(volume_str)))
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'):
@ -363,7 +418,5 @@ class Command(BaseCommand):
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)

58
apps/hadis/migrations/0011_bookreference_city_of_publication_and_more.py

@ -0,0 +1,58 @@
# Generated by Django 5.2.12 on 2026-05-10 10:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hadis', '0010_remove_hadiscorrection_description_and_more'),
]
operations = [
migrations.AddField(
model_name='bookreference',
name='city_of_publication',
field=models.JSONField(blank=True, default=list, verbose_name='City of Publication'),
),
migrations.AddField(
model_name='bookreference',
name='country_of_publication',
field=models.JSONField(blank=True, default=list, verbose_name='Country of Publication'),
),
migrations.AddField(
model_name='bookreference',
name='edition_number',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Edition Number'),
),
migrations.AddField(
model_name='bookreference',
name='order',
field=models.IntegerField(default=0, verbose_name='Order'),
),
migrations.AddField(
model_name='bookreference',
name='researcher',
field=models.JSONField(blank=True, default=list, verbose_name='Researcher'),
),
migrations.AddField(
model_name='transmitters',
name='features',
field=models.JSONField(blank=True, default=list, verbose_name='Features'),
),
migrations.AddField(
model_name='transmitters',
name='freed_slave_of',
field=models.JSONField(blank=True, default=list, verbose_name='Freed Slave Of'),
),
migrations.AddField(
model_name='transmitters',
name='occupation',
field=models.JSONField(blank=True, default=list, verbose_name='Occupation'),
),
migrations.AddField(
model_name='transmitters',
name='tags',
field=models.JSONField(blank=True, default=list, verbose_name='Tags'),
),
]

68
apps/hadis/migrations/0012_correctionreferenceimage_pages_and_more.py

@ -0,0 +1,68 @@
# Generated by Django 5.2.12 on 2026-05-10 10:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hadis', '0011_bookreference_city_of_publication_and_more'),
]
operations = [
migrations.AddField(
model_name='correctionreferenceimage',
name='pages',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='correctionreferenceimage',
name='url',
field=models.URLField(blank=True, max_length=1000, null=True),
),
migrations.AddField(
model_name='correctionreferenceimage',
name='volume',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='hadisreference',
name='pages',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='hadisreference',
name='url',
field=models.URLField(blank=True, max_length=1000, null=True),
),
migrations.AddField(
model_name='hadisreference',
name='volume',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='interpretationreference',
name='pages',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='interpretationreference',
name='url',
field=models.URLField(blank=True, max_length=1000, null=True),
),
migrations.AddField(
model_name='interpretationreference',
name='volume',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AlterField(
model_name='transmitters',
name='birth_year_hijri',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Birth Year (Hijri)'),
),
migrations.AlterField(
model_name='transmitters',
name='death_year_hijri',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Death Year (Hijri)'),
),
]

40
apps/hadis/migrations/0013_remove_correctionreferenceimage_pages_and_more.py

@ -0,0 +1,40 @@
# Generated by Django 5.2.12 on 2026-05-10 11:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hadis', '0012_correctionreferenceimage_pages_and_more'),
]
operations = [
migrations.RemoveField(
model_name='correctionreferenceimage',
name='pages',
),
migrations.RemoveField(
model_name='correctionreferenceimage',
name='url',
),
migrations.RemoveField(
model_name='correctionreferenceimage',
name='volume',
),
migrations.AddField(
model_name='correctionreference',
name='pages',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='correctionreference',
name='url',
field=models.URLField(blank=True, max_length=1000, null=True),
),
migrations.AddField(
model_name='correctionreference',
name='volume',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

9
apps/hadis/models/hadis.py

@ -421,6 +421,9 @@ class HadisReference(models.Model):
verbose_name=_('book reference'),
related_name='hadis_references'
)
volume = models.CharField(max_length=100, null=True, blank=True)
pages = models.CharField(max_length=100, null=True, blank=True)
url = models.URLField(max_length=1000, null=True, blank=True)
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'))
@ -582,6 +585,9 @@ 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)
volume = models.CharField(max_length=100, null=True, blank=True)
pages = models.CharField(max_length=100, null=True, blank=True)
url = models.URLField(max_length=1000, null=True, blank=True)
class CorrectionReferenceImage(models.Model):
reference = models.ForeignKey(CorrectionReference, related_name='images', on_delete=models.CASCADE)
@ -594,6 +600,9 @@ 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)
volume = models.CharField(max_length=100, null=True, blank=True)
pages = models.CharField(max_length=100, null=True, blank=True)
url = models.URLField(max_length=1000, null=True, blank=True)
class InterpretationReferenceImage(models.Model):
reference = models.ForeignKey(InterpretationReference, related_name='images', on_delete=models.CASCADE)

5
apps/hadis/models/reference.py

@ -74,6 +74,11 @@ class BookReference(models.Model):
publisher = models.JSONField(default = list , verbose_name=_('Publisher'))
subject_area = models.ManyToManyField(BookSubjectArea, related_name="book_subjects", verbose_name=_('subject area'), blank=True)
type = models.ForeignKey(BookType, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('type'))
order = models.IntegerField(default=0, verbose_name=_('Order'))
researcher = models.JSONField(default=list, blank=True, verbose_name=_('Researcher'))
city_of_publication = models.JSONField(default=list, blank=True, verbose_name=_('City of Publication'))
country_of_publication = models.JSONField(default=list, blank=True, verbose_name=_('Country of Publication'))
edition_number = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('Edition Number'))
rate = models.DecimalField(
max_digits=3,
decimal_places=2,

9
apps/hadis/models/transmitter.py

@ -243,14 +243,19 @@ class Transmitters(models.Model):
# ManyToMany for siblings allows bi-directional sibling relationships
siblings = models.ManyToManyField('self', blank=True, verbose_name=_('Siblings'))
freed_slave_of = models.JSONField(default=list, blank=True, verbose_name=_('Freed Slave Of'))
occupation = models.JSONField(default=list, blank=True, verbose_name=_('Occupation'))
features = models.JSONField(default=list, blank=True, verbose_name=_('Features'))
tags = models.JSONField(default=list, blank=True, verbose_name=_('Tags'))
# Geographic Information
origin = models.JSONField(default = list , verbose_name=_('Origin'))
lived_in = models.JSONField(default = list , verbose_name=_('Lived in'))
died_in = models.JSONField(default = list , verbose_name=_('Died in'))
# Date Information
birth_year_hijri = models.IntegerField(verbose_name=_("Birth Year (Hijri)"), null=True, blank=True)
death_year_hijri = models.IntegerField(verbose_name=_("Death Year (Hijri)"), null=True, blank=True)
birth_year_hijri = models.CharField(max_length=50, verbose_name=_("Birth Year (Hijri)"), null=True, blank=True)
death_year_hijri = models.CharField(max_length=50, verbose_name=_("Death Year (Hijri)"), null=True, blank=True)
age_at_death = models.PositiveIntegerField(verbose_name=_('Age at Death'), blank=True, null=True)
generation = models.PositiveIntegerField(verbose_name=_('Generation'), blank=True, null=True)
# Religious & Academic Information

8
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),
'text': correction.text,
'description': correction.text,
'translation': get_localized_text(correction.translation, request),
'share_link': correction.share_link,
})
@ -706,12 +706,14 @@ class HadisReferenceSerializer(serializers.ModelSerializer):
class HadisCorrectionSerializer(serializers.ModelSerializer):
"""Serializer for HadisCorrection"""
title = LocalizedField()
text = LocalizedField()
description = serializers.SerializerMethodField()
translation = LocalizedField()
bookmark = serializers.SerializerMethodField()
class Meta:
model = HadisCorrection
fields = ['id', 'title','slug', 'text', 'translation','share_link','bookmark']
fields = ['id', 'title','slug','description', 'translation','share_link','bookmark']
def get_description(self, obj):
return obj.text
def get_bookmark(self, obj):
"""Get bookmark information for this correction."""

Loading…
Cancel
Save