Skip to content
Snippets Groups Projects
Commit 1ef0b356 authored by Benoit Alessandroni's avatar Benoit Alessandroni
Browse files

Merge branch '5-limiting-votes' into 'master'

feature: one vote per user per poll

Closes #5

See merge request !16
parents 44724fb7 884a10d3
No related branches found
Tags v1.0.1
1 merge request!16feature: one vote per user per poll
Pipeline #7618 passed with stage
in 28 seconds
......@@ -2,6 +2,7 @@
image: python:3.6
stages:
- test
- release
publish:
......@@ -17,4 +18,15 @@ publish:
only:
- master
tags:
- deploy
\ No newline at end of file
- deploy
test:
stage: test
script:
- pip install .[dev]
- python -m unittest djangoldp_polls.tests.runner
except:
- master
- tags
tags:
- test
\ No newline at end of file
from django.contrib import admin
from guardian.admin import GuardedModelAdmin
from django.db import models
from .models import Poll
admin.site.register(Poll, GuardedModelAdmin)
......@@ -16,8 +16,8 @@ Including another URLconf
"""djangoldp project URL Configuration"""
from django.conf.urls import url,include
from .views import TotalVotes
from djangoldp.models import Model
from djangoldp_polls.views import TotalVotes, CanVoteOnPollViewSet
from djangoldp_polls.models import PollOption
urlpatterns = [
......@@ -28,4 +28,5 @@ urlpatterns = [
[]),
fields=Model.get_meta(PollOption, 'serializer_fields',[]),
nested_fields=Model.get_meta(PollOption, 'nested_fields', []))),
]
\ No newline at end of file
url(r'^polls/(?P<pk>[0-9]+)/can_vote/', CanVoteOnPollViewSet.as_view())
]
# Generated by Django 2.2.16 on 2020-09-23 13:55
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('djangoldp_polls', '0010_auto_20200923_0939'),
]
operations = [
migrations.AlterModelOptions(
name='poll',
options={'default_permissions': ('add', 'change', 'delete', 'view', 'control')},
),
migrations.AlterModelOptions(
name='polloption',
options={'default_permissions': ('add', 'change', 'delete', 'view', 'control')},
),
migrations.AlterModelOptions(
name='tag',
options={'default_permissions': ('add', 'change', 'delete', 'view', 'control')},
),
migrations.AlterModelOptions(
name='vote',
options={'default_permissions': ('add', 'change', 'delete', 'view', 'control')},
),
]
from django.conf import settings
from django.db import models
from djangoldp.models import Model
from django.db.models import Sum
from django.contrib.auth import get_user_model
from django.http import Http404
from rest_framework import serializers
from djangoldp.models import Model
from djangoldp.views import LDPViewSet
from djangoldp_conversation.models import Conversation
from djangoldp_circle.models import Circle
......@@ -17,7 +18,7 @@ from djangoldp_circle.models import Circle
class Tag (Model):
name = models.CharField(max_length=250,verbose_name="Name")
class Meta :
class Meta(Model.Meta):
serializer_fields = ['@id','name']
anonymous_perms = ['view']
authenticated_perms = ['inherit','add']
......@@ -29,7 +30,7 @@ class Tag (Model):
class PollOption (Model):
name = models.CharField(max_length=250,verbose_name="Options available for a vote")
class Meta :
class Meta(Model.Meta):
serializer_fields = ['@id','name']
nested_fields = ['userVote','relatedPollOptions']
anonymous_perms = ['view','add']
......@@ -54,7 +55,7 @@ class Poll (Model):
debate = models.ManyToManyField(Conversation, related_name='polls', blank=True)
circle = models.ForeignKey(Circle, null=True, related_name="polls", on_delete=models.SET_NULL)
class Meta :
class Meta(Model.Meta):
serializer_fields = ['@id','created_at','debate','pollOptions','votes','author','title','image','circle',\
'hostingOrganisation','startDate','endDate','shortDescription','longDescription','tags']
nested_fields = ['tags','votes','pollOptions','debate','circle']
......@@ -65,18 +66,39 @@ class Poll (Model):
return self.title
# I know this shouldn't live here, but putting it in views results in circular dependency problems
# https://git.startinblox.com/djangoldp-packages/djangoldp/issues/278
class VoteViewSet(LDPViewSet):
def is_safe_create(self, user, validated_data, *args, **kwargs):
try:
if 'poll' in validated_data.keys():
poll = Poll.objects.get(urlid=validated_data['poll']['urlid'])
else:
poll = self.get_parent()
if Vote.objects.filter(relatedPoll=poll, user=user).exists():
raise serializers.ValidationError('You may only vote on this poll once!')
except Poll.DoesNotExist:
return True
except (KeyError, AttributeError):
raise Http404('circle not specified with urlid')
return True
class Vote (Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='votes',null=True,blank=True, on_delete=models.SET_NULL)
chosenOption = models.ForeignKey(PollOption, related_name='userVote', on_delete=models.CASCADE)
relatedPoll = models.ForeignKey(Poll, related_name='votes', on_delete=models.CASCADE)
class Meta :
class Meta(Model.Meta):
auto_author = "user"
serializer_fields = ['@id','chosenOption','relatedPoll']
nested_fields = []
anonymous_perms = ['view','add','change']
authenticated_perms = ['inherit','add']
view_set = VoteViewSet
def __str__(self):
return self.chosenOption.__str__()
\ No newline at end of file
return self.chosenOption.__str__()
from django.contrib.auth.models import AbstractUser
from djangoldp.models import Model
class User(AbstractUser, Model):
class Meta(AbstractUser.Meta, Model.Meta):
serializer_fields = ['@id', 'username', 'first_name', 'last_name', 'email']
anonymous_perms = ['view', 'add']
authenticated_perms = ['inherit', 'change']
owner_perms = ['inherit']
import sys
import django
from django.conf import settings
from djangoldp.tests import settings_default
settings.configure(default_settings=settings_default,
DJANGOLDP_PACKAGES=['djangoldp_polls', 'djangoldp_circle', 'djangoldp_conversation' 'djangoldp_polls.tests', ],
INSTALLED_APPS=('django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.admin',
'django.contrib.messages',
'django.contrib.staticfiles',
'guardian',
'djangoldp_polls',
'djangoldp_polls.tests',
'djangoldp_circle',
'djangoldp_conversation',
'djangoldp',
),
SITE_URL='http://happy-dev.fr',
BASE_URL='http://happy-dev.fr',
REST_FRAMEWORK={
'DEFAULT_PAGINATION_CLASS': 'djangoldp.pagination.LDPPagination',
'PAGE_SIZE': 5
},
SEND_BACKLINKS=False,
JABBER_DEFAULT_HOST=None,
)
django.setup()
from django.test.runner import DiscoverRunner
test_runner = DiscoverRunner(verbosity=1)
failures = test_runner.run_tests([
'djangoldp_polls.tests.tests_votes',
])
if failures:
sys.exit(failures)
import uuid
import json
from datetime import datetime, timedelta
from django.conf import settings
from rest_framework.test import APITestCase, APIClient
from djangoldp_polls.models import Poll, PollOption, Vote
from djangoldp_polls.tests.models import User
class PermissionsTestCase(APITestCase):
def setUp(self):
self.client = APIClient()
self.setUpLoggedInUser()
def setUpLoggedInUser(self):
self.user = User(email='test@mactest.co.uk', first_name='Test', last_name='Mactest', username='test',
password='glass onion')
self.user.save()
self.client.force_authenticate(user=self.user)
def setUpPoll(self):
self.poll_option_a = PollOption.objects.create(name='Yes')
self.poll_option_b = PollOption.objects.create(name='No')
self.poll = self._get_poll('Test')
def _get_poll(self, title):
poll = Poll.objects.create(endDate=datetime.now(), title=title, hostingOrganisation='Test',
shortDescription='Hello', longDescription='Hello World')
poll.pollOptions.add(self.poll_option_a, self.poll_option_b)
return poll
def _get_poll_post_request(self, option):
return {
'@context': settings.LDP_RDF_CONTEXT,
'choiceValue': '',
'chosenOption': {'@id': option.urlid}
}
def test_can_vote_view(self):
# I should be able to vote
self.setUpPoll()
response = self.client.get('/polls/{}/can_vote/'.format(self.poll.pk))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, True)
# I should not be able to vote
Vote.objects.create(user=self.user, relatedPoll=self.poll, chosenOption=self.poll_option_a)
response = self.client.get('/polls/{}/can_vote/'.format(self.poll.pk))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, False)
def test_post_vote(self):
# post on one poll
self.setUpPoll()
body = self._get_poll_post_request(self.poll_option_a)
response = self.client.post('/polls/{}/votes/'.format(self.poll.pk), json.dumps(body), content_type='application/ld+json')
self.assertEqual(response.status_code, 201)
# post on another
other_poll = self._get_poll('Second Poll')
body = self._get_poll_post_request(self.poll_option_b)
response = self.client.post('/polls/{}/votes/'.format(other_poll.pk), json.dumps(body),
content_type='application/ld+json')
self.assertEqual(response.status_code, 201)
def test_post_vote_duplicate(self):
# post once
self.setUpPoll()
body = self._get_poll_post_request(self.poll_option_a)
response = self.client.post('/polls/{}/votes/'.format(self.poll.pk), json.dumps(body),
content_type='application/ld+json')
self.assertEqual(response.status_code, 201)
# post again - should be rejected
body = self._get_poll_post_request(self.poll_option_b)
response = self.client.post('/polls/{}/votes/'.format(self.poll.pk), json.dumps(body),
content_type='application/ld+json')
self.assertEqual(response.status_code, 400)
......@@ -4,12 +4,30 @@ from djangoldp.views import LDPViewSet
from datetime import datetime
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from .models import Poll,Vote
from .serializers import PollOptionSerializer
class CanVoteOnPollViewSet(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request, pk):
'''returns True if the user can vote, or False if they have already voted'''
try:
poll = Poll.objects.get(pk=pk)
can_vote = True
if Vote.objects.filter(relatedPoll=poll, user=request.user).exists():
can_vote = False
return Response(can_vote, status=status.HTTP_200_OK)
except Poll.DoesNotExist:
return Response(data={'error': {'poll': ['Could not find poll with this ID!']}},
status=status.HTTP_404_NOT_FOUND)
class FuturePollViewset(LDPViewSet):
model = Poll
......@@ -40,3 +58,8 @@ class TotalVotes(LDPViewSet):
def get_queryset(self, *args, **kwargs):
poll = self._get_poll_or_404()
return poll.pollOptions.all()
def get_serializer_class(self):
# NOTE: this is required because currently DjangoLDP overrides the serializer_class during __init__
# https://git.startinblox.com/djangoldp-packages/djangoldp/issues/241
return PollOptionSerializer
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment