Permalink
Browse files

Initial commit.

  • Loading branch information...
0 parents commit 0ef1c85b87fd153633a9f98b2eb70088a676466d Alex Buchanan committed Jun 3, 2010
@@ -0,0 +1,13 @@
+settings_local.py
+*.py[co]
+*.sw[po]
+.coverage
+pip-log.txt
+docs/_gh-pages
+build.py
+.DS_Store
+/media/img/uploads
+*-min.css
+*-all.css
+*-min.js
+*-all.js
@@ -0,0 +1,3 @@
+Basket
+
+A RESTful service for storing email addresses
No changes.
@@ -0,0 +1,49 @@
+from django.http import HttpResponse
+import oauth2 as oauth
+from piston.models import Consumer as ConsumerModel
+
+class BasketAuthentication(object):
+ """
+ This supplements Piston's auth system by providing 2-legged OAuth
+ using the oauth2 library. Piston's OAuth only supports a 3-legged scheme
+ [link to 2/3 legged definition]
+ """
+ def __init__(self, realm="Basket"):
+ self.realm = realm
+ self.server = oauth.Server()
+ self.server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+
+ def is_authenticated(self, request):
+ try:
+ if request.method == 'POST':
+ params = dict(request.POST.items())
+ else:
+ params = {}
+
+ oauth_req = oauth.Request.from_request(
+ request.method,
+ request.build_absolute_uri(),
+ headers=request.META,
+ parameters=params,
+ query_string=request.environ.get('QUERY_STRING', ''))
+
+ r = self.get_consumer_record(oauth_req.get_parameter('oauth_consumer_key'))
+ consumer = oauth.Consumer(key=r.key, secret=r.secret)
+
+ self.server.verify_request(oauth_req, consumer, None)
+ return True
+ except ConsumerModel.DoesNotExist, e:
+ return False
+ except oauth.Error, e:
+ return False
+
+ def get_consumer_record(self, key):
+ return ConsumerModel.objects.get(key=key)
+
+ def challenge(self):
+ response = HttpResponse()
+ response.status_code = 401
+ for k, v in self.server.build_authenticate_header().iteritems():
+ response[k] = v
+
+ return response
@@ -0,0 +1 @@
+[{"pk": 2, "model": "piston.consumer", "fields": {"status": "accepted", "name": "test", "secret": "testsecret", "user": null, "key": "testkey", "description": ""}}]
No changes.
@@ -0,0 +1,55 @@
+import time
+from nose.tools import eq_
+import unittest
+import oauth2 as oauth
+from piston.models import Consumer as ConsumerModel
+from . import BasketAuthentication
+from utils import RequestFactory
+
+
+class AuthTest(unittest.TestCase):
+ # why doesn't find+install this fixture?
+ fixtures = ['consumer.json']
+
+ def setUp(self):
+ ConsumerModel.objects.create(name='test', key='test', secret='test')
+
+ self.rf = RequestFactory()
+ # RequestFactory defaults SERVER_NAME to testserver
+ # it's important that this URL matches that, so the signature matches
+ self.url = "http://testserver/api"
+
+ self.auth_provider = BasketAuthentication()
+ self.method = "POST"
+ self.consumer = oauth.Consumer(key='test', secret='test')
+ self.signature_method = oauth.SignatureMethod_HMAC_SHA1()
+ # we're only using 2-legged auth, no need for tokens
+ self.token = None
+
+ self.params = {
+ 'oauth_consumer_key': self.consumer.key,
+ 'oauth_version': '1.0',
+ 'oauth_nonce': oauth.generate_nonce(),
+ 'oauth_timestamp': int(time.time()),
+ }
+
+ def tearDown(self):
+ ConsumerModel.objects.all().delete()
+
+ def build_request(self):
+ oauth_req = oauth.Request(method=self.method, url=self.url, parameters=self.params)
+ oauth_req.sign_request(self.signature_method, self.consumer, self.token)
+ header = oauth_req.to_header()
+ return self.rf.post(self.url, {}, **header)
+
+ def test_get_consumer_record(self):
+ self.auth_provider.get_consumer_record('test')
+
+ def test_valid_auth(self):
+ r = self.build_request()
+ eq_(self.auth_provider.is_authenticated(r), True)
+
+ def test_invalid_auth(self):
+ self.consumer = oauth.Consumer('test', 'fail')
+ r = self.build_request()
+ eq_(self.auth_provider.is_authenticated(r), False)
No changes.
@@ -0,0 +1,28 @@
+from django.core.exceptions import ValidationError
+from piston.handler import BaseHandler
+from piston.utils import rc
+from subscriptions.models import Subscription
+
+def validate(model):
+ def decorator(target):
+ def wrapper(self, request, *args, **kwargs):
+ try:
+ attrs = self.flatten_dict(request.data)
+ m = model(**attrs)
+ m.full_clean()
+ target(self, request, *args, **kwargs)
+ except ValidationError, e:
+ resp = rc.BAD_REQUEST
+ # TODO clean up validation errors
+ resp.write(str(e.message_dict))
+ return resp
+ return wrapper
+ return decorator
+
+class SubscriptionHandler(BaseHandler):
+ fields = ('email', 'campaign', 'active', 'source')
+ model = Subscription
+
+ @validate(Subscription)
+ def create(self, request):
+ return BaseHandler.create(self, request)
@@ -0,0 +1,7 @@
+from django.db import models
+
+class Subscription(models.Model):
+ email = models.EmailField()
+ campaign = models.CharField(max_length=255)
+ active = models.BooleanField(default=True)
+ source = models.CharField(max_length=255, blank=True, null=True)
@@ -0,0 +1,49 @@
+import time
+from nose.tools import eq_
+import unittest
+
+from django.core.exceptions import ValidationError
+from django.test.client import Client
+
+import oauth2 as oauth
+
+from .models import Subscription
+
+class SubscriptionTest(unittest.TestCase):
+ def test_validation(self):
+ a = Subscription()
+ # fail on blank email
+ self.assertRaises(ValidationError, a.full_clean)
+ a.email = 'foo';
+ # fail on bad email format
+ self.assertRaises(ValidationError, a.full_clean)
+ a.email = 'foo@foo.com';
+ # fail on blank campaign
+ self.assertRaises(ValidationError, a.full_clean)
+ a.campaign = 'foo'
+ # source can be None
+ a.source = None
+ # success
+ a.full_clean()
+ a.save()
+
+ #def test_create(self):
+ # resp = self.subscribe(email='')
+ # eq_(resp.status_code, 400)
+ # resp = self.subscribe(campaign='')
+ # eq_(resp.status_code, 400)
+ # count = Subscription.objects.count
+ # eq_(count(), 0)
+ # resp = self.subscribe()
+ # eq_(resp.status_code, 200, resp.content)
+ # eq_(count(), 1)
+ # new record is active
+
+
+ # test email required
+ # email format validation
+ # list / campaign ID required
+ # locale fallback to en-US
+ # test auth
+ # test welcome
+ # test unsubscribe
@@ -0,0 +1,13 @@
+from django.conf.urls.defaults import *
+from piston.resource import Resource
+
+from basketauth import BasketAuthentication
+from .handlers import SubscriptionHandler
+
+auth = BasketAuthentication()
+
+subscribe = Resource(handler=SubscriptionHandler, authentication=auth)
+
+urlpatterns = patterns('',
+ url('^subscribe/$', subscribe),
+)
@@ -0,0 +1,38 @@
+from django.test import Client
+from django.core.handlers.wsgi import WSGIRequest
+
+class RequestFactory(Client):
+ """
+ Class that lets you create mock Request objects for use in testing.
+
+ Usage:
+
+ rf = RequestFactory()
+ get_request = rf.get('/hello/')
+ post_request = rf.post('/submit/', {'foo': 'bar'})
+
+ This class re-uses the django.test.client.Client interface, docs here:
+ http://www.djangoproject.com/documentation/testing/#the-test-client
+
+ Once you have a request object you can pass it to any view function,
+ just as if that view had been hooked up using a URLconf.
+
+ """
+ def request(self, **request):
+ """
+ Similar to parent class, but returns the request object as soon as it
+ has created it.
+ """
+ environ = {
+ 'HTTP_COOKIE': self.cookies,
+ 'PATH_INFO': '/',
+ 'QUERY_STRING': '',
+ 'REQUEST_METHOD': 'GET',
+ 'SCRIPT_NAME': '',
+ 'SERVER_NAME': 'testserver',
+ 'SERVER_PORT': 80,
+ 'SERVER_PROTOCOL': 'HTTP/1.1',
+ }
+ environ.update(self.defaults)
+ environ.update(request)
+ return WSGIRequest(environ)
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+import os
+import site
+import sys
+
+
+ROOT = os.path.dirname(os.path.abspath(__file__))
+
+path = lambda *a: os.path.join(ROOT, *a)
+
+prev_sys_path = list(sys.path)
+
+site.addsitedir(path('apps'))
+site.addsitedir(path('libs'))
+
+# Move the new items to the front of sys.path. (via virtualenv)
+new_sys_path = []
+for item in list(sys.path):
+ if item not in prev_sys_path:
+ new_sys_path.append(item)
+ sys.path.remove(item)
+sys.path[:0] = new_sys_path
+
+# No third-party imports until we've added all our sitedirs!
+from django.core.management import execute_manager, setup_environ
+
+try:
+ import settings_local as settings
+except ImportError:
+ try:
+ import settings
+ except ImportError:
+ import sys
+ sys.stderr.write(
+ "Error: Tried importing 'settings_local.py' and 'settings.py' "
+ "but neither could be found (or they're throwing an ImportError)."
+ " Please come back and try again later.")
+ raise
+
+# The first thing execute_manager does is call `setup_environ`. Logging config
+# needs to access settings, so we'll setup the environ early.
+setup_environ(settings)
+
+
+if __name__ == "__main__":
+ execute_manager(settings)
@@ -0,0 +1 @@
+MySQL-python==1.2.3c1
@@ -0,0 +1,21 @@
+# This file pulls in everything a developer needs. If it's a basic package
+# needed to run the site, it belongs in requirements-prod.txt. If it's a
+# package for developers (testing, docs, etc.), it goes in this file. IT wants
+# to pull in a minimal set of packages to get a running site.
+
+-r prod.txt
+
+pep8==0.5
+-e git://github.com/jbalogh/check.git#egg=check
+-e git://github.com/django-extensions/django-extensions.git#egg=django_extensions
+-e git://github.com/dcramer/django-devserver.git#egg=django_devserver
+werkzeug
+ipython==0.10
+
+-e git://github.com/jbalogh/test-utils.git#egg=test-utils
+-e git://github.com/jbalogh/django-nose.git#egg=django_nose
+nose==0.11.1
+coverage==3.2b4
+mock==0.6.0
+
+pylint
@@ -0,0 +1,11 @@
+# compiled.txt has other prod packages that need to be compiled.
+
+-e git://github.com/django/django@5924f18c1100#egg=django
+pytz==2010e
+
+-e git://github.com/jbalogh/django-multidb-router.git#egg=django-multidb-router
+-e git://github.com/jbalogh/django-cache-machine.git#egg=django-cache-machine
+-e git://github.com/jbalogh/django-queryset-transform.git#egg=django-queryset-transform
+-e git://github.com/jsocol/commonware.git#egg=commonware
+
+http://bitbucket.org/jespern/django-piston/get/tip.zip
Oops, something went wrong.

0 comments on commit 0ef1c85

Please sign in to comment.