3 changed files with 165 additions and 1 deletions
@ -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"] |
||||
@ -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 |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue