123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571 |
- from django.core.exceptions import PermissionDenied, ValidationError
- from django.http import Http404
- from django.utils.translation import gettext as _
- from django.utils.translation import gettext_lazy, ngettext
- from rest_framework import serializers
- from misago.acl.objectacl import add_acl_to_obj
- from misago.categories import THREADS_ROOT_NAME
- from misago.conf import settings
- from misago.threads.mergeconflict import MergeConflict
- from misago.threads.models import Thread
- from misago.threads.permissions import (
- allow_delete_best_answer,
- allow_delete_event,
- allow_delete_post,
- allow_delete_thread,
- allow_merge_post,
- allow_merge_thread,
- allow_move_post,
- allow_split_post,
- can_reply_thread,
- can_see_thread,
- can_start_thread,
- exclude_invisible_posts,
- )
- from misago.threads.threadtypes import trees_map
- from misago.threads.utils import get_thread_id_from_url
- from misago.threads.validators import validate_category, validate_thread_title
- POSTS_LIMIT = settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL
- THREADS_LIMIT = settings.MISAGO_THREADS_PER_PAGE + settings.MISAGO_THREADS_TAIL
- __all__ = [
- "DeletePostsSerializer",
- "DeleteThreadsSerializer",
- "MergePostsSerializer",
- "MergeThreadSerializer",
- "MergeThreadsSerializer",
- "MovePostsSerializer",
- "NewThreadSerializer",
- "SplitPostsSerializer",
- ]
- class DeletePostsSerializer(serializers.Serializer):
- error_empty_or_required = gettext_lazy(
- "You have to specify at least one post to delete."
- )
- posts = serializers.ListField(
- allow_empty=False,
- child=serializers.IntegerField(
- error_messages={
- "invalid": gettext_lazy("One or more post ids received were invalid.")
- }
- ),
- error_messages={
- "required": error_empty_or_required,
- "null": error_empty_or_required,
- "empty": error_empty_or_required,
- },
- )
- def validate_posts(self, data):
- if len(data) > POSTS_LIMIT:
- message = ngettext(
- "No more than %(limit)s post can be deleted at single time.",
- "No more than %(limit)s posts can be deleted at single time.",
- POSTS_LIMIT,
- )
- raise ValidationError(message % {"limit": POSTS_LIMIT})
- user_acl = self.context["user_acl"]
- thread = self.context["thread"]
- posts_queryset = exclude_invisible_posts(
- user_acl, thread.category, thread.post_set
- )
- posts_queryset = posts_queryset.filter(id__in=data).order_by("id")
- posts = []
- for post in posts_queryset:
- post.category = thread.category
- post.thread = thread
- if post.is_event:
- allow_delete_event(user_acl, post)
- else:
- allow_delete_best_answer(user_acl, post)
- allow_delete_post(user_acl, post)
- posts.append(post)
- if len(posts) != len(data):
- raise PermissionDenied(_("One or more posts to delete could not be found."))
- return posts
- class MergePostsSerializer(serializers.Serializer):
- error_empty_or_required = gettext_lazy(
- "You have to select at least two posts to merge."
- )
- posts = serializers.ListField(
- child=serializers.IntegerField(
- error_messages={
- "invalid": gettext_lazy("One or more post ids received were invalid.")
- }
- ),
- error_messages={
- "null": error_empty_or_required,
- "required": error_empty_or_required,
- },
- )
- def validate_posts(self, data):
- data = list(set(data))
- if len(data) < 2:
- raise serializers.ValidationError(self.error_empty_or_required)
- if len(data) > POSTS_LIMIT:
- message = ngettext(
- "No more than %(limit)s post can be merged at single time.",
- "No more than %(limit)s posts can be merged at single time.",
- POSTS_LIMIT,
- )
- raise serializers.ValidationError(message % {"limit": POSTS_LIMIT})
- user_acl = self.context["user_acl"]
- thread = self.context["thread"]
- posts_queryset = exclude_invisible_posts(
- user_acl, thread.category, thread.post_set
- )
- posts_queryset = posts_queryset.filter(id__in=data).order_by("id")
- posts = []
- for post in posts_queryset:
- post.category = thread.category
- post.thread = thread
- try:
- allow_merge_post(user_acl, post)
- except PermissionDenied as e:
- raise serializers.ValidationError(e)
- if not posts:
- posts.append(post)
- continue
- authorship_error = _("Posts made by different users can't be merged.")
- if post.poster_id != posts[0].poster_id:
- raise serializers.ValidationError(authorship_error)
- elif (
- post.poster_id is None
- and posts[0].poster_id is None
- and post.poster_name != posts[0].poster_name
- ):
- raise serializers.ValidationError(authorship_error)
- if posts[0].is_first_post and post.is_best_answer:
- raise serializers.ValidationError(
- _(
- "Post marked as best answer can't be merged with thread's first post."
- )
- )
- if not posts[0].is_first_post:
- if (
- posts[0].is_hidden != post.is_hidden
- or posts[0].is_unapproved != post.is_unapproved
- ):
- raise serializers.ValidationError(
- _("Posts with different visibility can't be merged.")
- )
- posts.append(post)
- if len(posts) != len(data):
- raise serializers.ValidationError(
- _("One or more posts to merge could not be found.")
- )
- return posts
- class MovePostsSerializer(serializers.Serializer):
- error_empty_or_required = gettext_lazy(
- "You have to specify at least one post to move."
- )
- new_thread = serializers.CharField(
- error_messages={"required": gettext_lazy("Enter link to new thread.")}
- )
- posts = serializers.ListField(
- allow_empty=False,
- child=serializers.IntegerField(
- error_messages={
- "invalid": gettext_lazy("One or more post ids received were invalid.")
- }
- ),
- error_messages={
- "empty": error_empty_or_required,
- "null": error_empty_or_required,
- "required": error_empty_or_required,
- },
- )
- def validate_new_thread(self, data):
- request = self.context["request"]
- thread = self.context["thread"]
- viewmodel = self.context["viewmodel"]
- new_thread_id = get_thread_id_from_url(request, data)
- if not new_thread_id:
- raise serializers.ValidationError(_("This is not a valid thread link."))
- if new_thread_id == thread.pk:
- raise serializers.ValidationError(
- _("Thread to move posts to is same as current one.")
- )
- try:
- new_thread = viewmodel(request, new_thread_id).unwrap()
- except Http404:
- raise serializers.ValidationError(
- _(
- "The thread you have entered link to doesn't "
- "exist or you don't have permission to see it."
- )
- )
- if not new_thread.acl["can_reply"]:
- raise serializers.ValidationError(
- _("You can't move posts to threads you can't reply.")
- )
- return new_thread
- def validate_posts(self, data):
- data = list(set(data))
- if len(data) > POSTS_LIMIT:
- message = ngettext(
- "No more than %(limit)s post can be moved at single time.",
- "No more than %(limit)s posts can be moved at single time.",
- POSTS_LIMIT,
- )
- raise serializers.ValidationError(message % {"limit": POSTS_LIMIT})
- request = self.context["request"]
- thread = self.context["thread"]
- posts_queryset = exclude_invisible_posts(
- request.user_acl, thread.category, thread.post_set
- )
- posts_queryset = posts_queryset.filter(id__in=data).order_by("id")
- posts = []
- for post in posts_queryset:
- post.category = thread.category
- post.thread = thread
- try:
- allow_move_post(request.user_acl, post)
- posts.append(post)
- except PermissionDenied as e:
- raise serializers.ValidationError(e)
- if len(posts) != len(data):
- raise serializers.ValidationError(
- _("One or more posts to move could not be found.")
- )
- return posts
- class NewThreadSerializer(serializers.Serializer):
- title = serializers.CharField()
- category = serializers.IntegerField()
- weight = serializers.IntegerField(
- required=False,
- allow_null=True,
- max_value=Thread.WEIGHT_GLOBAL,
- min_value=Thread.WEIGHT_DEFAULT,
- )
- is_hidden = serializers.NullBooleanField(required=False)
- is_closed = serializers.NullBooleanField(required=False)
- def validate_title(self, title):
- settings = self.context["settings"]
- validate_thread_title(settings, title)
- return title
- def validate_category(self, category_id):
- user_acl = self.context["user_acl"]
- self.category = validate_category(user_acl, category_id)
- if not can_start_thread(user_acl, self.category):
- raise ValidationError(
- _("You can't create new threads in selected category.")
- )
- return self.category
- def validate_weight(self, weight):
- try:
- add_acl_to_obj(self.context["user_acl"], self.category)
- except AttributeError:
- return weight # don't validate weight further if category failed
- if weight > self.category.acl.get("can_pin_threads", 0):
- if weight == 2:
- raise ValidationError(
- _(
- "You don't have permission to pin threads globally in this category."
- )
- )
- else:
- raise ValidationError(
- _("You don't have permission to pin threads in this category.")
- )
- return weight
- def validate_is_hidden(self, is_hidden):
- try:
- add_acl_to_obj(self.context["user_acl"], self.category)
- except AttributeError:
- return is_hidden # don't validate hidden further if category failed
- if is_hidden and not self.category.acl.get("can_hide_threads"):
- raise ValidationError(
- _("You don't have permission to hide threads in this category.")
- )
- return is_hidden
- def validate_is_closed(self, is_closed):
- try:
- add_acl_to_obj(self.context["user_acl"], self.category)
- except AttributeError:
- return is_closed # don't validate closed further if category failed
- if is_closed and not self.category.acl.get("can_close_threads"):
- raise ValidationError(
- _("You don't have permission to close threads in this category.")
- )
- return is_closed
- class SplitPostsSerializer(NewThreadSerializer):
- error_empty_or_required = gettext_lazy(
- "You have to specify at least one post to split."
- )
- posts = serializers.ListField(
- allow_empty=False,
- child=serializers.IntegerField(
- error_messages={
- "invalid": gettext_lazy("One or more post ids received were invalid.")
- }
- ),
- error_messages={
- "empty": error_empty_or_required,
- "null": error_empty_or_required,
- "required": error_empty_or_required,
- },
- )
- def validate_posts(self, data):
- if len(data) > POSTS_LIMIT:
- message = ngettext(
- "No more than %(limit)s post can be split at single time.",
- "No more than %(limit)s posts can be split at single time.",
- POSTS_LIMIT,
- )
- raise ValidationError(message % {"limit": POSTS_LIMIT})
- thread = self.context["thread"]
- user_acl = self.context["user_acl"]
- posts_queryset = exclude_invisible_posts(
- user_acl, thread.category, thread.post_set
- )
- posts_queryset = posts_queryset.filter(id__in=data).order_by("id")
- posts = []
- for post in posts_queryset:
- post.category = thread.category
- post.thread = thread
- try:
- allow_split_post(user_acl, post)
- except PermissionDenied as e:
- raise ValidationError(e)
- posts.append(post)
- if len(posts) != len(data):
- raise ValidationError(_("One or more posts to split could not be found."))
- return posts
- class DeleteThreadsSerializer(serializers.Serializer):
- error_empty_or_required = gettext_lazy(
- "You have to specify at least one thread to delete."
- )
- threads = serializers.ListField(
- allow_empty=False,
- child=serializers.IntegerField(
- error_messages={
- "invalid": gettext_lazy("One or more thread ids received were invalid.")
- }
- ),
- error_messages={
- "required": error_empty_or_required,
- "null": error_empty_or_required,
- "empty": error_empty_or_required,
- },
- )
- def validate_threads(self, data):
- if len(data) > THREADS_LIMIT:
- message = ngettext(
- "No more than %(limit)s thread can be deleted at single time.",
- "No more than %(limit)s threads can be deleted at single time.",
- THREADS_LIMIT,
- )
- raise ValidationError(message % {"limit": THREADS_LIMIT})
- request = self.context["request"]
- viewmodel = self.context["viewmodel"]
- threads = []
- errors = []
- for thread_id in data:
- try:
- thread = viewmodel(request, thread_id).unwrap()
- allow_delete_thread(request.user_acl, thread)
- threads.append(thread)
- except PermissionDenied as e:
- errors.append(
- {
- "thread": {"id": thread.id, "title": thread.title},
- "error": str(e),
- }
- )
- except Http404 as e:
- pass # skip invisible threads
- if errors:
- raise serializers.ValidationError({"details": errors})
- if len(threads) != len(data):
- raise ValidationError(
- _("One or more threads to delete could not be found.")
- )
- return threads
- class MergeThreadSerializer(serializers.Serializer):
- other_thread = serializers.CharField(
- error_messages={"required": gettext_lazy("Enter link to new thread.")}
- )
- best_answer = serializers.IntegerField(
- required=False, error_messages={"invalid": gettext_lazy("Invalid choice.")}
- )
- poll = serializers.IntegerField(
- required=False, error_messages={"invalid": gettext_lazy("Invalid choice.")}
- )
- def validate_other_thread(self, data):
- request = self.context["request"]
- thread = self.context["thread"]
- viewmodel = self.context["viewmodel"]
- other_thread_id = get_thread_id_from_url(request, data)
- if not other_thread_id:
- raise ValidationError(_("This is not a valid thread link."))
- if other_thread_id == thread.pk:
- raise ValidationError(_("You can't merge thread with itself."))
- try:
- other_thread = viewmodel(request, other_thread_id).unwrap()
- allow_merge_thread(request.user_acl, other_thread, otherthread=True)
- except PermissionDenied as e:
- raise serializers.ValidationError(e)
- except Http404:
- raise ValidationError(
- _(
- "The thread you have entered link to doesn't "
- "exist or you don't have permission to see it."
- )
- )
- if not can_reply_thread(request.user_acl, other_thread):
- raise ValidationError(
- _("You can't merge this thread into thread you can't reply.")
- )
- return other_thread
- def validate(self, data):
- thread = self.context["thread"]
- other_thread = data["other_thread"]
- merge_conflict = MergeConflict(data, [thread, other_thread])
- merge_conflict.is_valid(raise_exception=True)
- data.update(merge_conflict.get_resolution())
- self.merge_conflict = merge_conflict.get_conflicting_fields()
- return data
- class MergeThreadsSerializer(NewThreadSerializer):
- error_empty_or_required = gettext_lazy(
- "You have to select at least two threads to merge."
- )
- threads = serializers.ListField(
- allow_empty=False,
- min_length=2,
- child=serializers.IntegerField(
- error_messages={
- "invalid": gettext_lazy("One or more thread ids received were invalid.")
- }
- ),
- error_messages={
- "empty": error_empty_or_required,
- "null": error_empty_or_required,
- "required": error_empty_or_required,
- "min_length": error_empty_or_required,
- },
- )
- best_answer = serializers.IntegerField(
- required=False, error_messages={"invalid": gettext_lazy("Invalid choice.")}
- )
- poll = serializers.IntegerField(
- required=False, error_messages={"invalid": gettext_lazy("Invalid choice.")}
- )
- def validate_threads(self, data):
- if len(data) > THREADS_LIMIT:
- message = ngettext(
- "No more than %(limit)s thread can be merged at single time.",
- "No more than %(limit)s threads can be merged at single time.",
- POSTS_LIMIT,
- )
- raise ValidationError(message % {"limit": THREADS_LIMIT})
- threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
- threads_queryset = (
- Thread.objects.filter(id__in=data, category__tree_id=threads_tree_id)
- .select_related("category")
- .order_by("-id")
- )
- user_acl = self.context["user_acl"]
- threads = []
- for thread in threads_queryset:
- add_acl_to_obj(user_acl, thread)
- if can_see_thread(user_acl, thread):
- threads.append(thread)
- if len(threads) != len(data):
- raise ValidationError(_("One or more threads to merge could not be found."))
- return threads
|