You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

492 lines
18 KiB

from django.contrib.auth import get_user_model
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from drf_spectacular.utils import extend_schema_field
from apps.cms.models.post import (
PostCategory,
PostTag,
Post,
PostRevision,
PostComment,
PostLike,
MediaAsset,
)
from apps.profiles.models.institution import Institution
from apps.cms.permissions import can_publish_or_moderate_post
User = get_user_model()
class PostCategorySerializer(serializers.ModelSerializer):
posts_count = serializers.SerializerMethodField()
class Meta:
model = PostCategory
fields = [
'id',
'name',
'slug',
'language',
'description',
'created_at',
'posts_count',
]
read_only_fields = ['id', 'slug', 'created_at', 'posts_count']
@extend_schema_field(serializers.IntegerField)
def get_posts_count(self, obj) -> int:
return obj.posts.filter(status=Post.Status.PUBLISHED).count()
class PostTagSerializer(serializers.ModelSerializer):
class Meta:
model = PostTag
fields = ['id', 'name', 'slug']
read_only_fields = ['id', 'slug']
class PostAuthorMiniSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'email', 'fullname', 'avatar']
read_only_fields = fields
class PostInstitutionMiniSerializer(serializers.ModelSerializer):
type_display = serializers.CharField(source='get_type_display', read_only=True)
class Meta:
model = Institution
fields = ['id', 'name', 'slug', 'type', 'type_display', 'avatar', 'city', 'country']
read_only_fields = fields
class PostAuthorDisplaySerializer(serializers.Serializer):
type = serializers.CharField(help_text="Author type: 'user' or 'institution'")
id = serializers.IntegerField(help_text="ID of the user or institution", allow_null=True)
name = serializers.CharField(help_text="Name of the author or institution")
slug = serializers.CharField(allow_null=True, required=False)
avatar = serializers.CharField(allow_null=True, required=False)
role_label = serializers.CharField(allow_null=True, required=False)
submitted_by = serializers.DictField(allow_null=True, required=False)
def resolve_post_author_display(obj, request=None) -> dict:
custom_avatar = obj.author_custom_avatar.url if getattr(obj, 'author_custom_avatar', None) else None
if custom_avatar and request and not custom_avatar.startswith(('http://', 'https://')):
custom_avatar = request.build_absolute_uri(custom_avatar)
if obj.author_type == Post.AuthorType.INSTITUTION and obj.institution:
inst = obj.institution
avatar_url = custom_avatar or (inst.avatar.url if getattr(inst, 'avatar', None) else None)
if avatar_url and request and not avatar_url.startswith(('http://', 'https://')):
avatar_url = request.build_absolute_uri(avatar_url)
user = obj.author
submitted_by = None
if user:
submitted_by = {
'id': user.id,
'name': getattr(user, 'fullname', None) or user.get_full_name() or user.email,
}
return {
'type': 'institution',
'id': inst.id,
'name': inst.name,
'slug': inst.slug,
'avatar': avatar_url,
'role_label': obj.author_role_label,
'submitted_by': submitted_by,
}
user = obj.author
avatar_url = custom_avatar or (user.avatar.url if (user and getattr(user, 'avatar', None)) else None)
if avatar_url and request and not avatar_url.startswith(('http://', 'https://')):
avatar_url = request.build_absolute_uri(avatar_url)
return {
'type': 'user',
'id': user.id if user else None,
'name': (getattr(user, 'fullname', None) or user.get_full_name() or user.email) if user else '',
'slug': getattr(user, 'username', None),
'avatar': avatar_url,
'role_label': obj.author_role_label,
'submitted_by': None,
}
class PostRevisionSerializer(serializers.ModelSerializer):
author = PostAuthorMiniSerializer(read_only=True)
reviewed_by = PostAuthorMiniSerializer(read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True)
category_name = serializers.CharField(source='category.name', read_only=True, allow_null=True)
class Meta:
model = PostRevision
fields = [
'id',
'post',
'author',
'title',
'excerpt',
'content',
'tags',
'featured_image',
'category',
'category_name',
'status',
'status_display',
'rejection_reason',
'reviewed_by',
'reviewed_at',
'created_at',
'updated_at',
]
read_only_fields = fields
class PostRevisionCreateSerializer(serializers.ModelSerializer):
class Meta:
model = PostRevision
fields = [
'title',
'excerpt',
'content',
'tags',
'featured_image',
'category',
]
class PostCommentSerializer(serializers.ModelSerializer):
user = PostAuthorMiniSerializer(read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = PostComment
fields = [
'id',
'post',
'user',
'author_name',
'author_email',
'content',
'status',
'status_display',
'created_at',
]
read_only_fields = ['id', 'post', 'user', 'status', 'status_display', 'created_at']
class PostCommentCreateSerializer(serializers.Serializer):
content = serializers.CharField(required=True)
author_name = serializers.CharField(required=False, allow_blank=True, max_length=255)
author_email = serializers.EmailField(required=False, allow_blank=True)
class PostListSerializer(serializers.ModelSerializer):
post_type_display = serializers.CharField(source='get_post_type_display', read_only=True)
author_type_display = serializers.CharField(source='get_author_type_display', read_only=True)
author_display = serializers.SerializerMethodField()
category_name = serializers.CharField(source='category.name', read_only=True, allow_null=True)
author = PostAuthorMiniSerializer(read_only=True)
institution = PostInstitutionMiniSerializer(read_only=True)
comments_count = serializers.SerializerMethodField()
is_liked = serializers.SerializerMethodField()
has_pending_revision = serializers.BooleanField(read_only=True)
class Meta:
model = Post
fields = [
'id',
'title',
'slug',
'post_type',
'post_type_display',
'author_type',
'author_type_display',
'author_display',
'category',
'category_name',
'language',
'status',
'rejection_reason',
'author',
'institution',
'author_role_label',
'author_custom_avatar',
'featured_image',
'excerpt',
'tags',
'reading_time_minutes',
'views_count',
'likes_count',
'comments_count',
'is_featured',
'publish_date',
'is_liked',
'has_pending_revision',
'reviewed_at',
'created_at',
]
read_only_fields = fields
@extend_schema_field(PostAuthorDisplaySerializer)
def get_author_display(self, obj) -> dict:
return resolve_post_author_display(obj, self.context.get('request'))
@extend_schema_field(serializers.IntegerField)
def get_comments_count(self, obj) -> int:
return obj.comments.filter(status=PostComment.Status.APPROVED).count()
@extend_schema_field(serializers.BooleanField)
def get_is_liked(self, obj) -> bool:
request = self.context.get('request')
if not request or not request.user.is_authenticated:
return False
return obj.is_liked_by(request.user)
class PostDetailSerializer(serializers.ModelSerializer):
post_type_display = serializers.CharField(source='get_post_type_display', read_only=True)
author_type_display = serializers.CharField(source='get_author_type_display', read_only=True)
author_display = serializers.SerializerMethodField()
category = PostCategorySerializer(read_only=True)
author = PostAuthorMiniSerializer(read_only=True)
institution = PostInstitutionMiniSerializer(read_only=True)
reviewed_by = PostAuthorMiniSerializer(read_only=True)
comments = serializers.SerializerMethodField()
comments_count = serializers.SerializerMethodField()
is_liked = serializers.SerializerMethodField()
has_pending_revision = serializers.BooleanField(read_only=True)
pending_revision = serializers.SerializerMethodField()
class Meta:
model = Post
fields = [
'id',
'title',
'slug',
'post_type',
'post_type_display',
'author_type',
'author_type_display',
'author_display',
'category',
'language',
'status',
'rejection_reason',
'author',
'institution',
'author_role_label',
'author_custom_avatar',
'featured_image',
'excerpt',
'content',
'tags',
'reading_time_minutes',
'views_count',
'likes_count',
'comments_count',
'is_featured',
'publish_date',
'is_liked',
'has_pending_revision',
'pending_revision',
'comments',
'reviewed_by',
'reviewed_at',
'created_at',
'updated_at',
]
read_only_fields = [
'id',
'slug',
'reading_time_minutes',
'views_count',
'likes_count',
'comments_count',
'is_liked',
'has_pending_revision',
'pending_revision',
'comments',
'reviewed_by',
'reviewed_at',
'created_at',
'updated_at',
]
@extend_schema_field(PostAuthorDisplaySerializer)
def get_author_display(self, obj) -> dict:
return resolve_post_author_display(obj, self.context.get('request'))
@extend_schema_field(PostRevisionSerializer)
def get_pending_revision(self, obj):
request = self.context.get('request')
if not request or not request.user.is_authenticated:
return None
user = request.user
is_author = (obj.author_id == user.id)
is_editor = can_publish_or_moderate_post(user, obj.institution)
if is_author or is_editor:
pending = obj.get_pending_revision()
if pending:
return PostRevisionSerializer(pending, context=self.context).data
return None
@extend_schema_field(PostCommentSerializer(many=True))
def get_comments(self, obj):
approved_comments = obj.comments.filter(status=PostComment.Status.APPROVED).order_by('-created_at')
return PostCommentSerializer(approved_comments, many=True).data
@extend_schema_field(serializers.IntegerField)
def get_comments_count(self, obj) -> int:
return obj.comments.filter(status=PostComment.Status.APPROVED).count()
@extend_schema_field(serializers.BooleanField)
def get_is_liked(self, obj) -> bool:
request = self.context.get('request')
if not request or not request.user.is_authenticated:
return False
return obj.is_liked_by(request.user)
class PostCreateUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = [
'title',
'post_type',
'author_type',
'category',
'language',
'status',
'rejection_reason',
'institution',
'author_role_label',
'author_custom_avatar',
'featured_image',
'excerpt',
'content',
'tags',
'is_featured',
'publish_date',
]
def validate(self, attrs):
request = self.context.get('request')
user = request.user if request else None
instance = getattr(self, 'instance', None)
target_author_type = attrs.get('author_type') or (instance.author_type if instance else Post.AuthorType.USER)
target_institution = attrs.get('institution') or (instance.institution if instance else None)
if target_author_type == Post.AuthorType.INSTITUTION:
if not target_institution:
raise serializers.ValidationError({
"institution": _("An institution is required when authoring on behalf of an institution.")
})
if user and not (user.is_superuser or getattr(user, 'is_super_admin', False)):
is_inst_editor = target_institution.is_editor(user)
has_reg_perm = False
if getattr(user, 'is_regional_admin', False) and hasattr(user, 'can_manage_region'):
has_reg_perm = user.can_manage_region(target_institution.region)
if not (is_inst_editor or has_reg_perm):
raise serializers.ValidationError({
"institution": _("You are not authorized to author content on behalf of this institution.")
})
target_status = attrs.get('status')
if target_status:
has_mod_cap = can_publish_or_moderate_post(user, target_institution)
if target_status == Post.Status.PUBLISHED:
if not has_mod_cap:
raise serializers.ValidationError({
"status": _("You do not have direct publishing capability for this institution. Please save as draft or submit for review.")
})
elif target_status == Post.Status.REJECTED:
if not has_mod_cap:
raise serializers.ValidationError({
"status": _("You do not have authority to reject posts for this institution.")
})
reason = attrs.get('rejection_reason', '') or (instance.rejection_reason if instance else '')
if not reason or not str(reason).strip():
raise serializers.ValidationError({
"rejection_reason": _("A rejection reason is required when rejecting a post.")
})
elif target_status == Post.Status.ARCHIVED:
if not has_mod_cap:
raise serializers.ValidationError({
"status": _("You do not have authority to archive posts for this institution.")
})
elif target_status in [Post.Status.PENDING_REVIEW, Post.Status.DRAFT]:
# If resubmitting from rejected to draft/pending_review, clear old rejection reason
if instance and instance.status == Post.Status.REJECTED and 'rejection_reason' not in attrs:
attrs['rejection_reason'] = ''
else:
if not instance:
attrs['status'] = Post.Status.DRAFT
return attrs
def create(self, validated_data):
request = self.context.get('request')
if request and request.user.is_authenticated:
validated_data['author'] = request.user
target_status = validated_data.get('status')
if target_status in [Post.Status.PUBLISHED, Post.Status.REJECTED]:
validated_data['reviewed_by'] = request.user
validated_data['reviewed_at'] = timezone.now()
return super().create(validated_data)
def update(self, instance, validated_data):
request = self.context.get('request')
target_status = validated_data.get('status')
if target_status and target_status != instance.status:
if target_status in [Post.Status.PUBLISHED, Post.Status.REJECTED]:
if request and request.user.is_authenticated:
validated_data['reviewed_by'] = request.user
validated_data['reviewed_at'] = timezone.now()
return super().update(instance, validated_data)
class PostLikeResponseSerializer(serializers.Serializer):
is_liked = serializers.BooleanField()
likes_count = serializers.IntegerField()
message = serializers.CharField()
class MediaAssetSerializer(serializers.ModelSerializer):
media_type_display = serializers.CharField(source='get_media_type_display', read_only=True)
uploaded_by_email = serializers.EmailField(source='uploaded_by.email', read_only=True, allow_null=True)
class Meta:
model = MediaAsset
fields = [
'id',
'file',
'title',
'media_type',
'media_type_display',
'file_size',
'dimensions',
'uploaded_by_email',
'created_at',
]
read_only_fields = ['id', 'media_type_display', 'file_size', 'uploaded_by_email', 'created_at']
def create(self, validated_data):
request = self.context.get('request')
if request and request.user.is_authenticated:
validated_data['uploaded_by'] = request.user
return super().create(validated_data)