Browse Source

feat(hadis): enhance hadis management with collections and new endpoints

- Introduced `HadisCollection` and `HadisInCollection` models to manage collections of hadis.
- Updated URL patterns to include new endpoints for collections and syncing hadis data.
- Enhanced admin interface to support management of hadis collections and their items.
- Added serializers for handling collections and syncing hadis data.
- Implemented a new API view for retrieving hadis statistics.
- Refactored existing serializers and views to accommodate new features and improve data handling.
master
mortezaei 5 months ago
parent
commit
196a2b4887
  1. 44
      apps/hadis/admin/hadis.py
  2. 11
      apps/hadis/admin/transmitter.py
  3. 226
      apps/hadis/migrations/0003_bookreference_narratorlayer_and_more.py
  4. 52
      apps/hadis/migrations/0004_hadiscollection_hadisincollection.py
  5. 3
      apps/hadis/models/__init__.py
  6. 4
      apps/hadis/models/category.py
  7. 71
      apps/hadis/models/hadis.py
  8. 109
      apps/hadis/models/reference.py
  9. 156
      apps/hadis/models/transmitter.py
  10. 42
      apps/hadis/serializers/category.py
  11. 59
      apps/hadis/serializers/hadis.py
  12. 21
      apps/hadis/urls.py
  13. 2
      apps/hadis/views/__init__.py
  14. 77
      apps/hadis/views/category.py
  15. 47
      apps/hadis/views/hadis.py
  16. 30
      apps/hadis/views/info.py

44
apps/hadis/admin/hadis.py

