Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

django-tastypie REST API for django-polls #7

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9ff0321
updated to work with django 1.6
armicron Feb 17, 2015
b47358e
fixes: anonymous user can't view poll details, IntegrityError
armicron Feb 18, 2015
03eb828
new fields, migration
armicron Feb 18, 2015
d7ac0ba
added exceptions
armicron Feb 20, 2015
32be967
model migration: allow anonymous users to vote, allow multiple choice…
armicron Feb 20, 2015
27d341c
added vote and count_percentage functions
armicron Feb 20, 2015
a04c21e
dummy api (commit will be rewrited/deleted)
armicron Feb 20, 2015
d34bd60
renamed vote.datetime field to vote.created
armicron Feb 24, 2015
c581ea5
UUIDField was replaced with CharField
armicron Feb 24, 2015
cc08c7a
uuid4 has 36 chars and reference should be unique
armicron Feb 27, 2015
825330f
modified PollResource and ResultResource
armicron Mar 2, 2015
6c4d323
user shouldn't be able to vote multiple times for same choice
armicron Mar 3, 2015
e351da6
changed authentication, user can vote only for himself
armicron Mar 3, 2015
517a889
start_votes and env_votes should be aware
armicron Mar 5, 2015
4d97af5
limit user's list, create a Vote with Poll.vote() method, added 'alre…
armicron Mar 10, 2015
da0c230
can_vote renamed to alredy_voted, a bug fixed in the Poll.vote method
armicron Mar 10, 2015
a16832d
splitting tests, api tests added
armicron Mar 10, 2015
adab97f
fixed 'Duplicate index 'polls_poll_reference_4cacbf22888a7509_uniq' d…
armicron Mar 11, 2015
8fb9217
tests are working with mysql now
armicron Mar 11, 2015
a06e550
support name spaced url/uri, poll and result by reference
miraculixx Mar 14, 2015
264fd8a
added django-extensions
miraculixx Mar 14, 2015
c6d9924
Merge pull request #1 from armicron/tastypie
miraculixx Mar 14, 2015
27e3e05
Merge pull request #2 from miraculixx/miraculixx-tastypie
miraculixx Mar 14, 2015
bfb5ee9
various fixes, add code field
miraculixx Mar 18, 2015
7798534
add more unit tests, fix some problems
miraculixx Mar 18, 2015
b2e0904
bump version
miraculixx Mar 18, 2015
0ffd10e
set django dependency
miraculixx Mar 18, 2015
cf8ac2d
Merge pull request #3 from miraculixx/miraculixx-tastypie
miraculixx Mar 18, 2015
6892874
add comment if it is submitted...
miraculixx May 3, 2015
8c91979
add test for comment in api
miraculixx May 3, 2015
59d9e77
bump version
miraculixx May 3, 2015
7c42a00
Merge pull request #4 from miraculixx/miraculixx-tastypie
miraculixx Mar 28, 2016
8333ab7
migrate to django 1.7, fixed bugs, increased stability
miraculixx Mar 28, 2016
8d10cf1
bumped version
miraculixx Mar 28, 2016
ae67d26
Merge pull request #5 from miraculixx/django17
miraculixx Mar 28, 2016
ae67f2e
accept votes by poll reference
miraculixx Apr 24, 2016
6fd7fd1
Merge pull request #7 from miraculixx/fix-6
miraculixx Apr 24, 2016
ead185d
bumped version
miraculixx Apr 24, 2016
1c0b316
allow multiple votes
miraculixx Apr 24, 2016
67a3f33
Merge pull request #9 from miraculixx/miraculixx-8
miraculixx Apr 24, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .project
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>django-polls</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>
8 changes: 8 additions & 0 deletions .pydevproject
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/${PROJECT_DIR_NAME}</path>
</pydev_pathproperty>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">django-polls</pydev_property>
</pydev_project>
11 changes: 11 additions & 0 deletions .settings/org.eclipse.core.resources.prefs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
eclipse.preferences.version=1
encoding//polls/migrations/0001_initial.py=utf-8
encoding//polls/south_migrations/0001_initial.py=utf-8
encoding//polls/south_migrations/0002_auto__add_vote__add_field_poll_description.py=utf-8
encoding//polls/south_migrations/0003_auto__add_unique_vote_poll_user.py=utf-8
encoding//polls/south_migrations/0004_auto__add_field_vote_comment__add_field_vote_datetime__add_field_vote_.py=utf-8
encoding//polls/south_migrations/0005_auto__chg_field_vote_user__del_unique_vote_user_poll.py=utf-8
encoding//polls/south_migrations/0006_auto__del_field_vote_datetime__add_field_vote_created.py=utf-8
encoding//polls/south_migrations/0007_auto__add_unique_vote_user_poll_choice__chg_field_poll_reference__add_.py=utf-8
encoding//polls/south_migrations/0008_auto__add_field_choice_code.py=utf-8
encoding//polls/south_migrations/0009_auto__add_unique_choice_poll_code.py=utf-8
2 changes: 1 addition & 1 deletion polls/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.1.0'
__version__ = '0.2.5dev'
3 changes: 2 additions & 1 deletion polls/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ class PollAdmin(admin.ModelAdmin):

class VoteAdmin(admin.ModelAdmin):
model = Vote
list_display = ('choice', 'user', 'poll')
list_display = ('choice', 'user', 'poll', 'created')
readonly_fields = ('created',)

admin.site.register(Poll, PollAdmin)
admin.site.register(Vote, VoteAdmin)
216 changes: 216 additions & 0 deletions polls/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
'''
Api("v1/poll")
POST /poll/ -- create a new poll, shall allow to post choices in the same API call
POST /choice/ -- add a choice to an existing poll
POST /vote/ -- vote on poll with pk
PUT /choice/ -- update choice data
PUT /poll/ -- update poll data
GET /poll/ -- retrieve the poll information, including choice details
GET /result/ -- retrieve the statistics on the poll.
This shall return a JSON formatted like so. Note the actual statistics calculation shall be implemented
in poll.service.stats (later on, this will be externalized into a batch job).
'''

from exceptions import PollClosed, PollNotOpen, PollNotAnonymous, PollNotMultiple
import json

from django.conf.urls import url
from django.contrib.auth import get_user_model
from django.core.urlresolvers import resolve
from django.forms.models import model_to_dict
from tastypie import fields
from tastypie import http
from tastypie.authentication import MultiAuthentication, BasicAuthentication, SessionAuthentication, \
Authentication
from tastypie.authorization import Authorization, \
DjangoAuthorization
from tastypie.exceptions import ImmediateHttpResponse
from tastypie.resources import ALL, NamespacedModelResource

from polls.exceptions import PollInvalidChoice
from polls.models import Poll, Choice, Vote
from polls.util import ReasonableDjangoAuthorization, IPAuthentication


class UserResource(NamespacedModelResource):

class Meta:
queryset = get_user_model().objects.all()
allowed_methods = ['get']
resource_name = 'user'
always_return_data = True
authentication = MultiAuthentication(
BasicAuthentication(), SessionAuthentication())
authorization = ReasonableDjangoAuthorization(read_detail='')
excludes = ['date_joined', 'password', 'is_superuser',
'is_staff', 'is_active', 'last_login', 'first_name', 'last_name']
filtering = {
'username': ALL,
}

def limit_list_by_user(self, request, object_list):
"""
limit the request object list to its own profile, except
for superusers. Superusers get a list of all users

note that for POST requests tastypie internally
queries get_object_list, and we should return a valid
list
"""
view, args, kwargs = resolve(request.path)
if request.method == 'GET' and not 'pk' in kwargs and not request.user.is_superuser:
return object_list.filter(pk=request.user.pk)
return object_list

def get_object_list(self, request):
object_list = super(UserResource, self).get_object_list(request)
object_list = self.limit_list_by_user(request, object_list)
return object_list


class PollResource(NamespacedModelResource):
# POST, GET, PUT
# user = fields.ForeignKey(UserResource, 'user')

