Browse Source

delete thread poll api endpoint

Rafał Pitoń 8 years ago
parent
commit
fa0f26c563

+ 10 - 5
misago/threads/api/threadpoll.py

@@ -9,7 +9,7 @@ from misago.acl import add_acl
 from misago.core.shortcuts import get_int_or_404
 from misago.core.shortcuts import get_int_or_404
 
 
 from ..models import Poll, PollVote
 from ..models import Poll, PollVote
-from ..permissions.polls import allow_start_poll, allow_edit_poll
+from ..permissions.polls import allow_start_poll, allow_edit_poll, allow_delete_poll
 from ..serializers import PollSerializer, NewPollSerializer, EditPollSerializer
 from ..serializers import PollSerializer, NewPollSerializer, EditPollSerializer
 from ..viewmodels.thread import ForumThread
 from ..viewmodels.thread import ForumThread
 
 
@@ -83,11 +83,16 @@ class ViewSet(viewsets.ViewSet):
         else:
         else:
             return Response(serializer.errors, status=400)
             return Response(serializer.errors, status=400)
 
 
+    @transaction.atomic
+    def delete(self, request, thread_pk, pk):
+        thread = self.get_thread(request, thread_pk)
+        instance = self.get_poll(thread, pk)
+
+        allow_delete_poll(request.user, instance)
+
+        instance.delete()
 
 
-    # edit poll
-    # delete poll
-    # vote in poll
-    # see voters
+        return Response({'detail': 'ok'})
 
 
 
 
 class ThreadPollViewSet(ViewSet):
 class ThreadPollViewSet(ViewSet):

+ 36 - 5
misago/threads/permissions/polls.py

@@ -92,7 +92,7 @@ ACL's for targets
 def add_acl_to_poll(user, poll):
 def add_acl_to_poll(user, poll):
     poll.acl.update({
     poll.acl.update({
         'can_edit': can_edit_poll(user, poll),
         'can_edit': can_edit_poll(user, poll),
-        'can_delete': False,
+        'can_delete': can_delete_poll(user, poll),
     })
     })
 
 
 
 
@@ -157,17 +157,48 @@ def allow_edit_poll(user, target):
                 user.acl['poll_edit_time'])
                 user.acl['poll_edit_time'])
             raise PermissionDenied(message % {'minutes': user.acl['poll_edit_time']})
             raise PermissionDenied(message % {'minutes': user.acl['poll_edit_time']})
 
 
-    if target.is_over:
-        raise PermissionDenied(_("This poll is over. You can't edit it."))
+        if target.is_over:
+            raise PermissionDenied(_("This poll is over. You can't edit it."))
 
 
     if not category_acl.get('can_close_threads'):
     if not category_acl.get('can_close_threads'):
         if target.category.is_closed:
         if target.category.is_closed:
-            raise PermissionDenied(_("This category is closed. You can't edito polls in it."))
+            raise PermissionDenied(_("This category is closed. You can't edit polls in it."))
         if target.thread.is_closed:
         if target.thread.is_closed:
-            raise PermissionDenied(_("This thread is closed. You can't edito polls in it."))
+            raise PermissionDenied(_("This thread is closed. You can't edit polls in it."))
 can_edit_poll = return_boolean(allow_edit_poll)
 can_edit_poll = return_boolean(allow_edit_poll)
 
 
 
 