@ -9,7 +9,8 @@ import json
from utils.admin import project_admin_site
from ..models import (
Hadis, HadisReference, HadisTag, HadisStatus, ReferenceImage
Hadis, HadisReference, HadisTag, HadisStatus, ReferenceImage,
HadisCollection, HadisInCollection
)
@ -99,7 +100,7 @@ class HadisReferenceInline(TabularInline):
"""Inline for HadisReference in Hadis admin"""
model = HadisReference
extra = 0
fields = ('book', 'description')
fields = ('book_reference',)
readonly_fields = ('created_at',)
@ -170,15 +171,15 @@ class HadisAdmin(ModelAdmin):
class HadisReferenceAdmin(ModelAdmin):
"""Admin for HadisReference model"""
list_display = ('hadis', 'book', 'created_at')
list_filter = ('created_at', 'book')
search_fields = ('hadis__title', 'book__title', 'description')
list_display = ('hadis', 'book_reference', 'created_at')
list_filter = ('created_at', 'book_reference')
search_fields = ('hadis__title', 'book_reference__title')
readonly_fields = ('created_at',)
inlines = [ReferenceImageInline]
fieldsets = (
(None, {
'fields': ('hadis', 'book', 'description')
'fields': ('hadis', 'book_reference')
}),
(_('Timestamps'), {
'fields': ('created_at',),
@ -201,9 +202,38 @@ class ReferenceImageAdmin(ModelAdmin):
)
class HadisInCollectionInline(TabularInline):
"""Inline for HadisInCollection in HadisCollection admin"""
model = HadisInCollection
extra = 0
fields = ('hadis', 'order')
ordering = ('order',)
class HadisCollectionAdmin(ModelAdmin):
"""Admin for HadisCollection model"""
list_display = ('title', 'slug', 'status', 'order', 'created_at')
list_filter = ('status', 'created_at')
search_fields = ('title', 'slug', 'summary')
readonly_fields = ('slug', 'created_at', 'updated_at')
ordering = ('order',)
inlines = [HadisInCollectionInline]
fieldsets = (
(None, {
'fields': ('title', 'slug', 'summary', 'status', 'order', 'thumbnail')
}),
(_('Timestamps'), {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
# Register models with the custom admin site
project_admin_site.register(HadisTag, HadisTagAdmin)
project_admin_site.register(HadisStatus, HadisStatusAdmin)
project_admin_site.register(Hadis, HadisAdmin)
project_admin_site.register(HadisReference, HadisReferenceAdmin)
project_admin_site.register(ReferenceImage, ReferenceImageAdmin)
project_admin_site.register(ReferenceImage, ReferenceImageAdmin)
project_admin_site.register(HadisCollection, HadisCollectionAdmin)

11
apps/hadis/admin/transmitter.py

@ -11,7 +11,7 @@ class HadisTransmitterInline(TabularInline):
"""Inline for HadisTransmitter in Transmitters admin"""
model = HadisTransmitter
extra = 0
fields = ('hadis', 'order', 'status', 'is_gap')
fields = ('hadis', 'order', 'status')
readonly_fields = ('created_at',)
@ -40,8 +40,8 @@ class TransmittersAdmin(ModelAdmin):
class HadisTransmitterAdmin(ModelAdmin):
"""Admin for HadisTransmitter model"""
list_display = ('hadis', 'transmitter', 'order', 'status', 'is_gap', 'created_at')
list_filter = ('status', 'is_gap', 'created_at')
list_display = ('hadis', 'transmitter', 'order', 'status', 'created_at')
list_filter = ('status', 'created_at')
search_fields = ('hadis__title', 'transmitter__full_name')
readonly_fields = ('created_at',)
ordering = ('hadis', 'order')
@ -50,9 +50,8 @@ class HadisTransmitterAdmin(ModelAdmin):
(None, {
'fields': ('hadis', 'transmitter', 'order')
}),
(_('Status & Gap Information'), {
'fields': ('status', 'is_gap'),
'description': _('Use "Is Gap" to mark missing links in the transmission chain')
(_('Status Information'), {
'fields': ('status',)
}),
(_('Timestamps'), {
'fields': ('created_at',),

226
apps/hadis/migrations/0003_bookreference_narratorlayer_and_more.py

@ -0,0 +1,226 @@
# Generated by Django 5.1.8 on 2025-12-03 23:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hadis', '0002_hadissect_hadisstatus_alter_hadis_options_and_more'),
]
operations = [
migrations.CreateModel(
name='BookReference',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=500, verbose_name='title')),
('description', models.TextField(blank=True, null=True, verbose_name='description')),
('language', models.CharField(blank=True, max_length=100, null=True, verbose_name='language')),
('isbn', models.CharField(blank=True, max_length=100, null=True, verbose_name='ISBN')),
('volume', models.CharField(blank=True, max_length=100, null=True, verbose_name='volume')),
('year_of_publication', models.CharField(blank=True, max_length=50, null=True, verbose_name='year of publication')),
('number_page', models.PositiveIntegerField(blank=True, null=True, verbose_name='number of pages')),
('rate', models.DecimalField(blank=True, decimal_places=2, help_text='Rating from 0 to 5', max_digits=3, null=True, verbose_name='rate')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
],
options={
'verbose_name': 'Book Reference',
'verbose_name_plural': 'Book References',
'ordering': ('-created_at',),
},
),
migrations.CreateModel(
name='NarratorLayer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name')),
('number', models.PositiveIntegerField(unique=True, verbose_name='layer number')),
('description', models.TextField(blank=True, null=True, verbose_name='description')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
],
options={
'verbose_name': 'Narrator Layer',
'verbose_name_plural': 'Narrator Layers',
'ordering': ['number'],
},
),
migrations.AlterUniqueTogether(
name='hadisreference',
unique_together=set(),
),
migrations.RemoveField(
model_name='hadistransmitter',
name='is_gap',
),
migrations.AddField(
model_name='hadis',
name='title_narrator',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='title narrator'),
),
migrations.AddField(
model_name='hadiscategory',
name='description',
field=models.TextField(blank=True, null=True, verbose_name='Description'),
),
migrations.AddField(
model_name='hadissect',
name='description',
field=models.TextField(blank=True, null=True, verbose_name='Description'),
),
migrations.AddField(
model_name='transmitters',
name='age_at_death',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Age at Death'),
),
migrations.AddField(
model_name='transmitters',
name='died_in',
field=models.CharField(blank=True, help_text='Place of death', max_length=255, null=True, verbose_name='Died In'),
),
migrations.AddField(
model_name='transmitters',
name='in_sahih_bukhari',
field=models.BooleanField(default=False, help_text='Is this narrator present in Sahih Bukhari?', verbose_name='In Sahih Bukhari'),
),
migrations.AddField(
model_name='transmitters',
name='in_sahih_muslim',
field=models.BooleanField(default=False, help_text='Is this narrator present in Sahih Muslim?', verbose_name='In Sahih Muslim'),
),
migrations.AddField(
model_name='transmitters',
name='known_as',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Known As'),
),
migrations.AddField(
model_name='transmitters',
name='kunya',
field=models.CharField(blank=True, help_text='e.g., Abu Abdullah', max_length=255, null=True, verbose_name='Kunya'),
),
migrations.AddField(
model_name='transmitters',
name='lived_in',
field=models.CharField(blank=True, help_text='Places where they lived', max_length=255, null=True, verbose_name='Lived In'),
),
migrations.AddField(
model_name='transmitters',
name='madhhab',
field=models.CharField(choices=[('shia', 'Shia'), ('sunni', 'Sunni'), ('hanafi', 'Hanafi'), ('maliki', 'Maliki'), ('shafii', "Shafi'i"), ('hanbali', 'Hanbali'), ('other', 'Other'), ('unknown', 'Unknown')], default='unknown', max_length=20, verbose_name='Madhhab/School of Thought'),
),
migrations.AddField(
model_name='transmitters',
name='nickname',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Nickname/Laqab'),
),
migrations.AddField(
model_name='transmitters',
name='origin',
field=models.CharField(blank=True, help_text='Place of origin', max_length=255, null=True, verbose_name='Origin'),
),
migrations.AddField(
model_name='transmitters',
name='reliability',
field=models.CharField(choices=[('very_reliable', 'Very Reliable'), ('reliable', 'Reliable'), ('acceptable', 'Acceptable'), ('weak', 'Weak'), ('very_weak', 'Very Weak'), ('unknown', 'Unknown')], default='unknown', max_length=20, verbose_name='Reliability Level'),
),
migrations.AlterField(
model_name='hadiscategory',
name='source_type',
field=models.CharField(choices=[('quran', 'Quran'), ('hadith', 'Hadith'), ('history', 'History'), ('fatwa', 'Fatwa'), ('quote', 'Quote')], max_length=10, verbose_name='Source Type'),
),
migrations.AlterField(
model_name='hadistransmitter',
name='status',
field=models.CharField(choices=[('reliable', 'Reliable'), ('weak', 'Weak'), ('unknown', 'Unknown')], default='unknown', help_text='Reliability status of the narrator', max_length=20, verbose_name='reliability status'),
),
migrations.AlterField(
model_name='hadistransmitter',
name='transmitter',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hadises', to='hadis.transmitters', verbose_name='transmitter'),
),
migrations.CreateModel(
name='BookAuthor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
('book_references', models.ManyToManyField(blank=True, related_name='authors', to='hadis.bookreference', verbose_name='book references')),
],
options={
'verbose_name': 'Book Author',
'verbose_name_plural': 'Book Authors',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='BookAttribute',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='title')),
('value', models.CharField(max_length=500, verbose_name='value')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
('book_references', models.ManyToManyField(blank=True, related_name='attributes', to='hadis.bookreference', verbose_name='book references')),
],
options={
'verbose_name': 'Book Attribute',
'verbose_name_plural': 'Book Attributes',
'ordering': ['title'],
},
),
migrations.AddField(
model_name='hadisreference',
name='book_reference',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hadis_references', to='hadis.bookreference', verbose_name='book reference'),
),
migrations.CreateModel(
name='BookReferenceImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='hadis/book_reference_images/', verbose_name='image')),
('order', models.PositiveIntegerField(default=0, verbose_name='order')),
('description', models.CharField(blank=True, max_length=255, null=True, verbose_name='description')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('book_reference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='hadis.bookreference', verbose_name='book reference')),
],
options={
'verbose_name': 'Book Reference Image',
'verbose_name_plural': 'Book Reference Images',
'ordering': ['order', '-created_at'],
},
),
migrations.AddField(
model_name='hadistransmitter',
name='narrator_layer',
field=models.ForeignKey(blank=True, help_text='The layer/class (Tabaqah) this narrator belongs to', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transmitters', to='hadis.narratorlayer', verbose_name='narrator layer'),
),
migrations.CreateModel(
name='TransmitterOpinion',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('scholar_name', models.CharField(help_text='Name of the scholar who gave this opinion', max_length=255, verbose_name='Scholar Name')),
('opinion_text', models.TextField(help_text="The scholar's opinion about this transmitter", verbose_name='Opinion Text')),
('status', models.CharField(choices=[('confirmed', 'Confirmed'), ('mixed', 'Mixed'), ('rejected', 'Rejected')], default='confirmed', help_text='Status of the opinion', max_length=20, verbose_name='Opinion Status')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
('transmitter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opinions', to='hadis.transmitters', verbose_name='transmitter')),
],
options={
'verbose_name': 'Transmitter Opinion',
'verbose_name_plural': 'Transmitter Opinions',
'ordering': ('-created_at',),
},
),
migrations.RemoveField(
model_name='hadisreference',
name='book',
),
migrations.RemoveField(
model_name='hadisreference',
name='description',
),
]

52
apps/hadis/migrations/0004_hadiscollection_hadisincollection.py

@ -0,0 +1,52 @@
# Generated by Django 3.2.4 on 2025-12-05 17:07
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import filer.fields.image
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
('hadis', '0003_bookreference_narratorlayer_and_more'),
]
operations = [
migrations.CreateModel(
name='HadisCollection',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='title')),
('slug', models.SlugField(blank=True, max_length=255, unique=True, verbose_name='slug')),
('summary', models.TextField(blank=True, null=True, verbose_name='summary')),
('status', models.BooleanField(default=True, verbose_name='status')),
('order', models.IntegerField(default=0, verbose_name='order')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
('thumbnail', filer.fields.image.FilerImageField(blank=True, help_text='thumbnail image', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.FILER_IMAGE_MODEL, verbose_name='thumbnail')),
],
options={
'verbose_name': 'hadis collection',
'verbose_name_plural': 'hadis collections',
'ordering': ('order',),
},
),
migrations.CreateModel(
name='HadisInCollection',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.IntegerField(default=0, verbose_name='order')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hadis_items', to='hadis.hadiscollection', verbose_name='collection')),
('hadis', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collection_items', to='hadis.hadis', verbose_name='hadis')),
],
options={
'verbose_name': 'hadis in collection',
'verbose_name_plural': 'hadis in collections',
'ordering': ('order',),
'unique_together': {('hadis', 'collection')},
},
),
]

