from rest_framework import serializers from django.core.exceptions import PermissionDenied, ValidationError from django.http import Http404 from django.utils import six from django.utils.translation import ugettext as _, ugettext_lazy, ungettext from misago.acl import add_acl from misago.categories import THREADS_ROOT_NAME from misago.conf import settings from misago.threads.models import Thread from misago.threads.permissions import ( 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.pollmergehandler import PollMergeHandler 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_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 = ugettext_lazy("You have to specify at least one post to delete.") posts = serializers.ListField( allow_empty=False, child=serializers.IntegerField( error_messages={ 'invalid': ugettext_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 = ungettext( "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 = self.context['user'] thread = self.context['thread'] posts_queryset = exclude_invisible_posts(user, 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, post) else: allow_delete_post(user, 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 = ugettext_lazy("You have to select at least two posts to merge.") posts = serializers.ListField( child=serializers.IntegerField( error_messages={ 'invalid': ugettext_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 = ungettext( "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 = self.context['user'] thread = self.context['thread'] posts_queryset = exclude_invisible_posts(user, 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, 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 = ugettext_lazy("You have to specify at least one post to move.") new_thread = serializers.CharField( error_messages={ 'required': ugettext_lazy("Enter link to new thread."), }, ) posts = serializers.ListField( allow_empty=False, child=serializers.IntegerField( error_messages={ 'invalid': ugettext_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 = ungettext( "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, 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, 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): return validate_title(title) def validate_category(self, category_id): self.category = validate_category(self.context['user'], category_id) if not can_start_thread(self.context['user'], self.category): raise ValidationError(_("You can't create new threads in selected category.")) return self.category def validate_weight(self, weight): try: add_acl(self.context['user'], 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(self.context['user'], 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(self.context['user'], 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 = ugettext_lazy("You have to specify at least one post to split.") posts = serializers.ListField( allow_empty=False, child=serializers.IntegerField( error_messages={ 'invalid': ugettext_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 = ungettext( "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 = self.context['user'] posts_queryset = exclude_invisible_posts(user, 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, 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 = ugettext_lazy("You have to specify at least one thread to delete.") threads = serializers.ListField( allow_empty=False, child=serializers.IntegerField( error_messages={ 'invalid': ugettext_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 = ungettext( "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, thread) threads.append(thread) except PermissionDenied as e: errors.append({ 'thread': { 'id': thread.id, 'title': thread.title }, 'error': six.text_type(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': ugettext_lazy("Enter link to new thread."), }, ) poll = serializers.IntegerField( required=False, error_messages={ 'invalid': ugettext_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, 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, 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'] polls_handler = PollMergeHandler([thread, other_thread]) if len(polls_handler.polls) == 1: data['poll'] = polls_handler.polls[0] elif polls_handler.is_merge_conflict(): if 'poll' in data: polls_handler.set_resolution(data['poll']) if polls_handler.is_valid(): data['poll'] = polls_handler.get_resolution() else: raise serializers.ValidationError({'poll': _("Invalid choice.")}) else: data['polls'] = polls_handler.get_available_resolutions() self.polls_handler = polls_handler return data class MergeThreadsSerializer(NewThreadSerializer): error_empty_or_required = ugettext_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': ugettext_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, }, ) def validate_threads(self, data): if len(data) > THREADS_LIMIT: message = ungettext( "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 = self.context['user'] threads = [] for thread in threads_queryset: add_acl(user, thread) if can_see_thread(user, thread): threads.append(thread) if len(threads) != len(data): raise ValidationError(_("One or more threads to merge could not be found.")) return threads