3 changed files with 283 additions and 2 deletions
@ -0,0 +1,186 @@ |
|||||
|
from rest_framework import serializers |
||||
|
|
||||
|
from .models import Article, ArticleCategory, ArticleContent, ContentPart, TextSection |
||||
|
|
||||
|
|
||||
|
class AbsoluteImageField(serializers.ImageField): |
||||
|
def to_representation(self, value): |
||||
|
if not value: |
||||
|
return None |
||||
|
request = self.context.get("request") |
||||
|
url = value.url if hasattr(value, "url") else str(value) |
||||
|
if request: |
||||
|
return request.build_absolute_uri(url) |
||||
|
return url |
||||
|
|
||||
|
|
||||
|
class AbsoluteFileField(serializers.FileField): |
||||
|
def to_representation(self, value): |
||||
|
if not value: |
||||
|
return None |
||||
|
request = self.context.get("request") |
||||
|
url = value.url if hasattr(value, "url") else str(value) |
||||
|
if request: |
||||
|
return request.build_absolute_uri(url) |
||||
|
return url |
||||
|
|
||||
|
|
||||
|
class AdminArticleCategorySerializer(serializers.ModelSerializer): |
||||
|
class Meta: |
||||
|
model = ArticleCategory |
||||
|
fields = ["id", "title", "slug", "status", "order"] |
||||
|
|
||||
|
|
||||
|
class AdminArticleListSerializer(serializers.ModelSerializer): |
||||
|
thumbnail = AbsoluteImageField(required=False, allow_null=True) |
||||
|
article_file = AbsoluteFileField(required=False, allow_null=True) |
||||
|
categories = AdminArticleCategorySerializer(many=True, read_only=True) |
||||
|
|
||||
|
class Meta: |
||||
|
model = Article |
||||
|
fields = [ |
||||
|
"id", |
||||
|
"title", |
||||
|
"slug", |
||||
|
"thumbnail", |
||||
|
"description", |
||||
|
"article_file", |
||||
|
"view_count", |
||||
|
"download_count", |
||||
|
"status", |
||||
|
"created_at", |
||||
|
"updated_at", |
||||
|
"categories", |
||||
|
] |
||||
|
read_only_fields = ["id", "view_count", "download_count", "created_at", "updated_at"] |
||||
|
|
||||
|
|
||||
|
class AdminArticleDetailSerializer(serializers.ModelSerializer): |
||||
|
thumbnail = AbsoluteImageField(required=False, allow_null=True) |
||||
|
article_file = AbsoluteFileField(required=False, allow_null=True) |
||||
|
categories = serializers.PrimaryKeyRelatedField( |
||||
|
queryset=ArticleCategory.objects.all(), |
||||
|
many=True, |
||||
|
required=False, |
||||
|
) |
||||
|
categories_detail = AdminArticleCategorySerializer(source="categories", many=True, read_only=True) |
||||
|
remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False) |
||||
|
remove_article_file = serializers.BooleanField(write_only=True, required=False, default=False) |
||||
|
|
||||
|
class Meta: |
||||
|
model = Article |
||||
|
fields = [ |
||||
|
"id", |
||||
|
"title", |
||||
|
"slug", |
||||
|
"thumbnail", |
||||
|
"description", |
||||
|
"content", |
||||
|
"article_file", |
||||
|
"view_count", |
||||
|
"download_count", |
||||
|
"status", |
||||
|
"created_at", |
||||
|
"updated_at", |
||||
|
"categories", |
||||
|
"categories_detail", |
||||
|
"embedded_in", |
||||
|
"remove_thumbnail", |
||||
|
"remove_article_file", |
||||
|
] |
||||
|
read_only_fields = ["id", "view_count", "download_count", "created_at", "updated_at"] |
||||
|
|
||||
|
def create(self, validated_data): |
||||
|
validated_data.pop("remove_thumbnail", False) |
||||
|
validated_data.pop("remove_article_file", False) |
||||
|
categories = validated_data.pop("categories", []) |
||||
|
article = super().create(validated_data) |
||||
|
if categories: |
||||
|
article.categories.set(categories) |
||||
|
return article |
||||
|
|
||||
|
def update(self, instance, validated_data): |
||||
|
remove_thumbnail = validated_data.pop("remove_thumbnail", False) |
||||
|
remove_article_file = validated_data.pop("remove_article_file", False) |
||||
|
categories = validated_data.pop("categories", None) |
||||
|
|
||||
|
if remove_thumbnail and instance.thumbnail: |
||||
|
instance.thumbnail.delete(save=False) |
||||
|
instance.thumbnail = None |
||||
|
|
||||
|
if remove_article_file and instance.article_file: |
||||
|
instance.article_file.delete(save=False) |
||||
|
instance.article_file = None |
||||
|
|
||||
|
article = super().update(instance, validated_data) |
||||
|
if categories is not None: |
||||
|
article.categories.set(categories) |
||||
|
return article |
||||
|
|
||||
|
|
||||
|
class AdminTextSectionSerializer(serializers.ModelSerializer): |
||||
|
id = serializers.IntegerField(required=False) |
||||
|
|
||||
|
class Meta: |
||||
|
model = TextSection |
||||
|
fields = ["id", "arabic_text", "translation", "order"] |
||||
|
|
||||
|
|
||||
|
class AdminContentPartSerializer(serializers.ModelSerializer): |
||||
|
id = serializers.IntegerField(required=False) |
||||
|
text_sections = AdminTextSectionSerializer(many=True, required=False) |
||||
|
|
||||
|
class Meta: |
||||
|
model = ContentPart |
||||
|
fields = ["id", "order", "text_sections"] |
||||
|
|
||||
|
|
||||
|
class AdminArticleContentSerializer(serializers.ModelSerializer): |
||||
|
id = serializers.IntegerField(required=False) |
||||
|
parts = AdminContentPartSerializer(many=True, required=False) |
||||
|
|
||||
|
class Meta: |
||||
|
model = ArticleContent |
||||
|
fields = [ |
||||
|
"id", |
||||
|
"article", |
||||
|
"title", |
||||
|
"content", |
||||
|
"priority", |
||||
|
"status", |
||||
|
"created_at", |
||||
|
"updated_at", |
||||
|
"parts", |
||||
|
] |
||||
|
read_only_fields = ["created_at", "updated_at"] |
||||
|
|
||||
|
def _sync_parts(self, article_content, parts_data): |
||||
|
article_content.parts.all().delete() |
||||
|
|
||||
|
for part_index, part_data in enumerate(parts_data, start=1): |
||||
|
text_sections_data = part_data.pop("text_sections", []) |
||||
|
part = ContentPart.objects.create( |
||||
|
article_content=article_content, |
||||
|
order=part_data.get("order") or part_index, |
||||
|
) |
||||
|
|
||||
|
for section_index, section_data in enumerate(text_sections_data, start=1): |
||||
|
TextSection.objects.create( |
||||
|
content_part=part, |
||||
|
arabic_text=section_data.get("arabic_text", ""), |
||||
|
translation=section_data.get("translation", ""), |
||||
|
order=section_data.get("order") or section_index, |
||||
|
) |
||||
|
|
||||
|
def create(self, validated_data): |
||||
|
parts_data = validated_data.pop("parts", []) |
||||
|
article_content = super().create(validated_data) |
||||
|
self._sync_parts(article_content, parts_data) |
||||
|
return article_content |
||||
|
|
||||
|
def update(self, instance, validated_data): |
||||
|
parts_data = validated_data.pop("parts", None) |
||||
|
article_content = super().update(instance, validated_data) |
||||
|
if parts_data is not None: |
||||
|
self._sync_parts(article_content, parts_data) |
||||
|
return article_content |
||||
@ -0,0 +1,87 @@ |
|||||
|
from django.db.models import Q |
||||
|
from rest_framework.authentication import TokenAuthentication |
||||
|
from rest_framework.decorators import action |
||||
|
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser |
||||
|
from rest_framework.permissions import IsAuthenticated |
||||
|
from rest_framework.response import Response |
||||
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet |
||||
|
|
||||
|
from apps.account.permissions import IsSuperAdmin |
||||
|
from utils.pagination import StandardResultsSetPagination |
||||
|
|
||||
|
from .models import Article, ArticleCategory, ArticleContent |
||||
|
from .serializers_admin import ( |
||||
|
AdminArticleCategorySerializer, |
||||
|
AdminArticleContentSerializer, |
||||
|
AdminArticleDetailSerializer, |
||||
|
AdminArticleListSerializer, |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class AdminArticleCategoryViewSet(ReadOnlyModelViewSet): |
||||
|
serializer_class = AdminArticleCategorySerializer |
||||
|
permission_classes = [IsAuthenticated, IsSuperAdmin] |
||||
|
authentication_classes = [TokenAuthentication] |
||||
|
pagination_class = None |
||||
|
|
||||
|
def get_queryset(self): |
||||
|
return ArticleCategory.objects.all().order_by("order", "title") |
||||
|
|
||||
|
|
||||
|
class AdminArticleViewSet(ModelViewSet): |
||||
|
permission_classes = [IsAuthenticated, IsSuperAdmin] |
||||
|
authentication_classes = [TokenAuthentication] |
||||
|
pagination_class = StandardResultsSetPagination |
||||
|
parser_classes = (MultiPartParser, FormParser, JSONParser) |
||||
|
|
||||
|
def get_serializer_class(self): |
||||
|
if self.action == "list": |
||||
|
return AdminArticleListSerializer |
||||
|
return AdminArticleDetailSerializer |
||||
|
|
||||
|
def get_queryset(self): |
||||
|
queryset = Article.objects.all().prefetch_related("categories") |
||||
|
|
||||
|
search_query = self.request.query_params.get("search") |
||||
|
if search_query: |
||||
|
queryset = queryset.filter( |
||||
|
Q(title__icontains=search_query) |
||||
|
| Q(slug__icontains=search_query) |
||||
|
| Q(description__icontains=search_query) |
||||
|
| Q(content__icontains=search_query) |
||||
|
) |
||||
|
|
||||
|
category_id = self.request.query_params.get("category") |
||||
|
if category_id and category_id != "all": |
||||
|
queryset = queryset.filter(categories__id=category_id) |
||||
|
|
||||
|
status_filter = self.request.query_params.get("status") |
||||
|
if status_filter in {"true", "false"}: |
||||
|
queryset = queryset.filter(status=status_filter == "true") |
||||
|
|
||||
|
return queryset.distinct().order_by("-created_at") |
||||
|
|
||||
|
|
||||
|
class AdminArticleContentViewSet(ModelViewSet): |
||||
|
serializer_class = AdminArticleContentSerializer |
||||
|
permission_classes = [IsAuthenticated, IsSuperAdmin] |
||||
|
authentication_classes = [TokenAuthentication] |
||||
|
parser_classes = (JSONParser, MultiPartParser, FormParser) |
||||
|
|
||||
|
def get_queryset(self): |
||||
|
queryset = ArticleContent.objects.all().prefetch_related("parts__text_sections") |
||||
|
article_id = self.request.query_params.get("article_id") |
||||
|
if article_id: |
||||
|
queryset = queryset.filter(article_id=article_id) |
||||
|
return queryset.order_by("priority", "created_at") |
||||
|
|
||||
|
@action(detail=False, methods=["post"]) |
||||
|
def reorder(self, request): |
||||
|
content_ids = request.data.get("content_ids", []) |
||||
|
if not content_ids: |
||||
|
return Response({"error": "content_ids list is required"}, status=400) |
||||
|
|
||||
|
for index, content_id in enumerate(content_ids, start=1): |
||||
|
ArticleContent.objects.filter(id=content_id).update(priority=index) |
||||
|
|
||||
|
return Response({"status": "success"}) |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue