From 541c42bc2db5f20225d03df9bf759d2cabec3731 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Mon, 8 Jun 2026 11:40:01 +0330 Subject: [PATCH] articles views and serializers added for dashboard --- apps/article/serializers_admin.py | 186 ++++++++++++++++++++++++++++++ apps/article/urls.py | 12 +- apps/article/views_admin.py | 87 ++++++++++++++ 3 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 apps/article/serializers_admin.py create mode 100644 apps/article/views_admin.py diff --git a/apps/article/serializers_admin.py b/apps/article/serializers_admin.py new file mode 100644 index 0000000..b2f01c7 --- /dev/null +++ b/apps/article/serializers_admin.py @@ -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 diff --git a/apps/article/urls.py b/apps/article/urls.py index c8ebfa3..f47f315 100755 --- a/apps/article/urls.py +++ b/apps/article/urls.py @@ -1,8 +1,15 @@ -from django.urls import path, re_path +from django.urls import include, path, re_path +from rest_framework.routers import SimpleRouter from .views import * +from .views_admin import AdminArticleCategoryViewSet, AdminArticleContentViewSet, AdminArticleViewSet app_name = 'article' +admin_router = SimpleRouter() +admin_router.register(r'articles', AdminArticleViewSet, basename='admin-articles') +admin_router.register(r'categories', AdminArticleCategoryViewSet, basename='admin-article-categories') +admin_router.register(r'contents', AdminArticleContentViewSet, basename='admin-article-contents') + urlpatterns = [ path('categories/', ArticleCategoryListAPIView.as_view(), name='category-list'), path('pinned-collections/', PinnedArticleCollectionListView.as_view(), name='pinned-collection-list'), @@ -10,8 +17,9 @@ urlpatterns = [ path('list/', ArticleListAPIView.as_view(), name='podcast-list'), re_path(r'detail/(?P[\w-]+)/$', ArticleDetailAPIView.as_view(), name='podcast-detail'), + path('admin/', include(admin_router.urls)), # # User playlist endpoints # path('user-playlist/', UserPlaylistCreateAPIView.as_view(), name='user-playlist-create'), # path('user-playlist/list/', UserPlaylistListAPIView.as_view(), name='user-playlist-list'), -] \ No newline at end of file +] diff --git a/apps/article/views_admin.py b/apps/article/views_admin.py new file mode 100644 index 0000000..554b379 --- /dev/null +++ b/apps/article/views_admin.py @@ -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"})