Browse Source

feat(references): add volume unique images and source/edition single tag system

master
Mohsen Taba 3 weeks ago
parent
commit
1f3a13861b
  1. 4
      apps/hadis/admin/reference.py
  2. 37
      apps/hadis/migrations/0037_remove_bookedition_tags_remove_bookreference_tags_and_more.py
  3. 23
      apps/hadis/models/reference.py
  4. 24
      apps/hadis/serializers/reference_v2.py
  5. 47
      apps/hadis/serializers/serializers_admin.py
  6. 13
      apps/hadis/urls.py
  7. 40
      apps/hadis/views/hadis.py
  8. 52
      apps/hadis/views/reference.py
  9. 2
      apps/hadis/views/reference_v2.py
  10. 19
      apps/hadis/views_admin.py

4
apps/hadis/admin/reference.py

@ -487,11 +487,11 @@ class BookReferenceAdmin(ModelAdmin):
inlines = [BookReferenceImageInline, BookAttributeInline]
# Use filter_horizontal for ManyToMany fields to make selection easier
filter_horizontal = ('subject_area', 'tags')
filter_horizontal = ('subject_area',)
fieldsets = (
(_('Basic Info'), {
'fields': ('title', 'description', 'slug', 'language', 'tags', 'type', 'author')
'fields': ('title', 'description', 'slug', 'language', 'tag', 'type', 'author')
}),
(_('Publication Info'), {
'fields': ('number_page',)

37
apps/hadis/migrations/0037_remove_bookedition_tags_remove_bookreference_tags_and_more.py

@ -0,0 +1,37 @@
# Generated by Django 4.2.30 on 2026-08-29 17:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('hadis', '0036_alter_narratorlayer_options_narratorlayer_hadis_and_more'),
]
operations = [
migrations.RemoveField(
model_name='bookedition',
name='tags',
),
migrations.RemoveField(
model_name='bookreference',
name='tags',
),
migrations.AddField(
model_name='bookedition',
name='tag',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='book_editions', to='hadis.booktag', verbose_name='Tag'),
),
migrations.AddField(
model_name='bookreference',
name='tag',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='book_references', to='hadis.booktag', verbose_name='Tag'),
),
migrations.AddField(
model_name='bookvolume',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='hadis/volume_images/', verbose_name='Image'),
),
]

23
apps/hadis/models/reference.py

@ -101,11 +101,13 @@ class BookReference(LowercaseSlugMixin, models.Model):
verbose_name=_('Subject Area'),
blank=True
)
tags = models.ManyToManyField(
tag = models.ForeignKey(
'BookTag',
verbose_name=_('Tags'),
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='book_references'
related_name='book_references',
verbose_name=_('Tag')
)
type = models.ForeignKey('BookType', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Type'))
author = models.ForeignKey(
@ -215,7 +217,14 @@ class BookEdition(models.Model):
source_url = models.URLField(max_length=1000, null=True, blank=True, verbose_name=_('Online Source URL'))
isbn = models.CharField(max_length=100, verbose_name=_('ISBN'), blank=True, null=True)
notes = models.JSONField(default=list, blank=True, verbose_name=_('Notes'))
tags = models.JSONField(default=list, blank=True, verbose_name=_('Tags'))
tag = models.ForeignKey(
'BookTag',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='book_editions',
verbose_name=_('Tag')
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
@ -235,6 +244,12 @@ class BookVolume(models.Model):
book_reference = models.ForeignKey(BookReference, on_delete=models.CASCADE, related_name='volumes', verbose_name=_('book reference'))
edition = models.ForeignKey(BookEdition, on_delete=models.CASCADE, related_name='volumes', verbose_name=_('edition'), null=True, blank=True)
title = models.CharField(max_length=255, verbose_name=_('Title'), help_text=_('e.g., Volume 1'), blank=True, null=True)
image = models.ImageField(
upload_to='hadis/volume_images/',
null=True,
blank=True,
verbose_name=_('Image')
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))

24
apps/hadis/serializers/reference_v2.py

@ -36,13 +36,13 @@ class BookEditorV2Serializer(serializers.ModelSerializer):
class BookReferenceV2ListSerializer(serializers.ModelSerializer):
title = LocalizedField()
authors = serializers.SerializerMethodField()
tags = BookTagV2Serializer(many=True, read_only=True)
tag = BookTagV2Serializer(read_only=True)
number_of_volumes = serializers.SerializerMethodField()
image = serializers.SerializerMethodField()
class Meta:
model = BookReference
fields = ['id', 'slug', 'title', 'authors', 'number_of_volumes', 'tags', 'image']
fields = ['id', 'slug', 'title', 'authors', 'number_of_volumes', 'tag', 'image']
def get_authors(self, obj):
if obj.author:
@ -53,8 +53,18 @@ class BookReferenceV2ListSerializer(serializers.ModelSerializer):
return obj.volumes.count()
def get_image(self, obj):
if hasattr(obj, 'thumbnail') and obj.thumbnail:
request = self.context.get('request')
if request:
return absolute_https_url(obj.thumbnail.url, request)
return absolute_https_url(obj.thumbnail.url)
first_volume = obj.volumes.first()
if first_volume:
if first_volume.image:
request = self.context.get('request')
if request:
return absolute_https_url(first_volume.image.url, request)
return absolute_https_url(first_volume.image.url)
first_image = first_volume.images.order_by('order').first()
if first_image and first_image.image:
request = self.context.get('request')
@ -66,7 +76,7 @@ class BookReferenceV2ListSerializer(serializers.ModelSerializer):
class BookEditionV2Serializer(serializers.ModelSerializer):
publisher = LocalizedField()
notes = LocalizedField()
tags = LocalizedField()
tag = BookTagV2Serializer(read_only=True)
publication_place = serializers.SerializerMethodField()
researchers = BookResearcherV2Serializer(many=True, read_only=True)
editors = BookEditorV2Serializer(many=True, read_only=True)
@ -75,7 +85,7 @@ class BookEditionV2Serializer(serializers.ModelSerializer):
model = BookEdition
fields = [
'id', 'publisher', 'publication_place', 'edition_number',
'year_of_publication', 'number_of_volumes', 'notes', 'source_url', 'tags',
'year_of_publication', 'number_of_volumes', 'notes', 'source_url', 'tag',
'researchers', 'editors'
]
@ -96,9 +106,13 @@ class BookVolumeV2Serializer(serializers.ModelSerializer):
fields = ['id', 'title', 'image']
def get_image(self, obj):
request = self.context.get('request')
if obj.image:
if request:
return absolute_https_url(obj.image.url, request)
return absolute_https_url(obj.image.url)
first_image = obj.images.order_by('order').first()
if first_image and first_image.image:
request = self.context.get('request')
if request:
return absolute_https_url(first_image.image.url, request)
return absolute_https_url(first_image.image.url)

47
apps/hadis/serializers/serializers_admin.py

@ -7,7 +7,7 @@ from utils.image_compression import maybe_compress_uploaded_file
from utils import absolute_https_url
from ..models import (
Hadis, HadisCategory, HadisStatus, HadisTag, HadisCorrection, HadisInterpretation, HadisCollection, HadisSect, HadisInCollection, HadisTransmitter, HadisReference,
BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookEdition, BookVolume, BookResearcher, BookEditor,
BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, BookResearcher, BookEditor,
Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, TransmitterOpinion, TransmitterOriginalText,
CorrectionReference, CorrectionReferenceImage, InterpretationReference, InterpretationReferenceImage,
OriginalTextReference, OriginalTextReferenceImage
@ -1179,9 +1179,19 @@ class AdminBookReferenceDocumentSerializer(serializers.ModelSerializer):
return obj.book_volume.title if getattr(obj, 'book_volume', None) else ""
class AdminBookTagSerializer(serializers.ModelSerializer):
title = serializers.JSONField(default=list)
class Meta:
model = BookTag
fields = ["id", "title", "created_at", "updated_at"]
class AdminBookReferenceListSerializer(serializers.ModelSerializer):
author_id = serializers.IntegerField(source="author.id", read_only=True, allow_null=True)
author_name = serializers.JSONField(source="author.name", read_only=True, allow_null=True)
tag_id = serializers.IntegerField(source="tag.id", read_only=True, allow_null=True)
tag_detail = AdminBookTagSerializer(source="tag", read_only=True)
volume = serializers.SerializerMethodField()
publisher = serializers.SerializerMethodField()
year_of_publication = serializers.SerializerMethodField()
@ -1195,6 +1205,8 @@ class AdminBookReferenceListSerializer(serializers.ModelSerializer):
"has_editions",
"author_id",
"author_name",
"tag_id",
"tag_detail",
"volume",
"publisher",
"year_of_publication",
@ -1219,6 +1231,7 @@ class AdminBookReferenceDetailSerializer(serializers.ModelSerializer):
subject_area_detail = AdminBookSubjectAreaSerializer(source="subject_area", many=True, read_only=True)
type_detail = AdminBookTypeSerializer(source="type", read_only=True)
author_detail = AdminBookAuthorSerializer(source="author", read_only=True)
tag_detail = AdminBookTagSerializer(source="tag", read_only=True)
subject_area = serializers.PrimaryKeyRelatedField(
queryset=BookSubjectArea.objects.all(),
@ -1235,6 +1248,11 @@ class AdminBookReferenceDetailSerializer(serializers.ModelSerializer):
required=False,
allow_null=True,
)
tag = serializers.PrimaryKeyRelatedField(
queryset=BookTag.objects.all(),
required=False,
allow_null=True,
)
source_url = serializers.SerializerMethodField()
isbn = serializers.SerializerMethodField()
@ -1303,6 +1321,8 @@ class AdminBookReferenceDetailSerializer(serializers.ModelSerializer):
"subject_area_detail",
"type",
"type_detail",
"tag",
"tag_detail",
"order",
"city_of_publication",
"country_of_publication",
@ -1548,6 +1568,8 @@ class AdminTransmitterOriginalTextSerializer(serializers.ModelSerializer):
class AdminBookVolumeSerializer(serializers.ModelSerializer):
remove_image = serializers.BooleanField(write_only=True, required=False, default=False)
class Meta:
model = BookVolume
fields = [
@ -1555,10 +1577,23 @@ class AdminBookVolumeSerializer(serializers.ModelSerializer):
"book_reference",
"edition",
"title",
"image",
"remove_image",
"created_at",
"updated_at",
]
def create(self, validated_data):
validated_data.pop("remove_image", False)
return super().create(validated_data)
def update(self, instance, validated_data):
remove_image = validated_data.pop("remove_image", False)
if remove_image and instance.image:
instance.image.delete(save=False)
instance.image = None
return super().update(instance, validated_data)
class AdminBookResearcherSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
@ -1583,7 +1618,12 @@ class AdminBookEditionSerializer(serializers.ModelSerializer):
city_of_publication = serializers.JSONField(required=False, default=list)
country_of_publication = serializers.JSONField(required=False, default=list)
notes = serializers.JSONField(required=False, default=list)
tags = serializers.JSONField(required=False, default=list)
tag = serializers.PrimaryKeyRelatedField(
queryset=BookTag.objects.all(),
required=False,
allow_null=True,
)
tag_detail = AdminBookTagSerializer(source="tag", read_only=True)
volumes_detail = AdminBookVolumeSerializer(source="volumes", many=True, read_only=True)
number_of_volumes = serializers.SerializerMethodField()
researchers = AdminBookResearcherSerializer(many=True, required=False)
@ -1605,7 +1645,8 @@ class AdminBookEditionSerializer(serializers.ModelSerializer):
"source_url",
"isbn",
"notes",
"tags",
"tag",
"tag_detail",
"volumes_detail",
"researchers",
"editors",

13
apps/hadis/urls.py

@ -3,19 +3,19 @@ from rest_framework.routers import SimpleRouter
from .views.category import HadisCategorySectListView, HadisCategoryTreeView, CategoriesView, CategoriesBySectView, HadisCategorySelectBySectView, HadisCategorySelectBySectSourceView , HadisCategoryTreeNormalView ,test_deploy,debug_headers,HadisCategoryXMindView
from .views.hadis import (
HadisCollectionListView, HadisListView, HadisBasicView, HadisDetailView, HadisSyncView, HadisTransmittersView, HadisCorrectionsView,HadisMainListView, HadisFiltersView, HadisLayersView,PinnedHadisCollectionListView, HadisFiltersSyncAPIView,
HadisCorrectionDetailView, HadisInterpretationDetailView
HadisCorrectionDetailView, HadisInterpretationDetailView, HadisInterpretsListView
)
from .views.transmitter import (
TransmitterView ,TransmitterDetailView, TransmitterSyncView,TransmitterOpinionView, TransmitterOriginalTextView, TransmitterFiltersView,
NarratorArgumentsListView , NarratorTeachersView, NarratorStudentsView, TransmitterOriginalTextDetailView
)
from .views.reference import BookDetailView, BookReferencesView, BookReferenceSyncView, BookAttributeView
from .views.reference import BookDetailView, BookReferencesView, BookReferenceSyncView, BookAttributeView, ReferenceExcerptsListView
from .views.reference_v2 import BookReferenceV2DetailView, BookReferenceV2SyncView, BookReferenceV2ListView
from .views.version import ContentReleaseSyncView
from .views.info import HadisInfoView
from .views_admin import (
AdminHadisCategoryViewSet, AdminHadisViewSet, AdminHadisCorrectionViewSet, AdminHadisInterpretationViewSet, AdminHadisStatusViewSet, AdminHadisTagViewSet, AdminHadisCollectionViewSet,
AdminBookSubjectAreaViewSet, AdminBookTypeViewSet, AdminBookAuthorViewSet, AdminBookAttributeViewSet, AdminBookReferenceImageViewSet, AdminBookReferenceDocumentViewSet, AdminBookReferenceViewSet,
AdminBookSubjectAreaViewSet, AdminBookTypeViewSet, AdminBookTagViewSet, AdminBookAuthorViewSet, AdminBookAttributeViewSet, AdminBookReferenceImageViewSet, AdminBookReferenceDocumentViewSet, AdminBookReferenceViewSet,
AdminTransmitterViewSet, AdminTransmitterReliabilityViewSet, DovodiBookSubjectAreaViewSet, DovodiBookTypeViewSet,
DovodiNarratorLayerViewSet, DovodiTransmitterReliabilityViewSet, DovodiOpinionStatusViewSet,
DovodiHadisSectViewSet, DovodiHadisCategoryViewSet, AdminCategoryQuranVerseViewSet, AdminHadisTransmitterViewSet,
@ -43,6 +43,7 @@ admin_router.register(r'collections', AdminHadisCollectionViewSet, basename='adm
admin_router.register(r'references', AdminBookReferenceViewSet, basename='admin-book-references')
admin_router.register(r'subject-areas', AdminBookSubjectAreaViewSet, basename='admin-book-subject-areas')
admin_router.register(r'book-types', AdminBookTypeViewSet, basename='admin-book-types')
admin_router.register(r'book-tags', AdminBookTagViewSet, basename='admin-book-tags')
admin_router.register(r'dovodi-subject-areas', DovodiBookSubjectAreaViewSet, basename='dovodi-book-subject-areas')
admin_router.register(r'dovodi-book-types', DovodiBookTypeViewSet, basename='dovodi-book-types')
admin_router.register(r'book-authors', AdminBookAuthorViewSet, basename='admin-book-authors')
@ -112,9 +113,12 @@ urlpatterns = [
path('narrators/', TransmitterView.as_view(), name='narrators'),
# Reference paths
path('v2/references/<str:reference_slug>/excerpts/', ReferenceExcerptsListView.as_view(), name='reference-excerpts-v2'),
path('v2/references/<str:slug>/', BookReferenceV2DetailView.as_view(), name='reference-detail-v2'),
path('v2/references/', BookReferenceV2ListView.as_view(), name='reference-list-v2'),
path('references/<str:reference_slug>', BookDetailView.as_view(), name='reference-detail'),
path('references/<str:reference_slug>/excerpts/', ReferenceExcerptsListView.as_view(), name='reference-excerpts'),
path('references/<str:reference_slug>/', BookDetailView.as_view(), name='reference-detail'),
path('references/<str:reference_slug>', BookDetailView.as_view(), name='reference-detail-no-slash'),
path('references/', BookReferencesView.as_view(), name='references'),
# Hadis detail paths (with slug, more specific)
@ -122,6 +126,7 @@ urlpatterns = [
path('<str:hadis_slug>/transmitters/', HadisTransmittersView.as_view(), name='hadis-transmitters'),
path('<str:hadis_slug>/transmitters/layers/', HadisLayersView.as_view(), name='hadis-layers'),
path('<str:hadis_slug>/corrections/', HadisCorrectionsView.as_view(), name='hadis-corrections'),
path('<str:hadis_slug>/interprets/', HadisInterpretsListView.as_view(), name='hadis-interprets'),
path('<str:hadis_slug>/', HadisBasicView.as_view(), name='hadis-basic'), # ← Least specific LAST
# path('test/test-deploy',test_deploy , name='test'),

40
apps/hadis/views/hadis.py

@ -935,3 +935,43 @@ class HadisInterpretationDetailView(RetrieveAPIView):
self.check_object_permissions(self.request, obj)
return obj
class HadisInterpretsListView(ListAPIView):
"""
API View to list all interpretations for the category of a specific Hadis.
"""
serializer_class = HadisInterpretationDetailSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [AllowAny]
pagination_class = StandardResultsSetPagination
lookup_field = 'slug'
lookup_url_kwarg = 'hadis_slug'
@swagger_auto_schema(
operation_summary="Get Hadis Interpretations",
operation_description="Returns a paginated list of all interpretations registered for the category of this hadis, with full details and references.",
tags=['Dobodbi - Hadis (V2)'],
)
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def get_queryset(self):
hadis_slug = self.kwargs.get('hadis_slug')
try:
hadis = Hadis.objects.select_related('category').get(slug=hadis_slug, status=True)
if not hadis.category_id:
return HadisInterpretation.objects.none()
return HadisInterpretation.objects.filter(
category_id=hadis.category_id
).select_related(
'category'
).prefetch_related(
'references__book_reference__author',
'references__edition',
'references__book_volume',
'references__images'
).order_by('priority', 'id')
except Hadis.DoesNotExist:
return HadisInterpretation.objects.none()

52
apps/hadis/views/reference.py

@ -1,10 +1,12 @@
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.generics import ListAPIView, RetrieveAPIView, ListCreateAPIView
from rest_framework.response import Response
from django.db.models import Q
from ..models import BookReference , BookAuthor , BookReferenceImage, BookAttribute
from drf_yasg.utils import swagger_auto_schema
from ..models import BookReference, BookAuthor, BookReferenceImage, BookAttribute, Hadis
from ..serializers.reference import BookAuthorSerializer, BookDetailSerializer, BookReferenceSerializer, BookReferenceSyncSerializer, BookAttributeSerializer
from ..serializers.hadis import HadisListSerializer
from ..serializers.category import get_localized_text
from ..docs import book_attributes_create_swagger, book_attributes_list_swagger, book_references_list_swagger, book_authors_list_swagger, book_detail_swagger, reference_sync_swagger
from utils.pagination import NoPagination
@ -218,3 +220,49 @@ class BookAttributeView(ListCreateAPIView):
return self.create(request, *args, **kwargs)
class ReferenceExcerptsListView(ListAPIView):
"""
API view to retrieve all hadiths associated with a specific book reference (excerpts)
"""
serializer_class = HadisListSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [AllowAny]
pagination_class = StandardResultsSetPagination
lookup_field = 'slug'
lookup_url_kwarg = 'reference_slug'
@swagger_auto_schema(
operation_summary="Get Reference Excerpts (Hadiths)",
operation_description="Returns a paginated list of all hadiths connected to this book reference.",
tags=['Dobodbi - Hadis (V2)'],
)
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def get_queryset(self):
reference_slug = self.kwargs.get('reference_slug')
return Hadis.objects.filter(
references__book_reference__slug=reference_slug,
status=True
).select_related(
'category__sect',
'hadis_status'
).distinct().order_by('id')
def get_serializer_context(self):
context = super().get_serializer_context()
user = self.request.user
if user.is_authenticated:
from apps.bookmark.models.bookmark import Bookmark
user_bookmarks = Bookmark.objects.filter(
user=user,
service=Bookmark.ServiceChoices.HADITH,
status=True
).values_list('content_id', flat=True)
context['user_bookmarked_hadis_ids'] = set(user_bookmarks)
else:
context['user_bookmarked_hadis_ids'] = set()
return context

2
apps/hadis/views/reference_v2.py

@ -156,7 +156,7 @@ class BookReferenceV2ListView(generics.ListAPIView):
return values
def get_queryset(self):
qs = BookReference.objects.select_related('author').prefetch_related('tags', 'volumes__images').order_by('-created_at')
qs = BookReference.objects.select_related('author', 'tag').prefetch_related('volumes__images').order_by('-created_at')
search = self.request.query_params.get('search')
if search:

19
apps/hadis/views_admin.py

@ -11,7 +11,7 @@ from utils.pagination import StandardResultsSetPagination, OptionalPagination
from .models import (
Hadis, HadisCategory, HadisCollection, HadisInCollection, HadisCorrection, HadisInterpretation, HadisStatus, HadisTag, HadisSect, HadisTransmitter,
BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookEdition, BookVolume,
BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume,
Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, TransmitterOpinion, TransmitterOriginalText
)
from .serializers.serializers_admin import (
@ -29,6 +29,7 @@ from .serializers.serializers_admin import (
AdminHadisTagSerializer,
AdminBookSubjectAreaSerializer,
AdminBookTypeSerializer,
AdminBookTagSerializer,
AdminBookAuthorSerializer,
AdminBookAttributeSerializer,
AdminBookReferenceImageSerializer,
@ -770,11 +771,27 @@ class AdminBookEditionViewSet(ModelViewSet):
return queryset.order_by("-created_at")
class AdminBookTagViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
serializer_class = AdminBookTagSerializer
pagination_class = None
parser_classes = (JSONParser, MultiPartParser, FormParser)
def get_queryset(self):
queryset = BookTag.objects.all()
search_query = self.request.query_params.get("search")
if search_query:
queryset = queryset.filter(title__icontains=search_query)
return queryset.order_by("-id")
class AdminBookVolumeViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
serializer_class = AdminBookVolumeSerializer
pagination_class = None
parser_classes = (MultiPartParser, FormParser, JSONParser)
def get_queryset(self):
queryset = BookVolume.objects.all()

Loading…
Cancel
Save