Browse Source

feat(hadis): support source image upload, newest-first category ordering, and sync layer description

master
Mohsen Taba 3 weeks ago
parent
commit
3c692f128f
  1. 32
      apps/hadis/models/reference.py
  2. 1
      apps/hadis/serializers/hadis.py
  3. 155
      apps/hadis/serializers/serializers_admin.py
  4. 10
      apps/hadis/views_admin.py

32
apps/hadis/models/reference.py

@ -10,8 +10,15 @@ class BookSubjectArea(models.Model):
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:
ordering = ('-created_at', '-id')
verbose_name = _('Subject Area')
verbose_name_plural = _('Subject Areas')
def __str__(self):
return self.title[0]['text'] if self.title else str(self.id)
if self.title and isinstance(self.title, list) and len(self.title) > 0 and isinstance(self.title[0], dict):
return self.title[0].get('text') or self.title[0].get('title') or str(self.id)
return str(self.id)
def get_title(self,lang):
"""
@ -23,11 +30,11 @@ class BookSubjectArea(models.Model):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('text', '') or tr.get('title', '')
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('text', '') or tr.get('title', '')
return None
@ -44,10 +51,9 @@ class BookTag(models.Model):
verbose_name_plural = _('Book Tags')
def __str__(self):
try:
return self.title[0]['text']
except:
return f"Tag {self.id}"
if self.title and isinstance(self.title, list) and len(self.title) > 0 and isinstance(self.title[0], dict):
return self.title[0].get('text') or self.title[0].get('title') or f"Tag {self.id}"
return f"Tag {self.id}"
class BookType(models.Model):
class Sect(models.TextChoices):
@ -65,7 +71,9 @@ class BookType(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self):
return self.title[0]['text'] if self.title else str(self.id)
if self.title and isinstance(self.title, list) and len(self.title) > 0 and isinstance(self.title[0], dict):
return self.title[0].get('text') or self.title[0].get('title') or str(self.id)
return str(self.id)
def get_title(self,lang):
"""
@ -77,11 +85,11 @@ class BookType(models.Model):
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
return tr.get('text', '') or tr.get('title', '')
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return tr.get('text', '') or tr.get('title', '')
return None
class BookReference(LowercaseSlugMixin, models.Model):
@ -145,7 +153,9 @@ class BookReference(LowercaseSlugMixin, models.Model):
ordering = ('-created_at',)
def __str__(self):
return self.title[0]['text'] if self.title else "Untitled Reference"
if self.title and isinstance(self.title, list) and len(self.title) > 0 and isinstance(self.title[0], dict):
return self.title[0].get('text') or self.title[0].get('title') or f"Reference {self.id}"
return f"Reference {self.id}"
def _get_json_field(self, field_name: str, lang: Optional[str]=None , fallback: str = "en"):
"""

1
apps/hadis/serializers/hadis.py

@ -198,6 +198,7 @@ class HadisSyncSerializer(serializers.ModelSerializer):
'reliability': reliability_block,
'layer_level': layer.number if layer else None,
'layer_name': get_localized_text(layer.name, request) if layer else None,
'layer_description': get_localized_text(layer.description, request) if layer else None,
'is_gap': transmitter_rel.is_gap,
'birth_year_hijri': t.birth_year_hijri,
'death_year_hijri': t.death_year_hijri,

155
apps/hadis/serializers/serializers_admin.py

@ -20,7 +20,9 @@ def safe_copy_data(data):
if hasattr(data, "lists"):
new_data = {}
for key, values in data.lists():
if key in ["delete_image_ids", "uploaded_images"]:
if key in ["subject_area", "authors", "book_references", "delete_image_ids", "uploaded_images"] or key.endswith("[]"):
new_data[key] = values
elif len(values) > 1:
new_data[key] = values
else:
new_data[key] = values[0] if values else None
@ -1220,6 +1222,8 @@ class AdminBookReferenceListSerializer(serializers.ModelSerializer):
volume = serializers.SerializerMethodField()
publisher = serializers.SerializerMethodField()
year_of_publication = serializers.SerializerMethodField()
thumbnail = serializers.SerializerMethodField()
image = serializers.SerializerMethodField()
class Meta:
model = BookReference
@ -1236,9 +1240,29 @@ class AdminBookReferenceListSerializer(serializers.ModelSerializer):
"publisher",
"year_of_publication",
"rate",
"thumbnail",
"image",
"created_at",
]
def get_thumbnail(self, obj):
first_img = obj.images.filter(book_volume__isnull=True).order_by("order", "id").first()
if first_img and first_img.image:
request = self.context.get("request")
if request:
return absolute_https_url(first_img.image.url, request)
return absolute_https_url(first_img.image.url)
first_vol = obj.volumes.filter(image__isnull=False).exclude(image="").first()
if first_vol and first_vol.image:
request = self.context.get("request")
if request:
return absolute_https_url(first_vol.image.url, request)
return absolute_https_url(first_vol.image.url)
return None
def get_image(self, obj):
return self.get_thumbnail(obj)
def get_volume(self, obj):
vols = list(obj.volumes.all())
return vols[0].title if vols else ""
@ -1289,6 +1313,29 @@ class AdminBookReferenceDetailSerializer(serializers.ModelSerializer):
country_of_publication = serializers.SerializerMethodField()
edition_number = serializers.SerializerMethodField()
thumbnail = serializers.SerializerMethodField()
image = serializers.SerializerMethodField()
remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False)
remove_image = serializers.BooleanField(write_only=True, required=False, default=False)
def get_thumbnail(self, obj):
first_img = obj.images.filter(book_volume__isnull=True).order_by("order", "id").first()
if first_img and first_img.image:
request = self.context.get("request")
if request:
return absolute_https_url(first_img.image.url, request)
return absolute_https_url(first_img.image.url)
first_vol = obj.volumes.filter(image__isnull=False).exclude(image="").first()
if first_vol and first_vol.image:
request = self.context.get("request")
if request:
return absolute_https_url(first_vol.image.url, request)
return absolute_https_url(first_vol.image.url)
return None
def get_image(self, obj):
return self.get_thumbnail(obj)
def get_source_url(self, obj):
ed = obj.editions.first()
return ed.source_url if ed else ""
@ -1355,27 +1402,133 @@ class AdminBookReferenceDetailSerializer(serializers.ModelSerializer):
"rate",
"author",
"author_detail",
"thumbnail",
"image",
"remove_thumbnail",
"remove_image",
"created_at",
"updated_at",
"share_link",
]
read_only_fields = ["id", "slug", "created_at", "updated_at", "share_link"]
def to_internal_value(self, data):
import json
mutable_data = safe_copy_data(data)
for json_field in ["title", "description", "language", "publisher", "city_of_publication", "country_of_publication"]:
if json_field in mutable_data:
val = mutable_data.get(json_field)
if isinstance(val, str):
try:
mutable_data[json_field] = json.loads(val)
except ValueError:
pass
for list_field in ["subject_area", "authors"]:
if list_field in mutable_data:
val = mutable_data.get(list_field)
if isinstance(val, str):
try:
parsed = json.loads(val)
mutable_data[list_field] = parsed if isinstance(parsed, list) else [parsed]
except ValueError:
if val.isdigit():
mutable_data[list_field] = [int(val)]
elif val.strip():
mutable_data[list_field] = [int(v.strip()) for v in val.split(",") if v.strip().isdigit()]
else:
mutable_data[list_field] = []
elif isinstance(val, int):
mutable_data[list_field] = [val]
elif isinstance(val, list):
cleaned_list = []
for item in val:
if isinstance(item, str):
try:
item_parsed = json.loads(item)
if isinstance(item_parsed, list):
cleaned_list.extend(item_parsed)
else:
cleaned_list.append(item_parsed)
except ValueError:
cleaned_list.append(int(item) if item.isdigit() else item)
else:
cleaned_list.append(item)
mutable_data[list_field] = cleaned_list
for int_field in ["number_page", "order", "type", "tag", "author"]:
if int_field in mutable_data:
val = mutable_data.get(int_field)
if val == "" or val == "null" or val == "undefined":
mutable_data[int_field] = None
if "rate" in mutable_data:
val = mutable_data.get("rate")
if val == "" or val == "null" or val == "undefined":
mutable_data["rate"] = None
if "has_editions" in mutable_data:
val = mutable_data.get("has_editions")
if isinstance(val, str):
mutable_data["has_editions"] = val.lower() in ["true", "1"]
return super().to_internal_value(mutable_data)
def create(self, validated_data):
subject_area = validated_data.pop("subject_area", [])
validated_data.pop("remove_thumbnail", False)
validated_data.pop("remove_image", False)
request = self.context.get("request")
thumbnail = None
if request and hasattr(request, "FILES"):
thumbnail = request.FILES.get("thumbnail") or request.FILES.get("image")
book = super().create(validated_data)
if subject_area:
book.subject_area.set(subject_area)
if thumbnail:
BookReferenceImage.objects.create(
book_reference=book,
book_volume=None,
image=thumbnail,
order=0,
)
return book
def update(self, instance, validated_data):
subject_area = validated_data.pop("subject_area", None)
remove_thumbnail = validated_data.pop("remove_thumbnail", False) or validated_data.pop("remove_image", False)
request = self.context.get("request")
thumbnail = None
if request and hasattr(request, "FILES"):
thumbnail = request.FILES.get("thumbnail") or request.FILES.get("image")
book = super().update(instance, validated_data)
if subject_area is not None:
book.subject_area.set(subject_area)
if remove_thumbnail:
instance.images.filter(book_volume__isnull=True, order=0).delete()
if thumbnail:
main_img = instance.images.filter(book_volume__isnull=True, order=0).first()
if main_img:
main_img.image = thumbnail
main_img.save()
else:
BookReferenceImage.objects.create(
book_reference=instance,
book_volume=None,
image=thumbnail,
order=0,
)
return book
# Category Quran Verse Admin Serializers
from ..models.category import CategoryQuranVerse, QuranSecondaryTranslation

10
apps/hadis/views_admin.py

@ -475,7 +475,7 @@ class AdminBookSubjectAreaViewSet(ReadOnlyModelViewSet):
pagination_class = None
def get_queryset(self):
return BookSubjectArea.objects.all().order_by("id")
return BookSubjectArea.objects.all().order_by("-created_at", "-id")
class AdminBookTypeViewSet(ReadOnlyModelViewSet):
@ -485,7 +485,7 @@ class AdminBookTypeViewSet(ReadOnlyModelViewSet):
pagination_class = None
def get_queryset(self):
return BookType.objects.all().order_by("id")
return BookType.objects.all().order_by("-created_at", "-id")
class DovodiBookSubjectAreaViewSet(ModelViewSet):
@ -500,7 +500,7 @@ class DovodiBookSubjectAreaViewSet(ModelViewSet):
search_query = self.request.query_params.get("search")
if search_query:
queryset = queryset.filter(title__icontains=search_query)
return queryset.order_by("id")
return queryset.order_by("-created_at", "-id")
class DovodiBookTypeViewSet(ModelViewSet):
@ -515,7 +515,7 @@ class DovodiBookTypeViewSet(ModelViewSet):
search_query = self.request.query_params.get("search")
if search_query:
queryset = queryset.filter(title__icontains=search_query)
return queryset.order_by("id")
return queryset.order_by("-created_at", "-id")
class AdminBookAuthorViewSet(ModelViewSet):
@ -530,7 +530,7 @@ class AdminBookAuthorViewSet(ModelViewSet):
search_query = self.request.query_params.get("search")
if search_query:
queryset = queryset.filter(name__icontains=search_query)
return queryset.order_by("id")
return queryset.order_by("-created_at", "-id")
@action(detail=True, methods=["post"], url_path="assign-references")
def assign_references(self, request, pk=None):

Loading…
Cancel
Save