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
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)
|