← Back to team overview

gavel-team team mailing list archive

[Branch ~gavel-team/gavel/simple_gavel] Rev 5: Got a basic REST API working.

 

------------------------------------------------------------
revno: 5
committer: Joshua Gardner <mellowcellofellow@xxxxxxxxx>
branch nick: simple_gavel
timestamp: Wed 2010-03-31 22:29:57 -0600
message:
  Got a basic REST API working.
  Usage example with curl in file gavel/rest_api_curl_example.txt.
added:
  gavel/api/
  gavel/api/__init__.py
  gavel/api/tests.py
  gavel/api/urls.py
  gavel/rest_api_curl_example.txt
  gavel/rules/api/
  gavel/rules/api/__init__.py
  gavel/rules/api/handlers.py
modified:
  gavel/rules/models.py
  gavel/settings.py
  gavel/urls.py


--
lp:gavel
https://code.launchpad.net/~gavel-team/gavel/simple_gavel

Your team Gavel Team is subscribed to branch lp:gavel.
To unsubscribe from this branch go to https://code.launchpad.net/~gavel-team/gavel/simple_gavel/+edit-subscription
=== added directory 'gavel/api'
=== added file 'gavel/api/__init__.py'
=== added file 'gavel/api/tests.py'
--- gavel/api/tests.py	1970-01-01 00:00:00 +0000
+++ gavel/api/tests.py	2010-04-01 04:29:57 +0000
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+    def test_basic_addition(self):
+        """
+        Tests that 1 + 1 always equals 2.
+        """
+        self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+

=== added file 'gavel/api/urls.py'
--- gavel/api/urls.py	1970-01-01 00:00:00 +0000
+++ gavel/api/urls.py	2010-04-01 04:29:57 +0000
@@ -0,0 +1,15 @@
+from django.conf.urls.defaults import *
+from piston.resource import Resource
+
+from gavel.rules.api.handlers import *
+
+motion_handler = Resource(MotionHandler)
+vote_handler = Resource(VoteHandler)
+
+urlpatterns = patterns('',
+    url(r'^motion/$', motion_handler),
+    url(r'^motion/(?P<motion_slug>[\w-]+)/$', motion_handler),
+    url(r'^motion/(?P<motion_slug>[\w-]+)/vote/$', vote_handler),
+    url(r'^motion/(?P<motion_slug>[\w-]+)/vote/(?P<vote_id>\d+)/$',
+        vote_handler),
+)