class Meta:
queryset = Poll.objects.all()
allowed_methods = ['get', 'post', 'put']
resource_name = 'poll'
always_return_data = True
# anyone can list and get polls, otherwise Django auth kicks in
authentication = MultiAuthentication(
BasicAuthentication(), SessionAuthentication(), Authentication())
authorization = ReasonableDjangoAuthorization(read_list='',
read_detail='')
filtering = {
'reference': 'exact',
}

def obj_create(self, bundle, **kwargs):
return super(PollResource, self).obj_create(bundle, user=bundle.request.user)

def dehydrate(self, bundle):
choices = Choice.objects.filter(poll=bundle.data['id'])
bundle.data['choices'] = [model_to_dict(choice) for choice in choices]
return bundle

def alter_detail_data_to_serialize(self, request, data):
data.data['already_voted'] = Poll.objects.get(
pk=data.data.get('id')).already_voted(user=request.user)
return data

def prepend_urls(self):
""" match by pk or reference """
return [
url(r"^(?P<resource_name>%s)/(?P<pk>[0-9]+)/$" % self._meta.resource_name,
self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
url(r"^(?P<resource_name>%s)/(?P<reference>[\w-]+)/$" % self._meta.resource_name,
self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
]


class ChoiceResource(NamespacedModelResource):
poll = fields.ToOneField(PollResource, 'poll')

class Meta:
queryset = Choice.objects.all()
allowed_methods = ['post', 'put']
authentication = MultiAuthentication(
BasicAuthentication(), SessionAuthentication())
authorization = DjangoAuthorization()
resource_name = 'choice'
always_return_data = True


class VoteResource(NamespacedModelResource):
user = fields.ToOneField(
UserResource, 'user', blank=True, null=True, readonly=True)
choice = fields.ToOneField(ChoiceResource, 'choice', readonly=True)
poll = fields.ToOneField(PollResource, 'poll', readonly=True)

class Meta:
queryset = Vote.objects.all()
allowed_methods = ['post', 'put']
# by default require authentication but regress for anonymous votes
authentication = IPAuthentication(BasicAuthentication(),
SessionAuthentication(),
Authentication())
# anyone can vote
authorization = Authorization()
resource_name = 'vote'
always_return_data = True

def obj_create(self, bundle, **kwargs):
poll = PollResource().get_via_uri(bundle.data.get('poll'))
if not poll.already_voted(bundle.request.user):
try:
choices = bundle.data.get('choice')
# convert single-choice into list
if isinstance(choices, basestring):
choices = [choices]
votes = poll.vote(choices=choices,
data=bundle.data.get('data'),
user=bundle.request.user,
comment=bundle.data.get('comment'))
except (PollClosed, PollNotOpen, PollNotAnonymous, PollNotMultiple):
raise ImmediateHttpResponse(
response=http.HttpForbidden('not allowed'))
except PollInvalidChoice:
raise ImmediateHttpResponse(
response=http.HttpBadRequest('invalid data'))
else:
bundle.obj = votes[0]
else:
raise ImmediateHttpResponse(
response=http.HttpForbidden('already voted'))
return bundle

def obj_update(self, bundle, **kwargs):
poll = PollResource().get_via_uri(bundle.data.get('poll'))
# non anonymous votes by the same user can be modified
if not poll.is_anonymous and bundle.obj.user == bundle.request.user:
bundle.obj.change_vote(choices=bundle.data.get('choice'),
data=bundle.data.get('data'),
user=bundle.request.user)
else:
raise ImmediateHttpResponse(
response=http.HttpForbidden('already voted'))

def dehydrate(self, bundle):
# convert JSON Field
bundle = super(VoteResource, self).dehydrate(bundle)
bundle.data['data'] = json.dumps(bundle.obj.data)
# represent values as strings
bundle.data['poll'] = self.get_resource_uri(bundle.obj.poll)
bundle.data['resource_uri'] = self.get_resource_uri(bundle.obj)
bundle.data['choice'] = bundle.obj.choice.code
return bundle


class ResultResource(NamespacedModelResource):

class Meta:
queryset = Poll.objects.all()
allowed_methods = ['get']
# anyone can get results
authentication = MultiAuthentication(
BasicAuthentication(), SessionAuthentication(), Authentication())
authorization = Authorization()
resource_name = 'result'
always_return_data = True
excludes = ['description', 'start_votes', 'end_votes',
'is_anonymous', 'is_multiple', 'is_closed', 'reference']

def prepend_urls(self):
""" match by pk or reference """
return [
url(r"^(?P<resource_name>%s)/(?P<pk>[0-9]+)/$" % self._meta.resource_name,
self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
url(r"^(?P<resource_name>%s)/(?P<reference>[\w-]+)/$" % self._meta.resource_name,
self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
]

def dehydrate(self, bundle):
poll = bundle.obj
bundle.data['stats'] = poll.get_stats()
return bundle
6 changes: 6 additions & 0 deletions polls/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class PollNotOpen(Exception): pass
class PollClosed(Exception): pass
class PollNotAnonymous(Exception): pass
class PollNotMultiple(Exception): pass
class PollChoiceRequired(Exception): pass
class PollInvalidChoice(Exception): pass
116 changes: 73 additions & 43 deletions polls/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,79 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from __future__ import unicode_literals

from django.db import models, migrations
import polls.models
import django.utils.timezone
from django.conf import settings
import django_extensions.db.fields.json
import uuid

class Migration(SchemaMigration):

def forwards(self, orm):
# Adding model 'Poll'
db.create_table('polls_poll', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('question', self.gf('django.db.models.fields.CharField')(max_length=255)),
))
db.send_create_signal('polls', ['Poll'])
class Migration(migrations.Migration):

# Adding model 'Choice'
db.create_table('polls_choice', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('poll', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['polls.Poll'])),
('choice', self.gf('django.db.models.fields.CharField')(max_length=255)),
))
db.send_create_signal('polls', ['Choice'])
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]