+def allow_delete_poll(user, target):
+    if user.is_anonymous():
+        raise PermissionDenied(_("You have to sign in to delete polls."))
+
+    category_acl = user.acl['categories'].get(target.category_id, {
+        'can_close_threads': False,
+    })
+
+    if not user.acl.get('can_delete_polls'):
+        raise PermissionDenied(_("You can't delete polls."))
+
+    if user.acl.get('can_delete_polls') < 2:
+        if user.pk != target.poster_id:
+            raise PermissionDenied(_("You can't delete other users polls in this category."))
+        if not has_time_to_edit_poll(user, target):
+            message = ungettext(
+                "You can't delete polls that are older than %(minutes)s minute.",
+                "You can't delete polls that are older than %(minutes)s minutes.",
+                user.acl['poll_edit_time'])
+            raise PermissionDenied(message % {'minutes': user.acl['poll_edit_time']})
+        if target.is_over:
+            raise PermissionDenied(_("This poll is over. You can't delete it."))
+
+    if not category_acl.get('can_close_threads'):
+        if target.category.is_closed:
+            raise PermissionDenied(_("This category is closed. You can't delete polls in it."))
+        if target.thread.is_closed:
+            raise PermissionDenied(_("This thread is closed. You can't delete polls in it."))
+can_delete_poll = return_boolean(allow_delete_poll)
+
+
 def has_time_to_edit_poll(user, target):
 def has_time_to_edit_poll(user, target):
     edit_time = user.acl['poll_edit_time']
     edit_time = user.acl['poll_edit_time']
     if edit_time:
     if edit_time:

+ 86 - 0
misago/threads/tests/test_thread_poll_api.py

@@ -1,5 +1,6 @@
 import json
 import json
 
 
+from django.contrib.auth import get_user_model
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
@@ -7,6 +8,7 @@ from misago.categories.models import Category
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 from .. import testutils
 from .. import testutils
+from ..models import Poll
 
 
 
 
 class ThreadPollApiTestCase(AuthenticatedUserTestCase):
 class ThreadPollApiTestCase(AuthenticatedUserTestCase):
@@ -49,3 +51,87 @@ class ThreadPollApiTestCase(AuthenticatedUserTestCase):
             new_acl['categories'][self.category.pk].update(category)
             new_acl['categories'][self.category.pk].update(category)
 
 
         override_acl(self.user, new_acl)
         override_acl(self.user, new_acl)
+
+    def mock_poll(self):
+        self.poll = self.thread.poll = Poll.objects.create(
+            category=self.category,
+            thread=self.thread,
+            poster=self.user,
+            poster_name=self.user.username,
+            poster_slug=self.user.slug,
+            poster_ip='127.0.0.1',
+            question="Lorem ipsum dolor met?",
+            choices=[
+                {
+                    'hash': 'aaaaaaaaaaaa',
+                    'label': 'Alpha',
+                    'votes': 1
+                },
+                {
+                    'hash': 'bbbbbbbbbbbb',
+                    'label': 'Beta',
+                    'votes': 0
+                },
+                {
+                    'hash': 'gggggggggggg',
+                    'label': 'Gamma',
+                    'votes': 2
+                },
+                {
+                    'hash': 'dddddddddddd',
+                    'label': 'Delta',
+                    'votes': 1
+                }
+            ],
+            allowed_choices=2,
+            votes=4
+        )
+
+        # one user voted for Alpha choice
+        User = get_user_model()
+        user = User.objects.create_user('bob', 'bob@test.com', 'Pass.123')
+
+        self.poll.pollvote_set.create(
+            category=self.category,
+            thread=self.thread,
+            voter=user,
+            voter_name=user.username,
+            voter_slug=user.slug,
+            voter_ip='127.0.0.1',
+            choice_hash='aaaaaaaaaaaa'
+        )
+
+        # test user voted on third and last choices
+        self.poll.pollvote_set.create(
+            category=self.category,
+            thread=self.thread,
+            voter=self.user,
+            voter_name=self.user.username,
+            voter_slug=self.user.slug,
+            voter_ip='127.0.0.1',
+            choice_hash='gggggggggggg'
+        )
+        self.poll.pollvote_set.create(
+            category=self.category,
+            thread=self.thread,
+            voter=self.user,
+            voter_name=self.user.username,
+            voter_slug=self.user.slug,
+            voter_ip='127.0.0.1',
+            choice_hash='dddddddddddd'
+        )
+
+        # somebody else voted on third option before being deleted
+        self.poll.pollvote_set.create(
+            category=self.category,
+            thread=self.thread,
+            voter_name='deleted',
+            voter_slug='deleted',
+            voter_ip='127.0.0.1',
+            choice_hash='gggggggggggg'
+        )
+
+        self.api_link = reverse('misago:api:thread-poll-detail', kwargs={
+            'thread_pk': self.thread.pk,
+            'pk': self.poll.pk
+        })

+ 172 - 0
misago/threads/tests/test_thread_polldelete_api.py

@@ -0,0 +1,172 @@
+from datetime import timedelta
+
+from django.core.urlresolvers import reverse
+from django.utils import timezone
+
+from ..models import Poll, PollVote
+from .test_thread_poll_api import ThreadPollApiTestCase
+
+
+class ThreadPollDeleteTests(ThreadPollApiTestCase):
+    def setUp(self):
+        super(ThreadPollDeleteTests, self).setUp()
+
+        self.mock_poll()
+
+    def test_anonymous(self):
+        """api requires you to sign in to delete poll"""
+        self.logout_user()
+
+        response = self.client.delete(self.api_link)
+        self.assertEqual(response.status_code, 403)
+
+    def test_invalid_thread_id(self):
+        """api validates that thread id is integer"""
+        api_link = reverse('misago:api:thread-poll-detail', kwargs={
+            'thread_pk': 'kjha6dsa687sa',
+            'pk': self.poll.pk
+        })
+
+        response = self.client.delete(api_link)
+        self.assertEqual(response.status_code, 404)
+
+    def test_nonexistant_thread_id(self):
+        """api validates that thread exists"""
+        api_link = reverse('misago:api:thread-poll-detail', kwargs={
+            'thread_pk': self.thread.pk + 1,
+            'pk': self.poll.pk
+        })
+
+        response = self.client.delete(api_link)
+        self.assertEqual(response.status_code, 404)
+
+    def test_invalid_poll_id(self):
+        """api validates that poll id is integer"""
+        api_link = reverse('misago:api:thread-poll-detail', kwargs={
+            'thread_pk': self.thread.pk,
+            'pk': 'sad98as7d97sa98'
+        })
+
+        response = self.client.delete(api_link)
+        self.assertEqual(response.status_code, 404)
+
+    def test_nonexistant_poll_id(self):
+        """api validates that poll exists"""
+        api_link = reverse('misago:api:thread-poll-detail', kwargs={
+            'thread_pk': self.thread.pk,
+            'pk': self.poll.pk + 123
+        })
+
+        response = self.client.delete(api_link)
+        self.assertEqual(response.status_code, 404)
+
+    def test_no_permission(self):
+        """api validates that user has permission to delete poll in thread"""
+        self.override_acl({
+            'can_delete_polls': 0
+        })
+
+        response = self.client.delete(self.api_link)
+        self.assertContains(response, "can't delete polls", status_code=403)
+
+    def test_no_permission_timeout(self):
+        """api validates that user's window to delete poll in thread has closed"""
+        self.override_acl({
+            'can_delete_polls': 1,
+            'poll_edit_time': 5
+        })
+
+        self.poll.posted_on = timezone.now() - timedelta(minutes=15)
+        self.poll.save()
+
+        response = self.client.delete(self.api_link)
+        self.assertContains(response, "can't delete polls that are older than 5 minutes", status_code=403)
+
+    def test_no_permission_poll_closed(self):
+        """api validates that user's window to delete poll in thread has closed"""
+        self.override_acl({
+            'can_delete_polls': 1
+        })
+
+        self.poll.posted_on = timezone.now() - timedelta(days=15)
+        self.poll.length = 5
+        self.poll.save()
+
+        response = self.client.delete(self.api_link)
+        self.assertContains(response, "This poll is over", status_code=403)
+
+    def test_no_permission_other_user_poll(self):
+        """api validates that user has permission to delete other user poll in thread"""
+        self.override_acl({
+            'can_delete_polls': 1
+        })
+
+        self.poll.poster = None
+        self.poll.save()
+
+        response = self.client.delete(self.api_link)
+        self.assertContains(response, "can't delete other users polls", status_code=403)
+
+    def test_no_permission_closed_thread(self):
+        """api validates that user has permission to delete poll in closed thread"""
+        self.override_acl(category={
+            'can_close_threads': 0
+        })
+
+        self.thread.is_closed = True
+        self.thread.save()
+
+        response = self.client.delete(self.api_link)
+        self.assertContains(response, "thread is closed", status_code=403)
+
+        self.override_acl(category={
+            'can_close_threads': 1
+        })
+
+        response = self.client.delete(self.api_link)
+        self.assertEqual(response.status_code, 200)
+
+    def test_no_permission_closed_category(self):
+        """api validates that user has permission to delete poll in closed category"""
+        self.override_acl(category={
+            'can_close_threads': 0
+        })
+
+        self.category.is_closed = True
+        self.category.save()
+
+        response = self.client.delete(self.api_link)
+        self.assertContains(response, "category is closed", status_code=403)
+
+        self.override_acl(category={
+            'can_close_threads': 1
+        })
+
+        response = self.client.delete(self.api_link)
+        self.assertEqual(response.status_code, 200)
+
+    def test_poll_delete(self):
+        """api deletes poll and associated votes"""
+        response = self.client.delete(self.api_link)
+        self.assertEqual(response.status_code, 200)
+
+        self.assertEqual(Poll.objects.count(), 0)
+        self.assertEqual(PollVote.objects.count(), 0)
+
+    def test_other_user_poll_delete(self):
+        """api deletes other user's poll and associated votes, even if its over"""
+        self.override_acl({
+            'can_delete_polls': 2,
+            'poll_edit_time': 5
+        })
+
+        self.poll.poster = None
+        self.poll.posted_on = timezone.now() - timedelta(days=15)
+        self.poll.length = 5
+        self.poll.save()
+
+        response = self.client.delete(self.api_link)
+        self.assertEqual(response.status_code, 200)
+
+        self.assertEqual(Poll.objects.count(), 0)
+        self.assertEqual(PollVote.objects.count(), 0)