=== added file 'gavel/rest_api_curl_example.txt'
--- gavel/rest_api_curl_example.txt	1970-01-01 00:00:00 +0000
+++ gavel/rest_api_curl_example.txt	2010-04-01 04:29:57 +0000
@@ -0,0 +1,90 @@
+$ # This document demonstrates using the REST API for Gavel. It uses
+$ # the command-line HTTP tool curl.
+$ 
+$ # We can create an object by retreiving URL with POST.
+$ # If data is provided on the command line, curl defaults to POST. Without it it defaults to GET.
+$ curl http://127.0.0.1:8000/api/motion/ -F "title=A Motion" -F "text=Create a parliamentary procedure program."
+{
+    "text": "Create a parliamentary procedure program.", 
+    "slug": "a-motion", 
+    "title": "A Motion"
+}
+$ 
+$ # We can now view that object by retreiving its url, composed of the slug, with GET.
+$ curl http://127.0.0.1:8000/api/motion/a-motion/
+{
+    "text": "Create a parliamentary procedure program.", 
+    "slug": "a-motion", 
+    "title": "A Motion"
+}
+$ 
+$ # We can modify an object by sending data with PUT.
+$ # With curl, the method used is defined with the -X option.
+$ curl http://127.0.0.1:8000/api/motion/a-motion/ -X PUT -F "text=Create a parliamentary procedure program with Python."
+{
+    "text": "Create a parliamentary procedure program with Python.", 
+    "slug": "a-motion", 
+    "title": "A Motion"
+}
+$ 
+$ # We can now create a vote object for the motion object. Again using POST.
+$ curl http://127.0.0.1:8000/api/motion/a-motion/vote/ -F "passed=True" -F "positive=4" -F "negative=3"
+{
+    "abstain": null, 
+    "positive": "4", 
+    "negative": "3", 
+    "datetime": "2010-03-31 23:15:40", 
+    "motion": {
+        "text": "Create a parliamentary procedure program with Python.", 
+        "slug": "a-motion", 
+        "title": "A Motion"
+    }, 
+    "passed": "True", 
+    "order": 0
+}
+$ 
+$ # We can also view the vote object with GET.
+$ # Vote objects are immutable. Not only is the save method short-circuited, but the PUT method is not allowed.
+$ curl http://127.0.0.1:8000/api/motion/a-motion/vote/0/
+{
+    "abstain": null, 
+    "positive": 4, 
+    "negative": 3, 
+    "datetime": "2010-03-31 23:15:40", 
+    "motion": {
+        "text": "Create a parliamentary procedure program with Python.", 
+        "slug": "a-motion", 
+        "title": "A Motion"
+    }, 
+    "passed": true, 
+    "order": 0
+}
+$ 
+$ # Vote objects can, however, be deleted with the DELETE method.
+$ curl http://127.0.0.1:8000/api/motion/a-motion/vote/0/ -X DELETE
+$ # As you can see below, it's gone. Still need to implement a 404 of some kind.
+$ curl http://127.0.0.1:8000/api/motion/a-motion/vote/0/
+Piston/0.2.3rc1 (Django 1.1.1) crash report:
+
+Traceback (most recent call last):
+
+  File "/home/josh/Projects/Gavel/simple_gavel/gavel/../gavel/rules/api/handlers.py", line 43, in read
+    return motion.vote_set.get(order=vote_id)
+
+  File "/usr/lib/pymodules/python2.6/django/db/models/manager.py", line 120, in get
+    return self.get_query_set().get(*args, **kwargs)
+
+  File "/usr/lib/pymodules/python2.6/django/db/models/query.py", line 305, in get
+    % self.model._meta.object_name)
+
+DoesNotExist: Vote matching query does not exist.
+$ 
+$ # We can also delete motion objects.
+$ curl http://127.0.0.1:8000/api/motion/a-motion/ -X DELETE
+$ 
+$ # Well, that explains the RESTful API now in place. Motions and Votes
+$ # can be created, read, updated (motions only), and deleted using only
+$ # HTTP methods. Only thing I can see being needed is a way to list all objects,
+$ # or apply filters on full lists even.
+$ 
+

=== added directory 'gavel/rules/api'
=== added file 'gavel/rules/api/__init__.py'
=== added file 'gavel/rules/api/handlers.py'
--- gavel/rules/api/handlers.py	1970-01-01 00:00:00 +0000
+++ gavel/rules/api/handlers.py	2010-04-01 04:29:57 +0000
@@ -0,0 +1,63 @@
+from piston.handler import BaseHandler
+from piston.utils import rc
+
+from gavel.rules.models import Motion, Vote
+
+class MotionHandler(BaseHandler):
+    allowed_methods = ('GET', 'PUT', 'POST', 'DELETE')
+    model = Motion
+
+    def read(self, request, motion_slug):
+        return Motion.objects.get(slug=motion_slug)
+
+    def create(self, request):
+        motion = Motion()
+
+        motion.title = request.POST.get('title')
+        motion.text = request.POST.get('text')
+        motion.save()
+
+        return motion
+    
+    def update(self, request, motion_slug):
+        motion = Motion.objects.get(slug=motion_slug)
+
+        motion.title = request.PUT.get('title') or motion.title
+        motion.text = request.PUT.get('text') or motion.text
+        motion.save()
+
+        return motion
+
+    def delete(self, request, motion_slug):
+        motion = Motion.objects.get(slug=motion_slug)
+        motion.delete()
+
+        return rc.DELETED
+
+class VoteHandler(BaseHandler):
+    allowed_methods = ('GET', 'POST', 'DELETE')
+    model = Vote
+    
+    def read(self, request, motion_slug, vote_id):
+        motion = Motion.objects.get(slug=motion_slug)
+        return motion.vote_set.get(order=vote_id)
+
+    def create(self, request, motion_slug):
+        vote = Vote()
+
+        vote.motion = Motion.objects.get(slug=motion_slug)
+        vote.passed = request.POST.get('passed')
+        vote.positive = request.POST.get('positive')
+        vote.negative = request.POST.get('negative')
+        vote.abstain = request.POST.get('abstain')
+
+        vote.save()
+
+        return vote
+
+    def delete(self, request, motion_slug, vote_id):
+        motion = Motion.objects.get(slug=motion_slug)
+        vote = motion.vote_set.get(order=vote_id)
+        vote.delete()
+
+        return rc.DELETED