def backwards(self, orm):
# Deleting model 'Poll'
db.delete_table('polls_poll')

# Deleting model 'Choice'
db.delete_table('polls_choice')


models = {
'polls.choice': {
'Meta': {'object_name': 'Choice'},
'choice': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'poll': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['polls.Poll']"})
},
'polls.poll': {
'Meta': {'object_name': 'Poll'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.CharField', [], {'max_length': '255'})
}
}

complete_apps = ['polls']
operations = [
migrations.CreateModel(
name='Choice',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('choice', models.CharField(max_length=255)),
('code', models.CharField(default=b'', max_length=36, blank=True)),
],
options={
'ordering': ['poll', 'choice'],
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Poll',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('question', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('reference', models.CharField(default=uuid.uuid4, unique=True, max_length=36)),
('is_anonymous', models.BooleanField(default=False, help_text='Allow to vote for anonymous user')),
('is_multiple', models.BooleanField(default=False, help_text='Allow to make multiple choices')),
('is_closed', models.BooleanField(default=False, help_text='Do not accept votes')),
('start_votes', models.DateTimeField(default=django.utils.timezone.now, help_text='The earliest time votes get accepted')),
('end_votes', models.DateTimeField(default=polls.models.vote_endtime, help_text='The latest time votes get accepted')),
],
options={
'ordering': ['-start_votes'],
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Vote',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('comment', models.TextField(max_length=144, null=True, blank=True)),
('created', models.DateTimeField(auto_now_add=True)),
('data', django_extensions.db.fields.json.JSONField(null=True, blank=True)),
('choice', models.ForeignKey(to='polls.Choice')),
('poll', models.ForeignKey(to='polls.Poll')),
('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True)),
],
options={
'ordering': ['poll', 'choice'],
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name='vote',
unique_together=set([('user', 'poll', 'choice')]),
),
migrations.AddField(
model_name='choice',
name='poll',
field=models.ForeignKey(to='polls.Poll'),
preserve_default=True,
),
migrations.AlterUniqueTogether(
name='choice',
unique_together=set([('poll', 'code')]),
),
]
Loading