+ 56 - 86
misago/threads/tests/test_thread_polledit_api.py

@@ -1,10 +1,8 @@
 from datetime import timedelta
 from datetime import timedelta
 
 
-from django.contrib.auth import get_user_model
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.utils import timezone
 from django.utils import timezone
 
 
-from ..models import Poll
 from ..serializers.poll import MAX_POLL_OPTIONS
 from ..serializers.poll import MAX_POLL_OPTIONS
 from .test_thread_poll_api import ThreadPollApiTestCase
 from .test_thread_poll_api import ThreadPollApiTestCase
 
 
@@ -13,92 +11,10 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
     def setUp(self):
     def setUp(self):
         super(ThreadPollEditTests, self).setUp()
         super(ThreadPollEditTests, self).setUp()
 
 
-        # mock poll
-        self.poll = self.thread.poll = Poll.objects.create(
-            category=self.category,
-            thread=self.thread,
-            poster=self.user,
-            poster_name=self.user.username,
-            poster_slug=self.user.slug,
-            poster_ip='127.0.0.1',
-            question="Lorem ipsum dolor met?",
-            choices=[
-                {
-                    'hash': 'aaaaaaaaaaaa',
-                    'label': 'Alpha',
-                    'votes': 1
-                },
-                {
-                    'hash': 'bbbbbbbbbbbb',
-                    'label': 'Beta',
-                    'votes': 0
-                },
-                {
-                    'hash': 'gggggggggggg',
-                    'label': 'Gamma',
-                    'votes': 2
-                },
-                {
-                    'hash': 'dddddddddddd',
-                    'label': 'Delta',
-                    'votes': 1
-                }
-            ],
-            allowed_choices=2,
-            votes=4
-        )
-
-        # one user voted for Alpha choice
-        User = get_user_model()
-        user = User.objects.create_user('bob', 'bob@test.com', 'Pass.123')
-
-        self.poll.pollvote_set.create(
-            category=self.category,
-            thread=self.thread,
-            voter=user,
-            voter_name=user.username,
-            voter_slug=user.slug,
-            voter_ip='127.0.0.1',
-            choice_hash='aaaaaaaaaaaa'
-        )
-
-        # test user voted on third and last choices
-        self.poll.pollvote_set.create(
-            category=self.category,
-            thread=self.thread,
-            voter=self.user,
-            voter_name=self.user.username,
-            voter_slug=self.user.slug,
-            voter_ip='127.0.0.1',
-            choice_hash='gggggggggggg'
-        )
-        self.poll.pollvote_set.create(
-            category=self.category,
-            thread=self.thread,
-            voter=self.user,
-            voter_name=self.user.username,
-            voter_slug=self.user.slug,
-            voter_ip='127.0.0.1',
-            choice_hash='dddddddddddd'
-        )
-
-        # somebody else voted on third option before being deleted
-        self.poll.pollvote_set.create(
-            category=self.category,
-            thread=self.thread,
-            voter_name='deleted',
-            voter_slug='deleted',
-            voter_ip='127.0.0.1',
-            choice_hash='gggggggggggg'
-        )
-
-        self.api_link = reverse('misago:api:thread-poll-detail', kwargs={
-            'thread_pk': self.thread.pk,
-            'pk': self.poll.pk
-        })
+        self.mock_poll()
 
 
     def test_anonymous(self):
     def test_anonymous(self):
-        """api requires you to sign in to create poll"""
+        """api requires you to sign in to edit poll"""
         self.logout_user()
         self.logout_user()
 
 
         response = self.put(self.api_link)
         response = self.put(self.api_link)
@@ -559,3 +475,57 @@ class ThreadPollEditTests(ThreadPollApiTestCase):
         self.assertEqual(response_json['votes'], 1)
         self.assertEqual(response_json['votes'], 1)
         self.assertEqual(self.poll.pollvote_set.count(), 1)
         self.assertEqual(self.poll.pollvote_set.count(), 1)
 
 
+    def test_moderate_user_poll(self):
+        """api edits all poll choices out in other users poll, even if its over"""
+        self.override_acl({
+            'can_edit_polls': 2,
+            'poll_edit_time': 5
+        })
+
+        self.poll.poster = None
+        self.poll.posted_on = timezone.now() - timedelta(days=15)
+        self.poll.length = 5
+        self.poll.save()
+
+        response = self.put(self.api_link, data={
+            'length': 40,
+            'question': "Select two best colors",
+            'allowed_choices': 2,
+            'allow_revotes': True,
+            'is_public': True,
+            'choices': [
+                {
+                    'label': '\nRed  '
+                },
+                {
+                    'label': 'Green'
+                },
+                {
+                    'label': 'Blue'
+                }
+            ]
+        })
+        self.assertEqual(response.status_code, 200)
+
+        response_json = response.json()
+
+        self.assertEqual(response_json['poster_name'], self.user.username)
+        self.assertEqual(response_json['poster_slug'], self.user.slug)
+        self.assertEqual(response_json['length'], 40)
+        self.assertEqual(response_json['question'], "Select two best colors")
+        self.assertEqual(response_json['allowed_choices'], 2)
+        self.assertTrue(response_json['allow_revotes'])
+
+        # you can't change poll's type after its posted
+        self.assertFalse(response_json['is_public'])
+
+        # choices were updated
+        self.assertEqual(len(response_json['choices']), 3)
+        self.assertEqual(len(set([c['hash'] for c in response_json['choices']])), 3)
+        self.assertEqual([c['label'] for c in response_json['choices']], ['Red', 'Green', 'Blue'])
+        self.assertEqual([c['votes'] for c in response_json['choices']], [0, 0, 0])
+        self.assertEqual([c['selected'] for c in response_json['choices']], [False, False, False])
+
+        # votes were removed
+        self.assertEqual(response_json['votes'], 0)
+        self.assertEqual(self.poll.pollvote_set.count(), 0)