=== modified file 'gavel/rules/models.py'
--- gavel/rules/models.py	2010-04-01 00:12:17 +0000
+++ gavel/rules/models.py	2010-04-01 04:29:57 +0000
@@ -11,11 +11,12 @@
     be voted on and subsidiary motions can be applied to it.
     """
     title = models.CharField("Motion Title", max_length=50)
-    slug = models.SlugField("Slug", max_length=50)
+    slug = models.SlugField("Slug", max_length=50, unique=True)
     text = models.TextField("Motion Text", max_length=10240)
     
     def save(self, *args, **kwargs):
-        self.slug = re.sub(r'[^\w-]', '',
+        if self.slug == '':
+            self.slug = re.sub(r'[^\w-]', '',
                            re.sub(r'\s+', '-', self.title.lower()))
 
         super(Motion, self).save(*args, **kwargs)
@@ -30,6 +31,7 @@
 
     motion = models.ForeignKey("Motion")
     passed = models.BooleanField("Vote Passed/Failed")
+    order = models.IntegerField("Per-Motion ID Number")
     datetime = models.DateTimeField("Time of Vote", auto_now_add=True)
     positive = models.IntegerField("Positive Votes", blank=True, null=True)
     negative = models.IntegerField("Negative Votes", blank=True, null=True)
@@ -37,11 +39,13 @@
                                   null=True)
 
     def __unicode__(self):
-        return u'%s: %s' % (self.motion.title,
+        return u'%s[%d]: %s' % (self.motion.title, self.order,
                             "Passed" if self.passed else "Failed")
 
     def save(self, *args, **kwargs):
         if self.id:
             raise PermissionDenied("Vote cannot be modified")
         
+        self.order = self.motion.vote_set.count()
         super(Vote, self).save(*args, **kwargs)
+

=== modified file 'gavel/settings.py'
--- gavel/settings.py	2010-04-01 00:12:17 +0000
+++ gavel/settings.py	2010-04-01 04:29:57 +0000
@@ -1,4 +1,4 @@
-# Django settings for simple_gavel project.
+# Django settings for gavel project.
 
 DEBUG = True
 TEMPLATE_DEBUG = DEBUG
@@ -63,7 +63,8 @@
     'django.contrib.auth.middleware.AuthenticationMiddleware',
 )
 
-ROOT_URLCONF = 'simple_gavel.urls'
+ROOT_URLCONF = 'gavel.urls'
+APPEND_SLASH = True
 
 TEMPLATE_DIRS = (
     # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".

=== modified file 'gavel/urls.py'
--- gavel/urls.py	2010-03-31 19:19:02 +0000
+++ gavel/urls.py	2010-04-01 04:29:57 +0000
@@ -6,7 +6,10 @@
 
 urlpatterns = patterns('',
     # Example:
-    # (r'^simple_gavel/', include('simple_gavel.foo.urls')),
+    # (r'^gavel/', include('simple_gavel.foo.urls')),
+    
+    # Piston RESTful API
+    url(r'^api/', include('gavel.api.urls')),
 
     # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 
     # to INSTALLED_APPS to enable admin documentation: