diff --git a/apps/blog/serializers_admin.py b/apps/blog/serializers_admin.py new file mode 100644 index 0000000..8ec1f92 --- /dev/null +++ b/apps/blog/serializers_admin.py @@ -0,0 +1,73 @@ +from rest_framework import serializers +from .models import Blog, BlogContent, BlogSeo +from utils import absolute_url + + +# ─── Helper: return full URL for an ImageField ──────────────────────────────── +class AbsoluteImageField(serializers.ImageField): + """ + Wraps DRF's ImageField to return a full absolute URL on read, + while accepting a real uploaded file (InMemoryUploadedFile / + TemporaryUploadedFile) on write – just like a normal 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 + + +# ─── Serializers ────────────────────────────────────────────────────────────── + +class AdminBlogSeoSerializer(serializers.ModelSerializer): + class Meta: + model = BlogSeo + fields = ["id", "title", "keywords", "description", "language"] + + +class AdminBlogContentSerializer(serializers.ModelSerializer): + image = AbsoluteImageField(required=False, allow_null=True) + + class Meta: + model = BlogContent + fields = [ + "id", "blog", "title", "content", "slug", "image", "order", + "created_at", "updated_at", + ] + + +class AdminBlogListSerializer(serializers.ModelSerializer): + """ + Lightweight serializer for the admin data table. + Exposes raw JSON fields so the React frontend can manage translations. + """ + thumbnail = AbsoluteImageField(required=False, allow_null=True) + + class Meta: + model = Blog + fields = [ + "id", "title", "slogan", "summary", "slug", "thumbnail", + "views_count", "created_at", "updated_at", + ] + read_only_fields = ["id", "views_count", "created_at", "updated_at"] + + +class AdminBlogDetailSerializer(serializers.ModelSerializer): + """ + Full serializer for the admin detail/edit view. + Includes nested contents and SEO data. + """ + thumbnail = AbsoluteImageField(required=False, allow_null=True) + contents = AdminBlogContentSerializer(many=True, read_only=True) + seos = AdminBlogSeoSerializer(many=True, read_only=True) + + class Meta: + model = Blog + fields = [ + "id", "title", "slogan", "summary", "slug", "thumbnail", + "views_count", "created_at", "updated_at", "contents", "seos", + ] + read_only_fields = ["id", "views_count", "created_at", "updated_at"] \ No newline at end of file diff --git a/apps/blog/urls.py b/apps/blog/urls.py index 64de8f3..fc9d95d 100644 --- a/apps/blog/urls.py +++ b/apps/blog/urls.py @@ -1,8 +1,18 @@ -from django.urls import path, re_path +from django.urls import path, re_path , include +from rest_framework.routers import DefaultRouter + from .views import BlogListAPIView, RelatedBlogsAPIView, BlogDetailBySlugAPIView +from .views_admin import AdminBlogViewSet, AdminBlogContentViewSet, AdminBlogSeoViewSet + app_name = 'blog' +# --- Admin Router Setup --- +admin_router = DefaultRouter() +admin_router.register(r'blogs', AdminBlogViewSet, basename='admin-blogs') +admin_router.register(r'contents', AdminBlogContentViewSet, basename='admin-blog-contents') +admin_router.register(r'seo', AdminBlogSeoViewSet, basename='admin-blog-seo') + urlpatterns = [ # Blog list with search and sort_by filters path('list/', BlogListAPIView.as_view(), name='blog-list'), @@ -14,6 +24,8 @@ urlpatterns = [ re_path(r'^detail/(?P[\w\-\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u200C\u200D]+)/$', BlogDetailBySlugAPIView.as_view(), name='blog-detail'), + + path('admin/', include(admin_router.urls)), ] diff --git a/apps/blog/views_admin.py b/apps/blog/views_admin.py new file mode 100644 index 0000000..b10fc28 --- /dev/null +++ b/apps/blog/views_admin.py @@ -0,0 +1,79 @@ +from django.db.models import Q +from rest_framework.viewsets import ModelViewSet +from rest_framework.permissions import IsAuthenticated, IsAdminUser +from rest_framework.authentication import TokenAuthentication +from rest_framework.parsers import MultiPartParser, FormParser, JSONParser + +from .models import Blog, BlogContent, BlogSeo +from .serializers_admin import ( + AdminBlogListSerializer, + AdminBlogDetailSerializer, + AdminBlogContentSerializer, + AdminBlogSeoSerializer +) + +class AdminBlogViewSet(ModelViewSet): + """ + Admin CRUD ViewSet for Blog management. + """ + permission_classes = [IsAuthenticated, IsAdminUser] + authentication_classes = [TokenAuthentication] + # Allow multipart so admins can upload thumbnail images directly + parser_classes = (MultiPartParser, FormParser, JSONParser) + + def get_serializer_class(self): + if self.action == 'list': + return AdminBlogListSerializer + return AdminBlogDetailSerializer + + def get_queryset(self): + queryset = Blog.objects.all().prefetch_related('contents', 'seos') + + # Custom Search handling for JSON fields + search_query = self.request.query_params.get('search', None) + if search_query: + # Note: Searching inside a JSON array of dicts in Django can be tricky + # depending on your DB (PostgreSQL vs SQLite). + # This is a safe fallback using text casting for SQLite/PG compatibility. + queryset = queryset.filter( + Q(title__icontains=search_query) | + Q(slogan__icontains=search_query) + ) + + return queryset.order_by('-created_at') + +class AdminBlogContentViewSet(ModelViewSet): + """ + Admin CRUD ViewSet for managing the content blocks inside a specific blog. + """ + serializer_class = AdminBlogContentSerializer + permission_classes = [IsAuthenticated, IsAdminUser] + authentication_classes = [TokenAuthentication] + parser_classes = (MultiPartParser, FormParser, JSONParser) + + def get_queryset(self): + queryset = BlogContent.objects.all() + + # Filter contents by blog_id if provided in the URL query params + blog_id = self.request.query_params.get('blog_id', None) + if blog_id: + queryset = queryset.filter(blog_id=blog_id) + + return queryset.order_by('order', 'created_at') + +class AdminBlogSeoViewSet(ModelViewSet): + """ + Admin CRUD ViewSet for managing SEO meta tags for a specific blog. + """ + serializer_class = AdminBlogSeoSerializer + permission_classes = [IsAuthenticated, IsAdminUser] + authentication_classes = [TokenAuthentication] + + def get_queryset(self): + queryset = BlogSeo.objects.all() + + blog_id = self.request.query_params.get('blog_id', None) + if blog_id: + queryset = queryset.filter(blog_id=blog_id) + + return queryset \ No newline at end of file