Browse Source

Handle merging best answers

Rafał Pitoń 7 years ago
parent
commit
a9d201ea36

+ 32 - 30
misago/threads/api/threadendpoints/merge.py

@@ -1,3 +1,4 @@
+from rest_framework.exceptions import ValidationError
 from rest_framework.response import Response
 from rest_framework.response import Response
 
 
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
@@ -29,14 +30,14 @@ def thread_merge_endpoint(request, thread, viewmodel):
     if not serializer.is_valid():
     if not serializer.is_valid():
         if 'other_thread' in serializer.errors:
         if 'other_thread' in serializer.errors:
             errors = serializer.errors['other_thread']
             errors = serializer.errors['other_thread']
-        elif 'poll' in serializer.errors:
-            errors = serializer.errors['poll']
-        elif 'polls' in serializer.errors:
-            return Response({'polls': serializer.errors['polls']}, status=400)
         elif 'best_answer' in serializer.errors:
         elif 'best_answer' in serializer.errors:
             errors = serializer.errors['best_answer']
             errors = serializer.errors['best_answer']
         elif 'best_answers' in serializer.errors:
         elif 'best_answers' in serializer.errors:
             return Response({'best_answers': serializer.errors['best_answers']}, status=400)
             return Response({'best_answers': serializer.errors['best_answers']}, status=400)
+        elif 'poll' in serializer.errors:
+            errors = serializer.errors['poll']
+        elif 'polls' in serializer.errors:
+            return Response({'polls': serializer.errors['polls']}, status=400)
         else:
         else:
             errors = list(serializer.errors.values())[0]
             errors = list(serializer.errors.values())[0]
         return Response({'detail': errors[0]}, status=400)
         return Response({'detail': errors[0]}, status=400)
@@ -44,16 +45,6 @@ def thread_merge_endpoint(request, thread, viewmodel):
     # merge conflict
     # merge conflict
     other_thread = serializer.validated_data['other_thread']
     other_thread = serializer.validated_data['other_thread']
 
 
-    poll = serializer.validated_data.get('poll')
-    if 'poll' in serializer.merge_conflict:
-        if poll and poll.thread_id != other_thread.id:
-            other_thread.poll.delete()
-            poll.move(other_thread)
-        elif not poll:
-            other_thread.poll.delete()
-    elif poll:
-        poll.move(other_thread)
-
     best_answer = serializer.validated_data.get('best_answer')
     best_answer = serializer.validated_data.get('best_answer')
     if 'best_answer' in serializer.merge_conflict and not best_answer:
     if 'best_answer' in serializer.merge_conflict and not best_answer:
         other_thread.clear_best_answer()
         other_thread.clear_best_answer()
@@ -65,6 +56,16 @@ def thread_merge_endpoint(request, thread, viewmodel):
         other_thread.best_answer_marked_by_name = thread.best_answer_marked_by_name
         other_thread.best_answer_marked_by_name = thread.best_answer_marked_by_name
         other_thread.best_answer_marked_by_slug = thread.best_answer_marked_by_slug
         other_thread.best_answer_marked_by_slug = thread.best_answer_marked_by_slug
 
 
+    poll = serializer.validated_data.get('poll')
+    if 'poll' in serializer.merge_conflict:
+        if poll and poll.thread_id != other_thread.id:
+            other_thread.poll.delete()
+            poll.move(other_thread)
+        elif not poll:
+            other_thread.poll.delete()
+    elif poll:
+        poll.move(other_thread)
+
     # merge thread contents
     # merge thread contents
     moderation.merge_thread(request, other_thread, thread)
     moderation.merge_thread(request, other_thread, thread)
 
 
@@ -119,26 +120,15 @@ def threads_merge_endpoint(request):
     if invalid_threads:
     if invalid_threads:
         return Response(invalid_threads, status=403)
         return Response(invalid_threads, status=403)
 
 
-    polls_handler = PollMergeHandler(threads)
-    if len(polls_handler.polls) == 1:
-        poll = polls_handler.polls[0]
-    elif polls_handler.is_merge_conflict():
-        if 'poll' in request.data:
-            polls_handler.set_resolution(request.data.get('poll'))
-            if polls_handler.is_valid():
-                poll = polls_handler.get_resolution()
-            else:
-                return Response({'detail': _("Invalid choice.")}, status=400)
-        else:
-            return Response({'polls': polls_handler.get_available_resolutions()}, status=400)
-    else:
-        poll = None
+    # handle merge conflict
+    merge_conflict = MergeConflict(serializer.validated_data, threads)
+    merge_conflict.is_valid(raise_exception=True)
 
 
-    new_thread = merge_threads(request, serializer.validated_data, threads, poll)
+    new_thread = merge_threads(request, serializer.validated_data, threads, merge_conflict)
     return Response(ThreadsListSerializer(new_thread).data)
     return Response(ThreadsListSerializer(new_thread).data)
 
 
 
 
-def merge_threads(request, validated_data, threads, poll):
+def merge_threads(request, validated_data, threads, merge_conflict):
     new_thread = Thread(
     new_thread = Thread(
         category=validated_data['category'],
         category=validated_data['category'],
         started_on=threads[0].started_on,
         started_on=threads[0].started_on,
@@ -148,6 +138,18 @@ def merge_threads(request, validated_data, threads, poll):
     new_thread.set_title(validated_data['title'])
     new_thread.set_title(validated_data['title'])
     new_thread.save()
     new_thread.save()
 
 
+    resolution = merge_conflict.get_resolution()
+
+    best_answer = resolution.get('best_answer')
+    if best_answer:
+        new_thread.best_answer_id = best_answer.best_answer_id
+        new_thread.best_answer_is_protected = best_answer.best_answer_is_protected
+        new_thread.best_answer_marked_on = best_answer.best_answer_marked_on
+        new_thread.best_answer_marked_by_id = best_answer.best_answer_marked_by_id
+        new_thread.best_answer_marked_by_name = best_answer.best_answer_marked_by_name
+        new_thread.best_answer_marked_by_slug = best_answer.best_answer_marked_by_slug
+
+    poll = resolution.get('poll')
     if poll:
     if poll:
         poll.move(new_thread)
         poll.move(new_thread)
 
 

+ 2 - 2
misago/threads/mergeconflict.py

@@ -51,7 +51,7 @@ class BestAnswerMergeHandler(MergeConflictHandler):
             if thread.best_answer_id:
             if thread.best_answer_id:
                 self.items.append(thread)
                 self.items.append(thread)
                 self.choices[thread.pk] = thread
                 self.choices[thread.pk] = thread
-        self.items.sort(key=lambda choice: (thread.title, thread.id))
+        self.items.sort(key=lambda thread: (thread.title, thread.id))
 
 
     def get_available_resolutions(self):
     def get_available_resolutions(self):
         resolutions = [[0, _("Unmark all best answers")]]
         resolutions = [[0, _("Unmark all best answers")]]
@@ -98,7 +98,7 @@ class MergeConflict(object):
     def is_merge_conflict(self):
     def is_merge_conflict(self):
         return bool(self._conflicts)
         return bool(self._conflicts)
 
 
-    def get_merge_conflict(self):
+    def get_conflicting_fields(self):
         return [i.data_name for i in self._conflicts]
         return [i.data_name for i in self._conflicts]
 
 
     def set_resolution(self, data):
     def set_resolution(self, data):

+ 13 - 1
misago/threads/serializers/moderation.py

@@ -467,7 +467,7 @@ class MergeThreadSerializer(serializers.Serializer):
         merge_conflict = MergeConflict(data, [thread, other_thread])
         merge_conflict = MergeConflict(data, [thread, other_thread])
         merge_conflict.is_valid(raise_exception=True)
         merge_conflict.is_valid(raise_exception=True)
         data.update(merge_conflict.get_resolution())
         data.update(merge_conflict.get_resolution())
-        self.merge_conflict = merge_conflict.get_merge_conflict()
+        self.merge_conflict = merge_conflict.get_conflicting_fields()
 
 
         return data
         return data
 
 
@@ -490,6 +490,18 @@ class MergeThreadsSerializer(NewThreadSerializer):
             'min_length': error_empty_or_required,
             'min_length': error_empty_or_required,
         },
         },
     )
     )
+    best_answer = serializers.IntegerField(
+        required=False,
+        error_messages={
+            'invalid': ugettext_lazy("Invalid choice."),
+        },
+    )
+    poll = serializers.IntegerField(
+        required=False,
+        error_messages={
+            'invalid': ugettext_lazy("Invalid choice."),
+        },
+    )
 
 
     def validate_threads(self, data):
     def validate_threads(self, data):
         if len(data) > THREADS_LIMIT:
         if len(data) > THREADS_LIMIT:

+ 17 - 17
misago/threads/tests/test_mergeconflict.py

@@ -38,7 +38,7 @@ class MergeConflictTests(TestCase):
         threads = [self.create_plain_thread() for i in range(10)]
         threads = [self.create_plain_thread() for i in range(10)]
         merge_conflict = MergeConflict(threads=threads)
         merge_conflict = MergeConflict(threads=threads)
         self.assertFalse(merge_conflict.is_merge_conflict())
         self.assertFalse(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), [])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), [])
 
 
         merge_conflict.is_valid(raise_exception=True)
         merge_conflict.is_valid(raise_exception=True)
         self.assertEqual(merge_conflict.get_resolution(), {})
         self.assertEqual(merge_conflict.get_resolution(), {})
@@ -51,7 +51,7 @@ class MergeConflictTests(TestCase):
         ]
         ]
         merge_conflict = MergeConflict(threads=threads)
         merge_conflict = MergeConflict(threads=threads)
         self.assertFalse(merge_conflict.is_merge_conflict())
         self.assertFalse(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), [])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), [])
 
 
         merge_conflict.is_valid(raise_exception=True)
         merge_conflict.is_valid(raise_exception=True)
         self.assertEqual(merge_conflict.get_resolution(), {
         self.assertEqual(merge_conflict.get_resolution(), {
@@ -66,7 +66,7 @@ class MergeConflictTests(TestCase):
         ]
         ]
         merge_conflict = MergeConflict(threads=threads)
         merge_conflict = MergeConflict(threads=threads)
         self.assertFalse(merge_conflict.is_merge_conflict())
         self.assertFalse(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), [])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), [])
 
 
         merge_conflict.is_valid(raise_exception=True)
         merge_conflict.is_valid(raise_exception=True)
         self.assertEqual(merge_conflict.get_resolution(), {
         self.assertEqual(merge_conflict.get_resolution(), {
@@ -103,7 +103,7 @@ class MergeConflictTests(TestCase):
 
 
         merge_conflict = MergeConflict(threads=threads)
         merge_conflict = MergeConflict(threads=threads)
         self.assertTrue(merge_conflict.is_merge_conflict())
         self.assertTrue(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), ['best_answer'])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), ['best_answer'])
 
 
         # without choice, conflict lists resolutions
         # without choice, conflict lists resolutions
         try:
         try:
@@ -111,7 +111,7 @@ class MergeConflictTests(TestCase):
             self.fail("merge_conflict.is_valid() should raise ValidationError")
             self.fail("merge_conflict.is_valid() should raise ValidationError")
         except ValidationError as e:
         except ValidationError as e:
             self.assertTrue(merge_conflict.is_merge_conflict())
             self.assertTrue(merge_conflict.is_merge_conflict())
-            self.assertEqual(merge_conflict.get_merge_conflict(), ['best_answer'])
+            self.assertEqual(merge_conflict.get_conflicting_fields(), ['best_answer'])
             self.assertEqual(e.detail, {
             self.assertEqual(e.detail, {
                 'best_answers': [['0', 'Unmark all best answers']] + [
                 'best_answers': [['0', 'Unmark all best answers']] + [
                     [
                     [
@@ -133,7 +133,7 @@ class MergeConflictTests(TestCase):
         # conflict returns selected resolution
         # conflict returns selected resolution
         merge_conflict = MergeConflict({'best_answer': best_answers[0].id}, threads)
         merge_conflict = MergeConflict({'best_answer': best_answers[0].id}, threads)
         self.assertTrue(merge_conflict.is_merge_conflict())
         self.assertTrue(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), ['best_answer'])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), ['best_answer'])
         merge_conflict.is_valid(raise_exception=True)
         merge_conflict.is_valid(raise_exception=True)
         self.assertEqual(merge_conflict.get_resolution(), {
         self.assertEqual(merge_conflict.get_resolution(), {
             'best_answer': best_answers[0],
             'best_answer': best_answers[0],
@@ -143,7 +143,7 @@ class MergeConflictTests(TestCase):
         # conflict returns no-choice resolution
         # conflict returns no-choice resolution
         merge_conflict = MergeConflict({'best_answer': 0}, threads)
         merge_conflict = MergeConflict({'best_answer': 0}, threads)
         self.assertTrue(merge_conflict.is_merge_conflict())
         self.assertTrue(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), ['best_answer'])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), ['best_answer'])
         merge_conflict.is_valid(raise_exception=True)
         merge_conflict.is_valid(raise_exception=True)
         self.assertEqual(merge_conflict.get_resolution(), {
         self.assertEqual(merge_conflict.get_resolution(), {
             'best_answer': None,
             'best_answer': None,
@@ -161,7 +161,7 @@ class MergeConflictTests(TestCase):
 
 
         merge_conflict = MergeConflict(threads=threads)
         merge_conflict = MergeConflict(threads=threads)
         self.assertTrue(merge_conflict.is_merge_conflict())
         self.assertTrue(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), ['poll'])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), ['poll'])
 
 
         # without choice, conflict lists resolutions
         # without choice, conflict lists resolutions
         try:
         try:
@@ -169,7 +169,7 @@ class MergeConflictTests(TestCase):
             self.fail("merge_conflict.is_valid() should raise ValidationError")
             self.fail("merge_conflict.is_valid() should raise ValidationError")
         except ValidationError as e:
         except ValidationError as e:
             self.assertTrue(merge_conflict.is_merge_conflict())
             self.assertTrue(merge_conflict.is_merge_conflict())
-            self.assertEqual(merge_conflict.get_merge_conflict(), ['poll'])
+            self.assertEqual(merge_conflict.get_conflicting_fields(), ['poll'])
             self.assertEqual(e.detail, {
             self.assertEqual(e.detail, {
                 'polls': [['0', 'Delete all polls']] + [
                 'polls': [['0', 'Delete all polls']] + [
                     [
                     [
@@ -191,7 +191,7 @@ class MergeConflictTests(TestCase):
         # conflict returns selected resolution
         # conflict returns selected resolution
         merge_conflict = MergeConflict({'poll': polls[0].poll.id}, threads)
         merge_conflict = MergeConflict({'poll': polls[0].poll.id}, threads)
         self.assertTrue(merge_conflict.is_merge_conflict())
         self.assertTrue(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), ['poll'])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), ['poll'])
         merge_conflict.is_valid(raise_exception=True)
         merge_conflict.is_valid(raise_exception=True)
         self.assertEqual(merge_conflict.get_resolution(), {
         self.assertEqual(merge_conflict.get_resolution(), {
             'best_answer': best_answers[0],
             'best_answer': best_answers[0],
@@ -201,7 +201,7 @@ class MergeConflictTests(TestCase):
         # conflict returns no-choice resolution
         # conflict returns no-choice resolution
         merge_conflict = MergeConflict({'poll': 0}, threads)
         merge_conflict = MergeConflict({'poll': 0}, threads)
         self.assertTrue(merge_conflict.is_merge_conflict())
         self.assertTrue(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), ['poll'])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), ['poll'])
         merge_conflict.is_valid(raise_exception=True)
         merge_conflict.is_valid(raise_exception=True)
         self.assertEqual(merge_conflict.get_resolution(), {
         self.assertEqual(merge_conflict.get_resolution(), {
             'best_answer': best_answers[0],
             'best_answer': best_answers[0],
@@ -219,7 +219,7 @@ class MergeConflictTests(TestCase):
 
 
         merge_conflict = MergeConflict(threads=threads)
         merge_conflict = MergeConflict(threads=threads)
         self.assertTrue(merge_conflict.is_merge_conflict())
         self.assertTrue(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), ['best_answer', 'poll'])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), ['best_answer', 'poll'])
 
 
         # without choice, conflict lists all resolutions
         # without choice, conflict lists all resolutions
         try:
         try:
@@ -227,7 +227,7 @@ class MergeConflictTests(TestCase):
             self.fail("merge_conflict.is_valid() should raise ValidationError")
             self.fail("merge_conflict.is_valid() should raise ValidationError")
         except ValidationError as e:
         except ValidationError as e:
             self.assertTrue(merge_conflict.is_merge_conflict())
             self.assertTrue(merge_conflict.is_merge_conflict())
-            self.assertEqual(merge_conflict.get_merge_conflict(), ['best_answer', 'poll'])
+            self.assertEqual(merge_conflict.get_conflicting_fields(), ['best_answer', 'poll'])
             self.assertEqual(e.detail, {
             self.assertEqual(e.detail, {
                 'best_answers': [['0', 'Unmark all best answers']] + [
                 'best_answers': [['0', 'Unmark all best answers']] + [
                     [
                     [
@@ -285,7 +285,7 @@ class MergeConflictTests(TestCase):
         valid_choices = {'best_answer': best_answers[0].id, 'poll': polls[0].poll.id}
         valid_choices = {'best_answer': best_answers[0].id, 'poll': polls[0].poll.id}
         merge_conflict = MergeConflict(valid_choices, threads)
         merge_conflict = MergeConflict(valid_choices, threads)
         self.assertTrue(merge_conflict.is_merge_conflict())
         self.assertTrue(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), ['best_answer', 'poll'])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), ['best_answer', 'poll'])
         merge_conflict.is_valid(raise_exception=True)
         merge_conflict.is_valid(raise_exception=True)
         self.assertEqual(merge_conflict.get_resolution(), {
         self.assertEqual(merge_conflict.get_resolution(), {
             'best_answer': best_answers[0],
             'best_answer': best_answers[0],
@@ -295,7 +295,7 @@ class MergeConflictTests(TestCase):
         # conflict returns no-choice resolution
         # conflict returns no-choice resolution
         merge_conflict = MergeConflict({'best_answer': 0, 'poll': 0}, threads)
         merge_conflict = MergeConflict({'best_answer': 0, 'poll': 0}, threads)
         self.assertTrue(merge_conflict.is_merge_conflict())
         self.assertTrue(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), ['best_answer', 'poll'])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), ['best_answer', 'poll'])
         merge_conflict.is_valid(raise_exception=True)
         merge_conflict.is_valid(raise_exception=True)
         self.assertEqual(merge_conflict.get_resolution(), {
         self.assertEqual(merge_conflict.get_resolution(), {
             'best_answer': None,
             'best_answer': None,
@@ -305,7 +305,7 @@ class MergeConflictTests(TestCase):
         # conflict allows mixing no-choice with choice
         # conflict allows mixing no-choice with choice
         merge_conflict = MergeConflict({'best_answer': best_answers[0].id, 'poll': 0}, threads)
         merge_conflict = MergeConflict({'best_answer': best_answers[0].id, 'poll': 0}, threads)
         self.assertTrue(merge_conflict.is_merge_conflict())
         self.assertTrue(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), ['best_answer', 'poll'])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), ['best_answer', 'poll'])
         merge_conflict.is_valid(raise_exception=True)
         merge_conflict.is_valid(raise_exception=True)
         self.assertEqual(merge_conflict.get_resolution(), {
         self.assertEqual(merge_conflict.get_resolution(), {
             'best_answer': best_answers[0],
             'best_answer': best_answers[0],
@@ -314,7 +314,7 @@ class MergeConflictTests(TestCase):
 
 
         merge_conflict = MergeConflict({'best_answer': 0, 'poll': polls[0].poll.id}, threads)
         merge_conflict = MergeConflict({'best_answer': 0, 'poll': polls[0].poll.id}, threads)
         self.assertTrue(merge_conflict.is_merge_conflict())
         self.assertTrue(merge_conflict.is_merge_conflict())
-        self.assertEqual(merge_conflict.get_merge_conflict(), ['best_answer', 'poll'])
+        self.assertEqual(merge_conflict.get_conflicting_fields(), ['best_answer', 'poll'])
         merge_conflict.is_valid(raise_exception=True)
         merge_conflict.is_valid(raise_exception=True)
         self.assertEqual(merge_conflict.get_resolution(), {
         self.assertEqual(merge_conflict.get_resolution(), {
             'best_answer': None,
             'best_answer': None,

+ 6 - 6
misago/threads/tests/test_thread_merge_api.py

@@ -440,7 +440,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         with self.assertRaises(Thread.DoesNotExist):
         with self.assertRaises(Thread.DoesNotExist):
             Thread.objects.get(pk=self.thread.pk)
             Thread.objects.get(pk=self.thread.pk)
 
 
-        # best answer is kept sin other thread
+        # best answer is kept in other thread
         other_thread = Thread.objects.get(pk=other_thread.pk)
         other_thread = Thread.objects.get(pk=other_thread.pk)
         self.assertEqual(other_thread.best_answer, best_answer)
         self.assertEqual(other_thread.best_answer, best_answer)
 
 
@@ -469,7 +469,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         with self.assertRaises(Thread.DoesNotExist):
         with self.assertRaises(Thread.DoesNotExist):
             Thread.objects.get(pk=self.thread.pk)
             Thread.objects.get(pk=self.thread.pk)
 
 
-        # best answer is kept sin other thread
+        # best answer is kept in other thread
         other_thread = Thread.objects.get(pk=other_thread.pk)
         other_thread = Thread.objects.get(pk=other_thread.pk)
         self.assertEqual(other_thread.best_answer, best_answer)
         self.assertEqual(other_thread.best_answer, best_answer)
 
 
@@ -538,8 +538,8 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(
         self.assertEqual(
             Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
             Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
 
 
-    def test_threads_merge_conflict_delete_all_best_answers(self):
-        """api unmarks all best answers when delete all choice is selected"""
+    def test_threads_merge_conflict_unmark_all_best_answers(self):
+        """api unmarks all best answers when unmark all choice is selected"""
         self.override_acl({'can_merge_threads': 1})
         self.override_acl({'can_merge_threads': 1})
         self.override_other_acl({'can_merge_threads': 1})
         self.override_other_acl({'can_merge_threads': 1})
 
 
@@ -636,7 +636,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
             Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
             Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
 
 
     def test_merge_threads_kept_poll(self):
     def test_merge_threads_kept_poll(self):
-        """api merges two threads successfully, keeping poll from old thread"""
+        """api merges two threads successfully, keeping poll from other thread"""
         self.override_acl({'can_merge_threads': 1})
         self.override_acl({'can_merge_threads': 1})
         self.override_other_acl({'can_merge_threads': 1})
         self.override_other_acl({'can_merge_threads': 1})
 
 
@@ -662,7 +662,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(PollVote.objects.filter(poll=poll, thread=other_thread).count(), 4)
         self.assertEqual(PollVote.objects.filter(poll=poll, thread=other_thread).count(), 4)
 
 
     def test_merge_threads_moved_poll(self):
     def test_merge_threads_moved_poll(self):
-        """api merges two threads successfully, moving poll from other thread"""
+        """api merges two threads successfully, moving poll from old thread"""
         self.override_acl({'can_merge_threads': 1})
         self.override_acl({'can_merge_threads': 1})
         self.override_other_acl({'can_merge_threads': 1})
         self.override_other_acl({'can_merge_threads': 1})
 
 

+ 200 - 11
misago/threads/tests/test_threads_merge_api.py

@@ -790,7 +790,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
 
 
         self.assertEqual(
         self.assertEqual(
             list(postreads.values_list('post_id', flat=True)),
             list(postreads.values_list('post_id', flat=True)),
-            [self.thread.first_post_id, thread.first_post_id]
+            [self.thread.first_post_id, thread.first_post_id],
         )
         )
         self.assertEqual(postreads.filter(thread=new_thread).count(), 2)
         self.assertEqual(postreads.filter(thread=new_thread).count(), 2)
         self.assertEqual(postreads.filter(category=self.category).count(), 2)
         self.assertEqual(postreads.filter(category=self.category).count(), 2)
@@ -800,8 +800,191 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.user.subscription_set.get(thread=new_thread)
         self.user.subscription_set.get(thread=new_thread)
         self.user.subscription_set.get(category=self.category)
         self.user.subscription_set.get(category=self.category)
 
 
+    def test_merge_threads_merged_best_answer(self):
+        """api merges two threads successfully, moving best answer to old thread"""
+        self.override_acl({'can_merge_threads': 1})
+
+        other_thread = testutils.post_thread(self.category)
+
+        best_answer = testutils.reply_thread(self.thread)
+        self.thread.set_best_answer(self.user, best_answer)
+        self.thread.save()
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+            }),
+            content_type="application/json",
+        )
+        self.assertEqual(response.status_code, 200)
+        
+        # best answer is set on new thread
+        new_thread = Thread.objects.get(pk=response.json()['id'])
+        self.assertEqual(new_thread.best_answer_id, best_answer.id)
+
+    def test_merge_threads_merge_conflict_best_answer(self):
+        """api errors on merge conflict, returning list of available best answers"""
+        self.override_acl({'can_merge_threads': 1})
+
+        best_answer = testutils.reply_thread(self.thread)
+        self.thread.set_best_answer(self.user, best_answer)
+        self.thread.save()
+        
+        other_thread = testutils.post_thread(self.category)
+        other_best_answer = testutils.reply_thread(other_thread)
+        other_thread.set_best_answer(self.user, other_best_answer)
+        other_thread.save()
+
+        response = self.client.post(
+            self.api_link, 
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+            }),
+            content_type="application/json",
+        )
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {
+            'best_answers': [
+                ['0', "Unmark all best answers"],
+                [str(self.thread.id), self.thread.title],
+                [str(other_thread.id), other_thread.title],
+            ]
+        })
+
+        # best answers were untouched
+        self.assertEqual(self.thread.post_set.count(), 2)
+        self.assertEqual(other_thread.post_set.count(), 2)
+        self.assertEqual(Thread.objects.get(pk=self.thread.pk).best_answer_id, best_answer.id)
+        self.assertEqual(
+            Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
+
+    def test_threads_merge_conflict_best_answer_invalid_resolution(self):
+        """api errors on invalid merge conflict resolution"""
+        self.override_acl({'can_merge_threads': 1})
+
+        best_answer = testutils.reply_thread(self.thread)
+        self.thread.set_best_answer(self.user, best_answer)
+        self.thread.save()
+        
+        other_thread = testutils.post_thread(self.category)
+        other_best_answer = testutils.reply_thread(other_thread)
+        other_thread.set_best_answer(self.user, other_best_answer)
+        other_thread.save()
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+                'best_answer': other_thread.id + 10,
+            }),
+            content_type="application/json",
+        )
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {'best_answer': ["Invalid choice."]})
+
+        # best answers were untouched
+        self.assertEqual(self.thread.post_set.count(), 2)
+        self.assertEqual(other_thread.post_set.count(), 2)
+        self.assertEqual(Thread.objects.get(pk=self.thread.pk).best_answer_id, best_answer.id)
+        self.assertEqual(
+            Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id)
+
+    def test_threads_merge_conflict_unmark_all_best_answers(self):
+        """api unmarks all best answers when unmark all choice is selected"""
+        self.override_acl({'can_merge_threads': 1})
+
+        best_answer = testutils.reply_thread(self.thread)
+        self.thread.set_best_answer(self.user, best_answer)
+        self.thread.save()
+        
+        other_thread = testutils.post_thread(self.category)
+        other_best_answer = testutils.reply_thread(other_thread)
+        other_thread.set_best_answer(self.user, other_best_answer)
+        other_thread.save()
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+                'best_answer': 0,
+            }),
+            content_type="application/json",
+        )
+        self.assertEqual(response.status_code, 200)
+
+        # best answer is not set on new thread
+        new_thread = Thread.objects.get(pk=response.json()['id'])
+        self.assertIsNone(new_thread.best_answer_id)
+
+    def test_threads_merge_conflict_keep_first_best_answer(self):
+        """api unmarks other best answer on merge"""
+        self.override_acl({'can_merge_threads': 1})
+
+        best_answer = testutils.reply_thread(self.thread)
+        self.thread.set_best_answer(self.user, best_answer)
+        self.thread.save()
+        
+        other_thread = testutils.post_thread(self.category)
+        other_best_answer = testutils.reply_thread(other_thread)
+        other_thread.set_best_answer(self.user, other_best_answer)
+        other_thread.save()
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+                'best_answer': self.thread.pk,
+            }),
+            content_type="application/json",
+        )
+        self.assertEqual(response.status_code, 200)
+
+        # selected best answer is set on new thread
+        new_thread = Thread.objects.get(pk=response.json()['id'])
+        self.assertEqual(new_thread.best_answer_id, best_answer.id)
+
+    def test_threads_merge_conflict_keep_other_best_answer(self):
+        """api unmarks first best answer on merge"""
+        self.override_acl({'can_merge_threads': 1})
+
+        best_answer = testutils.reply_thread(self.thread)
+        self.thread.set_best_answer(self.user, best_answer)
+        self.thread.save()
+        
+        other_thread = testutils.post_thread(self.category)
+        other_best_answer = testutils.reply_thread(other_thread)
+        other_thread.set_best_answer(self.user, other_best_answer)
+        other_thread.save()
+
+        response = self.client.post(
+            self.api_link,
+            json.dumps({
+                'threads': [self.thread.id, other_thread.id],
+                'title': 'Merged thread!',
+                'category': self.category.id,
+                'best_answer': other_thread.pk,
+            }),
+            content_type="application/json",
+        )
+        self.assertEqual(response.status_code, 200)
+
+        # selected best answer is set on new thread
+        new_thread = Thread.objects.get(pk=response.json()['id'])
+        self.assertEqual(new_thread.best_answer_id, other_best_answer.id)
+
     def test_merge_threads_kept_poll(self):
     def test_merge_threads_kept_poll(self):
-        """api merges two threads successfully, keeping poll from old thread"""
+        """api merges two threads successfully, keeping poll from other thread"""
         self.override_acl({'can_merge_threads': True})
         self.override_acl({'can_merge_threads': True})
 
 
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
@@ -829,7 +1012,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(PollVote.objects.count(), 4)
         self.assertEqual(PollVote.objects.count(), 4)
 
 
     def test_merge_threads_moved_poll(self):
     def test_merge_threads_moved_poll(self):
-        """api merges two threads successfully, moving poll from other thread"""
+        """api merges two threads successfully, moving poll from old thread"""
         self.override_acl({'can_merge_threads': True})
         self.override_acl({'can_merge_threads': True})
 
 
         other_thread = testutils.post_thread(self.category)
         other_thread = testutils.post_thread(self.category)
@@ -856,7 +1039,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(Poll.objects.count(), 1)
         self.assertEqual(Poll.objects.count(), 1)
         self.assertEqual(PollVote.objects.count(), 4)
         self.assertEqual(PollVote.objects.count(), 4)
 
 
-    def test_threads_merge_conflict(self):
+    def test_threads_merge_conflict_poll(self):
         """api errors on merge conflict, returning list of available polls"""
         """api errors on merge conflict, returning list of available polls"""
         self.override_acl({'can_merge_threads': True})
         self.override_acl({'can_merge_threads': True})
 
 
@@ -878,9 +1061,15 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(
         self.assertEqual(
             response.json(), {
             response.json(), {
                 'polls': [
                 'polls': [
-                    [0, "Delete all polls"],
-                    [poll.pk, poll.question],
-                    [other_poll.pk, other_poll.question],
+                    ['0', "Delete all polls"],
+                    [
+                        str(other_poll.pk),
+                        u'{} ({})'.format(other_poll.question, other_poll.thread.title),
+                    ],
+                    [
+                        str(poll.pk),
+                        u'{} ({})'.format(poll.question, poll.thread.title),
+                    ],
                 ],
                 ],
             }
             }
         )
         )
@@ -889,7 +1078,7 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
         self.assertEqual(Poll.objects.count(), 2)
         self.assertEqual(Poll.objects.count(), 2)
         self.assertEqual(PollVote.objects.count(), 8)
         self.assertEqual(PollVote.objects.count(), 8)
 
 
-    def test_threads_merge_conflict_invalid_resolution(self):
+    def test_threads_merge_conflict_poll_invalid_resolution(self):
         """api errors on invalid merge conflict resolution"""
         """api errors on invalid merge conflict resolution"""
         self.override_acl({'can_merge_threads': True})
         self.override_acl({'can_merge_threads': True})
 
 
@@ -904,21 +1093,21 @@ class ThreadsMergeApiTests(ThreadsApiTestCase):
                 'threads': [self.thread.id, other_thread.id],
                 'threads': [self.thread.id, other_thread.id],
                 'title': 'Merged thread!',
                 'title': 'Merged thread!',
                 'category': self.category.id,
                 'category': self.category.id,
-                'poll': 'dsa7dsadsa9789',
+                'poll': other_thread.poll.id + 10,
             }),
             }),
             content_type="application/json",
             content_type="application/json",
         )
         )
 
 
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.json(), {
         self.assertEqual(response.json(), {
-            'detail': "Invalid choice.",
+            'poll': ["Invalid choice."],
         })
         })
 
 
         # polls and votes were untouched
         # polls and votes were untouched
         self.assertEqual(Poll.objects.count(), 2)
         self.assertEqual(Poll.objects.count(), 2)
         self.assertEqual(PollVote.objects.count(), 8)
         self.assertEqual(PollVote.objects.count(), 8)
 
 
-    def test_threads_merge_conflict_delete_all(self):
+    def test_threads_merge_conflict_delete_all_polls(self):
         """api deletes all polls when delete all choice is selected"""
         """api deletes all polls when delete all choice is selected"""
         self.override_acl({'can_merge_threads': True})
         self.override_acl({'can_merge_threads': True})