3
apps/hadis/models/__init__.py

@ -1,3 +1,4 @@
from .category import *
from .hadis import *
from .transmitter import *
from .transmitter import *
from .reference import *

4
apps/hadis/models/category.py

@ -29,11 +29,15 @@ class HadisCategory(MPTTModel):
class SourceType(models.TextChoices):
QURAN = 'quran', _('Quran')
HADITH = 'hadith', _('Hadith')
HISTORY = 'history', _('History')
FATWA = 'fatwa', _('Fatwa')
QUOTE = 'quote', _('Quote')
parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
sect = models.ForeignKey(HadisSect, on_delete=models.PROTECT, verbose_name=_('Sect'), null=False, blank=False)
source_type = models.CharField(max_length=10, choices=SourceType.choices, verbose_name=_('Source Type'))
title = models.CharField(max_length=256, verbose_name=_('Title'))
description = models.TextField(verbose_name=_('Description'), null=True, blank=True)
order = models.IntegerField(default=0, verbose_name=_('order'))
xmind_file = models.FileField(upload_to='hadis/xmind_files/', verbose_name=_('xmind file'), null=True, blank=True)
slug = None

71
apps/hadis/models/hadis.py

@ -2,6 +2,61 @@ from django.db import models
from django.db.models import F
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.utils.text import slugify
from filer.fields.image import FilerImageField
class HadisCollection(models.Model):
title = models.CharField(max_length=255, verbose_name=_('title'))
slug = models.SlugField(max_length=255, unique=True, verbose_name=_('slug'), blank=True)
summary = models.TextField(verbose_name=_('summary'), null=True, blank=True)
status = models.BooleanField(default=True, verbose_name=_('status'))
order = models.IntegerField(default=0, verbose_name=_('order'))
thumbnail = FilerImageField(
related_name="+",
on_delete=models.CASCADE,
help_text=_('thumbnail image'),
null=True,
blank=True,
verbose_name=_('thumbnail')
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.title, allow_unicode=True)
slug = base_slug
counter = 1
while HadisCollection.objects.filter(slug=slug).exclude(pk=self.pk).exists():
slug = f"{base_slug}-{counter}"
counter += 1
self.slug = slug
super().save(*args, **kwargs)
def __str__(self):
return self.title
class Meta:
verbose_name = _('hadis collection')
verbose_name_plural = _('hadis collections')
ordering = ('order',)
class HadisInCollection(models.Model):
hadis = models.ForeignKey('Hadis', on_delete=models.CASCADE, verbose_name=_('hadis'), related_name='collection_items')
collection = models.ForeignKey(HadisCollection, on_delete=models.CASCADE, verbose_name=_('collection'), related_name='hadis_items')
order = models.IntegerField(default=0, verbose_name=_('order'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
class Meta:
verbose_name = _('hadis in collection')
verbose_name_plural = _('hadis in collections')
ordering = ('order',)
unique_together = ('hadis', 'collection')
def __str__(self):
return f"{self.collection.title} - {self.hadis.number}"
class HadisTag(models.Model):
@ -43,7 +98,9 @@ class HadisStatus(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)
title_narrator = models.CharField(max_length=255, verbose_name=_('title narrator'), null=True, blank=True)
title = models.CharField(max_length=255, verbose_name=_('title'), null=True, blank=True)
text = models.TextField(verbose_name=_('text'))
translation = models.JSONField(verbose_name=_('translation'), default=list)
status = models.BooleanField(default=True, verbose_name=_('visibility'))
@ -99,17 +156,23 @@ class HadisReference(models.Model):
verbose_name=_('hadis'),
related_name='references'
)
book = models.ForeignKey("library.Book", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('book'), related_name='hadis_references')
description = models.TextField(verbose_name=_('description'), blank=True, null=True)
book_reference = models.ForeignKey(
"hadis.BookReference",
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_('book reference'),
related_name='hadis_references'
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
class Meta:
verbose_name = _('Hadis Reference')
verbose_name_plural = _('Hadis References')
unique_together = ('hadis', 'book')
# unique_together = ('hadis', 'book_reference')
def __str__(self):
return f'{self.hadis.number}-{self.book.title if self.book else "No Book"}'
return f'{self.hadis.number}-{self.book_reference.title if self.book_reference else "No Book Reference"}'
class ReferenceImage(models.Model):
reference = models.ForeignKey(HadisReference, verbose_name="Hadis Reference", on_delete=models.CASCADE)

109
apps/hadis/models/reference.py

@ -0,0 +1,109 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class BookReference(models.Model):
"""
Model for hadis book references with detailed information
This is different from library books - these are reference books for hadis
"""
title = models.CharField(max_length=500, verbose_name=_('title'))
description = models.TextField(verbose_name=_('description'), blank=True, null=True)
language = models.CharField(max_length=100, verbose_name=_('language'), blank=True, null=True)
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)
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)
rate = models.DecimalField(
max_digits=3,
decimal_places=2,
verbose_name=_('rate'),
blank=True,
null=True,
help_text=_('Rating from 0 to 5')
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
class Meta:
verbose_name = _('Book Reference')
verbose_name_plural = _('Book References')
ordering = ('-created_at',)
def __str__(self):
return self.title
class BookReferenceImage(models.Model):
"""
Model for book reference images - multiple images per book reference
"""
book_reference = models.ForeignKey(
BookReference,
on_delete=models.CASCADE,
related_name='images',
verbose_name=_('book reference')
)
image = models.ImageField(upload_to='hadis/book_reference_images/', verbose_name=_('image'))
order = models.PositiveIntegerField(default=0, verbose_name=_('order'))
description = models.CharField(max_length=255, verbose_name=_('description'), blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
class Meta:
verbose_name = _('Book Reference Image')
verbose_name_plural = _('Book Reference Images')
ordering = ['order', '-created_at']
def __str__(self):
return f"{self.book_reference.title} - Image {self.order}"
class BookAuthor(models.Model):
"""
Model for book reference authors
"""
name = models.CharField(max_length=255, verbose_name=_('name'))
book_references = models.ManyToManyField(
BookReference,
related_name='authors',
verbose_name=_('book references'),
blank=True
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
class Meta:
verbose_name = _('Book Author')
verbose_name_plural = _('Book Authors')
ordering = ['name']
def __str__(self):
return self.name
class BookAttribute(models.Model):
"""
Model for book reference attributes - custom key-value pairs
"""
title = models.CharField(max_length=255, verbose_name=_('title'))
value = models.CharField(max_length=500, verbose_name=_('value'))
book_references = models.ManyToManyField(
BookReference,
related_name='attributes',
verbose_name=_('book references'),
blank=True
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
class Meta:
verbose_name = _('Book Attribute')
verbose_name_plural = _('Book Attributes')
ordering = ['title']
def __str__(self):
return f"{self.title}: {self.value}"

156
apps/hadis/models/transmitter.py

@ -5,11 +5,89 @@ from django.utils.translation import gettext_lazy as _
from filer.fields.image import FilerImageField
class NarratorLayer(models.Model):
"""
Model for narrator layers/classes (Tabaqat)
Represents the classification level of narrators in hadis chains
"""
name = models.CharField(max_length=255, verbose_name=_('name'))
number = models.PositiveIntegerField(verbose_name=_('layer number'), unique=True)
description = models.TextField(verbose_name=_('description'), blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
class Meta:
verbose_name = _('Narrator Layer')
verbose_name_plural = _('Narrator Layers')
ordering = ['number']
def __str__(self):
return f"{_('Layer')} {self.number} - {self.name}"
class Transmitters(models.Model):
class ReliabilityLevel(models.TextChoices):
VERY_RELIABLE = 'very_reliable', _('Very Reliable')
RELIABLE = 'reliable', _('Reliable')
ACCEPTABLE = 'acceptable', _('Acceptable')
WEAK = 'weak', _('Weak')
VERY_WEAK = 'very_weak', _('Very Weak')
UNKNOWN = 'unknown', _('Unknown')
class MadhhabChoices(models.TextChoices):
SHIA = 'shia', _('Shia')
SUNNI = 'sunni', _('Sunni')
HANAFI = 'hanafi', _('Hanafi')
MALIKI = 'maliki', _('Maliki')
SHAFII = 'shafii', _('Shafi\'i')
HANBALI = 'hanbali', _('Hanbali')
OTHER = 'other', _('Other')
UNKNOWN = 'unknown', _('Unknown')
# Basic Information
full_name = models.CharField(max_length=255, verbose_name=_('full name'))
kunya = models.CharField(max_length=255, verbose_name=_('Kunya'), blank=True, null=True, help_text=_('e.g., Abu Abdullah'))
known_as = models.CharField(max_length=255, verbose_name=_('Known As'), blank=True, null=True)
nickname = models.CharField(max_length=255, verbose_name=_('Nickname/Laqab'), blank=True, null=True)
# Geographic Information
origin = models.CharField(max_length=255, verbose_name=_('Origin'), blank=True, null=True, help_text=_('Place of origin'))
lived_in = models.CharField(max_length=255, verbose_name=_('Lived In'), blank=True, null=True, help_text=_('Places where they lived'))
died_in = models.CharField(max_length=255, verbose_name=_('Died In'), blank=True, null=True, help_text=_('Place of death'))
# 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)
age_at_death = models.PositiveIntegerField(verbose_name=_('Age at Death'), blank=True, null=True)
# Religious & Academic Information
reliability = models.CharField(
max_length=20,
choices=ReliabilityLevel.choices,
default=ReliabilityLevel.UNKNOWN,
verbose_name=_('Reliability Level')
)
madhhab = models.CharField(
max_length=20,
choices=MadhhabChoices.choices,
default=MadhhabChoices.UNKNOWN,
verbose_name=_('Madhhab/School of Thought')
)
# Presence in Famous Collections
in_sahih_muslim = models.BooleanField(
default=False,
verbose_name=_('In Sahih Muslim'),
help_text=_('Is this narrator present in Sahih Muslim?')
)
in_sahih_bukhari = models.BooleanField(
default=False,
verbose_name=_('In Sahih Bukhari'),
help_text=_('Is this narrator present in Sahih Bukhari?')
)
# Additional Information
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
thumbnail = FilerImageField(
related_name="+",
@ -18,6 +96,7 @@ class Transmitters(models.Model):
null=True,
blank=True
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
@ -32,6 +111,10 @@ class Transmitters(models.Model):
class HadisTransmitter(models.Model):
class ReliabilityStatus(models.TextChoices):
RELIABLE = 'reliable', _('Reliable')
WEAK = 'weak', _('Weak')
UNKNOWN = 'unknown', _('Unknown')
hadis = models.ForeignKey(
"hadis.Hadis",
@ -43,29 +126,29 @@ class HadisTransmitter(models.Model):
Transmitters,
on_delete=models.CASCADE,
verbose_name=_('transmitter'),
related_name='hadises',
null=True,
blank=True,
help_text=_('Leave empty if this represents a gap in the chain')
related_name='hadises'
)
status = models.ForeignKey(
"hadis.HadisStatus",
narrator_layer = models.ForeignKey(
NarratorLayer,
on_delete=models.SET_NULL,
verbose_name=_('status'),
verbose_name=_('narrator layer'),
related_name='transmitters',
null=True,
blank=True
blank=True,
help_text=_('The layer/class (Tabaqah) this narrator belongs to')
)
status = models.CharField(
max_length=20,
choices=ReliabilityStatus.choices,
default=ReliabilityStatus.UNKNOWN,
verbose_name=_('reliability status'),
help_text=_('Reliability status of the narrator')
)
order = models.PositiveIntegerField(
default=0,
verbose_name=_('Order'),
help_text=_('Order in the chain of transmission')
)
is_gap = models.BooleanField(
default=False,
verbose_name=_('Is Gap'),
help_text=_('Check this if this represents a gap in the transmission chain')
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
@ -76,4 +159,49 @@ class HadisTransmitter(models.Model):
unique_together = ('hadis', 'transmitter', 'order')
def __str__(self):
return f'{self.hadis.number} - {self.transmitter.full_name if self.transmitter else "Gap"} ({self.order})'
layer_info = f" - {self.narrator_layer}" if self.narrator_layer else ""
return f'{self.hadis.number} - {self.transmitter.full_name} ({self.order}){layer_info}'
class TransmitterOpinion(models.Model):
"""
Model for scholarly opinions about transmitters
"""
class OpinionStatus(models.TextChoices):
CONFIRMED = 'confirmed', _('Confirmed')
MIXED = 'mixed', _('Mixed')
REJECTED = 'rejected', _('Rejected')
transmitter = models.ForeignKey(
Transmitters,
on_delete=models.CASCADE,
verbose_name=_('transmitter'),
related_name='opinions'
)
scholar_name = models.CharField(
max_length=255,
verbose_name=_('Scholar Name'),
help_text=_('Name of the scholar who gave this opinion')
)
opinion_text = models.TextField(
verbose_name=_('Opinion Text'),
help_text=_('The scholar\'s opinion about this transmitter')
)
status = models.CharField(
max_length=20,
choices=OpinionStatus.choices,
default=OpinionStatus.CONFIRMED,
verbose_name=_('Opinion Status'),
help_text=_('Status of the opinion')
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
class Meta:
verbose_name = _('Transmitter Opinion')
verbose_name_plural = _('Transmitter Opinions')
ordering = ('-created_at',)
def __str__(self):
return f"{self.scholar_name}'s opinion on {self.transmitter.full_name} ({self.status})"

42
apps/hadis/serializers/category.py

@ -6,22 +6,30 @@ from ..models import HadisSect, HadisCategory, Hadis
class HadisCategorySectListSerializer(serializers.ModelSerializer):
"""Serializer for HadisSect list with grouped response"""
source_types = serializers.SerializerMethodField()
class Meta:
model = HadisSect
fields = ['id', 'title']
fields = ['id', 'title', 'description', 'source_types']
def get_source_types(self, obj):
"""Get unique source types for this sect's categories"""
source_types = HadisCategory.objects.filter(
sect=obj
).values_list('source_type', flat=True).distinct()
return list(source_types)
def to_representation(self, instance):
# This method will be overridden in the view to create the grouped response
return super().to_representation(instance)
class HadisCategoryTreeSerializer(serializers.ModelSerializer):
"""Serializer for HadisCategory tree structure"""
sect_id = serializers.IntegerField(source='sect.id', read_only=True)
sect_type = serializers.CharField(source='sect.sect_type', read_only=True)
children = serializers.SerializerMethodField()
class Meta:
model = HadisCategory
fields = ['id', 'title', 'source_type']
fields = ['id', 'title', 'source_type', 'sect_id', 'sect_type', 'children']
def get_name(self, obj):
"""Get category name based on request language"""
@ -79,10 +87,27 @@ class HadisCategoryTreeSerializer(serializers.ModelSerializer):
"""Check if category has xmind file"""
return bool(obj.xmind_file)
def get_thumbnail(self, obj):
"""Get absolute URL for thumbnail"""
if hasattr(obj, 'thumbnail') and obj.thumbnail:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.thumbnail.url)
return obj.thumbnail.url
return None
def get_hadis_index(self, obj):
"""Get list of hadis numbers in this category (not including children)"""
return list(
Hadis.objects.filter(
category=obj,
status=True
).order_by('number').values_list('number', flat=True)
)
def to_dict(self, c):
"""Convert category to dictionary"""
children = c.get_children().filter(sect=c.sect).order_by('order')
# Filter children that have either children or hadis
filtered_children = []
for child in children:
has_children = child.get_children().filter(sect=c.sect).exists()
@ -93,9 +118,12 @@ class HadisCategoryTreeSerializer(serializers.ModelSerializer):
return {
'id': c.id,
'name': self.get_name(c),
'description': c.description,
'hadis_count': self.get_hadis_count(c),
'has_hadis': self.get_has_hadis(c),
'hadis_index': self.get_hadis_index(c) if self.get_has_hadis(c) else [],
'order': c.order,
'thumbnail': self.get_thumbnail(c),
'xmind_file': self.get_xmind_file(c),
'has_xmind_file': self.get_has_xmind_file(c),
'children': [] if not filtered_children else [self.to_dict(i) for i in filtered_children],

59
apps/hadis/serializers/hadis.py

@ -3,11 +3,68 @@ from django.utils.translation import gettext_lazy as _
from ..models import (
Hadis, HadisStatus, HadisTag, HadisTransmitter,
HadisReference, ReferenceImage, Transmitters
HadisReference, ReferenceImage, Transmitters, HadisCollection
)
from apps.library.serializers import BookSerializer
class HadisCollectionListSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
class Meta:
model = HadisCollection
fields = ['id', 'title', 'slug', 'thumbnail']
def get_thumbnail(self, obj):
if obj.thumbnail:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.thumbnail.url)
return obj.thumbnail.url
return None
class HadisSyncSerializer(serializers.ModelSerializer):
"""Serializer for syncing all hadis data"""
translations = serializers.SerializerMethodField()
hadis_status = serializers.SerializerMethodField()
tags = serializers.SerializerMethodField()
class Meta:
model = Hadis
fields = [
'id', 'number', 'category_id', 'title', 'title_narrator',
'text', 'translations', 'explanation', 'address',
'hadis_status', 'hadis_status_text', 'share_link', 'tags', 'links'
]
def get_translations(self, obj):
"""Get all translations"""
translations_dict = {}
if obj.translation and isinstance(obj.translation, list):
for tr in obj.translation:
if isinstance(tr, dict):
lang_code = tr.get('language_code')
title = tr.get('title')
if lang_code:
translations_dict[lang_code] = title
return translations_dict
def get_hadis_status(self, obj):
"""Get hadis status info"""
if obj.hadis_status:
return {
'id': obj.hadis_status.id,
'title': obj.hadis_status.title,
'color': obj.hadis_status.color
}
return None
def get_tags(self, obj):
"""Get tags"""
return [{'id': tag.id, 'title': tag.title} for tag in obj.tags.all()]
class HadisListSerializer(serializers.ModelSerializer):
"""Serializer for Hadis list"""
category = serializers.SerializerMethodField()

21
apps/hadis/urls.py

@ -1,16 +1,15 @@
from django.urls import path
from .views.category import HadisSectListView, HadisCategoryTreeView
from .views.hadis import HadisListView, HadisDetailView
from .views.category import HadisCategorySectListView, HadisCategoryTreeView
from .views.hadis import HadisCollectionListView, HadisListView, HadisDetailView, HadisSyncView
from .views.info import HadisInfoView
urlpatterns = [
# Hadis Sect endpoints
path('categories/', HadisSectListView.as_view(), name='hadis-sect-list'),
# Hadis Category endpoints
path('categories/<int:sect_id>/', HadisCategoryTreeView.as_view(), name='hadis-category-tree'),
# Hadis endpoints
path('<int:category_id>/hadis/', HadisListView.as_view(), name='hadis-list'),
path('hadis/<int:hadis_id>/', HadisDetailView.as_view(), name='hadis-detail'),
path('collections/', HadisCollectionListView.as_view(), name='hadis-collection-list'),
path('sync/sects/', HadisCategorySectListView.as_view(), name='hadis-sect-list'),
path('sync/categories/tree/', HadisCategoryTreeView.as_view(), name='hadis-category-tree'),
path('sync/hadis/', HadisSyncView.as_view(), name='hadis-sync'),
path('info/', HadisInfoView.as_view(), name='hadis-info'),
path('category/<int:category_id>/', HadisListView.as_view(), name='hadis-list'),
path('<int:hadis_id>/', HadisDetailView.as_view(), name='hadis-detail'),
]

2
apps/hadis/views/__init__.py

@ -1,3 +1,3 @@
from .category import *
from .hadis import *
# from .transmitter import *
from .info import *

77
apps/hadis/views/category.py

@ -8,7 +8,7 @@ from ..serializers import HadisCategorySectListSerializer, HadisCategoryTreeSeri
from ..docs import hadis_sect_list_swagger, hadis_category_tree_swagger
class HadisSectListView(ListAPIView):
class HadisCategorySectListView(ListAPIView):
"""
API view to list all HadisSects grouped by sect_type (shia/sunni)
"""
@ -35,7 +35,6 @@ class HadisSectListView(ListAPIView):
sect_data = {
'id': sect.id,
'title': sect.title,
'seo_field': None
}
if sect.sect_type == HadisSect.SectType.SHIA:
@ -54,8 +53,8 @@ class HadisSectListView(ListAPIView):
class HadisCategoryTreeView(ListAPIView):
"""
API view to get HadisCategory tree structure by sect_id
Returns categories grouped by source_type (quran/hadith)
API view to get all HadisCategory tree structure grouped by sect and source_type
Returns all categories in a tree structure
"""
serializer_class = HadisCategoryTreeSerializer
pagination_class = NoPagination
@ -65,52 +64,62 @@ class HadisCategoryTreeView(ListAPIView):
return self.list(request, *args, **kwargs)
def get_queryset(self):
sect_id = self.kwargs.get('sect_id')
sect = get_object_or_404(HadisSect, id=sect_id, is_active=True)
# Get root categories (no parent) for this sect
return HadisCategory.objects.filter(
sect=sect,
parent__isnull=True
).order_by('order')
parent__isnull=True,
sect__is_active=True
).order_by('sect__order', 'order')
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
# Group categories by source_type
grouped_data = {
'quran': [],
'hadith': []
}
grouped_data = {}
# Create serializer instance for to_dict method
serializer_instance = HadisCategoryTreeSerializer(context={'request': request})
# گروه‌بندی بر اساس sect
for category in queryset:
sect_id = str(category.sect.id)
if sect_id not in grouped_data:
# اضافه کردن اطلاعات sect
grouped_data[sect_id] = {
'sect_info': {
'id': category.sect.id,
'sect_type': category.sect.sect_type,
'title': category.sect.title,
'description': category.sect.description
},
'source_types': [],
'categories': {}
}
# اضافه کردن source_type به لیست
if category.source_type not in grouped_data[sect_id]['source_types']:
grouped_data[sect_id]['source_types'].append(category.source_type)
# گروه‌بندی categories بر اساس source_type
if category.source_type not in grouped_data[sect_id]['categories']:
grouped_data[sect_id]['categories'][category.source_type] = []
category_data = serializer_instance.to_dict(category)
grouped_data[sect_id]['categories'][category.source_type].append(category_data)
if category.source_type == HadisCategory.SourceType.QURAN:
grouped_data['quran'].append(category_data)
elif category.source_type == HadisCategory.SourceType.HADITH:
grouped_data['hadith'].append(category_data)
# Calculate total count including all descendants recursively
def count_objects_recursive(data_list):
"""Count all objects including children recursively"""
def count_children(children_list):
count = 0
for item in data_list:
count += 1 # Count current item
for item in children_list:
count += 1
if 'children' in item and item['children']:
count += count_objects_recursive(item['children']) # Count children recursively
count += count_children(item['children'])
return count
# Calculate total count from grouped data
total_count = (
count_objects_recursive(grouped_data['quran']) +
count_objects_recursive(grouped_data['hadith'])
)
total_count = 0
for sect_data in grouped_data.values():
for source_categories in sect_data['categories'].values():
for item in source_categories:
total_count += 1
if 'children' in item and item['children']:
total_count += count_children(item['children'])
# Create response with count and results
response_data = {
'count': total_count,
'results': grouped_data

47
apps/hadis/views/hadis.py

@ -1,11 +1,54 @@
from rest_framework.generics import ListAPIView, RetrieveAPIView
from django.shortcuts import get_object_or_404
from utils.pagination import NoPagination
from rest_framework.response import Response
from ..models import HadisCategory, Hadis
from ..serializers import HadisListSerializer, HadisDetailSerializer
from ..models import HadisCategory, Hadis, HadisCollection
from ..serializers import HadisListSerializer, HadisDetailSerializer, HadisCollectionListSerializer, HadisSyncSerializer
from ..docs import hadis_list_swagger, hadis_detail_swagger
class HadisCollectionListView(ListAPIView):
"""
API view to list all hadis collections
"""
queryset = HadisCollection.objects.filter(status=True).order_by('order')
serializer_class = HadisCollectionListSerializer
pagination_class = NoPagination
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
class HadisSyncView(ListAPIView):
"""
API view to sync all hadis data for offline mode
"""
serializer_class = HadisSyncSerializer
pagination_class = NoPagination
def get_queryset(self):
return Hadis.objects.filter(status=True).select_related(
'category', 'hadis_status'
).prefetch_related('tags').order_by('id')
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
grouped_data = {}
for hadis_data in serializer.data:
hadis_id = str(hadis_data['id'])
grouped_data[hadis_id] = hadis_data
response_data = {
'count': queryset.count(),
'results': grouped_data
}
return Response(response_data)
class HadisListView(ListAPIView):
"""
API view to list Hadis by category_id

30
apps/hadis/views/info.py

@ -0,0 +1,30 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from ..models import HadisSect, BookReference, Transmitters
from apps.bookmark.models import Bookmark
class HadisInfoView(APIView):
"""
API view to get hadis statistics
"""
def get(self, request, *args, **kwargs):
category_count = HadisSect.objects.filter(is_active=True).count()
reference_count = BookReference.objects.count()
bookmark_count = Bookmark.objects.filter(
service=Bookmark.ServiceChoices.HADITH,
status=True
).count()
narrator_count = Transmitters.objects.count()
data = {
'category_count': category_count,
'reference_count': reference_count,
'bookmark_count': bookmark_count,
'narrator_count': narrator_count
}
return Response(data, status=status.HTTP_200_OK)
Loading…
Cancel
Save