diff --git a/apps/course/admin/course.py b/apps/course/admin/course.py index 631bf3f..e4f17b3 100644 --- a/apps/course/admin/course.py +++ b/apps/course/admin/course.py @@ -1,3 +1,4 @@ +from utils.admin import admin_url_generator import os import hashlib diff --git a/apps/hadis/management/commands/import_legacy_data.py b/apps/hadis/management/commands/import_legacy_data.py index 850d2be..cf25170 100644 --- a/apps/hadis/management/commands/import_legacy_data.py +++ b/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) + + # 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) - # Scan Book Folder for Specific Volume Images and PDFs - book_folder = os.path.join(base_dir, 'books', base_legacy_id) + # Attach Media 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) - + 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) \ No newline at end of file diff --git a/apps/hadis/migrations/0011_bookreference_city_of_publication_and_more.py b/apps/hadis/migrations/0011_bookreference_city_of_publication_and_more.py new file mode 100644 index 0000000..72e84b0 --- /dev/null +++ b/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'), + ), + ] diff --git a/apps/hadis/migrations/0012_correctionreferenceimage_pages_and_more.py b/apps/hadis/migrations/0012_correctionreferenceimage_pages_and_more.py new file mode 100644 index 0000000..f2264a1 --- /dev/null +++ b/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)'), + ), + ] diff --git a/apps/hadis/migrations/0013_remove_correctionreferenceimage_pages_and_more.py b/apps/hadis/migrations/0013_remove_correctionreferenceimage_pages_and_more.py new file mode 100644 index 0000000..6cc1984 --- /dev/null +++ b/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), + ), + ] diff --git a/apps/hadis/models/hadis.py b/apps/hadis/models/hadis.py index 01f728b..0d694d5 100644 --- a/apps/hadis/models/hadis.py +++ b/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) diff --git a/apps/hadis/models/reference.py b/apps/hadis/models/reference.py index 080e140..2d9dd97 100644 --- a/apps/hadis/models/reference.py +++ b/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, diff --git a/apps/hadis/models/transmitter.py b/apps/hadis/models/transmitter.py index 774bd42..f0fd7a1 100644 --- a/apps/hadis/models/transmitter.py +++ b/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 diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index 55e607a..89eeb18 100644 --- a/apps/hadis/serializers/hadis.py +++ b/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."""