Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Starting on badge API

  • Loading branch information...
commit 8a4c1eb29cfcc1057e73f9dbf9ed501b00639a77 1 parent c50ec1c
@lmorchard authored
View
7 TODO.md
@@ -119,3 +119,10 @@
* Allow badger app to be more reusable by checking for installation of notification, etc?
* Wishlist of badges
+
+* JSON POST body data not covered by OAuth signature
+ * Is this an issue?
+ * Switch to POST encoded parameters to create badges?
+ * Puts params into OAuth signature
+ * Or, stick an HMAC-SHA or MD5 of the POST body into a query param
+
View
0  apps/badges/api/__init__.py
No changes.
View
47 apps/badges/api/hacks.py
@@ -0,0 +1,47 @@
+"""Hacks to make the API work"""
+import logging
+log = logging.getLogger('nose.badger')
+
+# Monkeypatch the piston app, because it tries parsing a JSON POST body as
+# form-encoded params. That breaks oauth.
+
+import piston.authentication
+from oauth import oauth
+
+def initialize_server_request(request):
+ """
+ HACK: (LMO) This is the point of the monkeypatch for piston, which attempts to
+ only include parameters from a POST request if the request body can
+ be parsed as parameters. (eg. JSON shouldn't bs parsed as params)
+
+ See also: http://oauth.net/core/1.0/#rfc.section.9.1.1
+ See also: http://getsatisfaction.com/oauth/topics/how_to_normalize_request_including_get_params_and_xml_body
+ """
+ include_post_body_as_params = ("POST" == request.method and (
+ 'multipart/form-data' in request.META['CONTENT_TYPE'] or
+ 'application/x-www-form-urlencoded' in request.META['CONTENT_TYPE']
+ ))
+ if include_post_body_as_params: # Use merged GET and POST params.
+ params = dict(request.REQUEST.items())
+ else: # Just use GET params.
+ params = dict(request.GET.items())
+
+ # Seems that we want to put HTTP_AUTHORIZATION into 'Authorization'
+ # for oauth.py to understand. Lovely.
+ request.META['Authorization'] = request.META.get('HTTP_AUTHORIZATION', '')
+
+ oauth_request = oauth.OAuthRequest.from_request(
+ request.method, request.build_absolute_uri(),
+ headers=request.META, parameters=params,
+ query_string=request.environ.get('QUERY_STRING', ''))
+
+ if oauth_request:
+ oauth_server = oauth.OAuthServer(piston.authentication.oauth_datastore(oauth_request))
+ oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT())
+ oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1())
+ else:
+ oauth_server = None
+
+ return oauth_server, oauth_request
+
+piston.authentication.initialize_server_request = initialize_server_request
View
276 apps/badges/api/handlers.py
@@ -0,0 +1,276 @@
+"""
+Badger API
+"""
+from datetime import datetime
+
+import piston
+
+from piston.handler import BaseHandler, AnonymousBaseHandler, typemapper
+from piston.utils import rc, require_mime, require_extended
+from piston.emitters import Emitter
+
+from oauth import oauth
+
+from django.utils import simplejson as json
+
+from django.http import HttpResponse, HttpResponseRedirect
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.template.defaultfilters import slugify
+
+from avatar.models import avatar_file_path
+from avatar.templatetags.avatar_tags import avatar_url
+
+from django.core.urlresolvers import reverse
+from django.core.exceptions import ValidationError
+
+from django.contrib.sites.models import Site
+from django.contrib.auth.models import User
+
+from badges import BADGE_DEFAULT_SIZE
+from badges.models import ( Badge, BadgeNomination, BadgeAward,
+ BadgeAwardee, badge_file_path )
+
+import badges.api.hacks # Ensure piston gets monkeypatched
+
+AVATAR_DEFAULT_SIZE = 80
+
+
+def site_url(path):
+ #return path
+ if path.startswith('http'): return path
+ return 'http://%s%s' % (Site.objects.get_current().domain, path)
+
+def request_token_ready(request, token):
+ error = request.GET.get('error', '')
+ ctx = RequestContext(request, { 'error': error, 'token': token})
+ return render_to_response('piston/request_token_ready.html',
+ context_instance = ctx)
+
+def render_created(self, request, new_obj, new_url):
+ """Quick hack to return a 201 Created along with a JSON rendering of
+ what was created"""
+ resp = rc.CREATED
+ emitter, ct = Emitter.get('json')
+ resp['Content-Type'] = ct
+ resp['Location'] = new_url
+ srl = emitter(new_obj, typemapper, self, self.fields, False)
+ resp.content = srl.render(request)
+ return resp
+
+
+class IndexHandler(BaseHandler):
+ """Index entry point, offers links to docs and major collections"""
+
+ anonymous = 'AnonymousIndexHandler'
+ allowed_methods = ('GET',)
+
+ def read(self, request):
+ """Since we have a user, add profile links to response"""
+ links_to_build = {
+ 'badges': 'badges_api_collection',
+ 'profiles': 'badges_api_profile_collection',
+ 'docs': 'badges_api_docs',
+ }
+ links = dict(
+ (n[0], site_url(reverse(n[1])))
+ for n in links_to_build.items()
+ )
+ if request.user.is_authenticated():
+ links['authenticated'] = {
+ 'text/html': site_url(request.user.get_absolute_url()),
+ 'application/json': site_url(reverse('badges_api_profile',
+ kwargs={'username': request.user.username}))
+ }
+ return links
+
+
+class AnonymousIndexHandler(IndexHandler, AnonymousBaseHandler):
+ pass
+
+
+class ProfileHandler(BaseHandler):
+ model = User
+ anonymous = 'AnonymousProfileHandler'
+ fields = ( 'username', 'image', 'links', )
+
+ @classmethod
+ def resource_uri(handler, obj=None):
+ return ('badges_api_profile', [ '%s' % obj, ])
+
+ @classmethod
+ def image(handler, user):
+ return {
+ 'href': site_url(avatar_url(user, AVATAR_DEFAULT_SIZE)),
+ 'width': AVATAR_DEFAULT_SIZE, 'height': AVATAR_DEFAULT_SIZE,
+ }
+
+ @classmethod
+ def links(handler, user):
+ return {
+ 'self': {
+ 'text/html': site_url(user.get_absolute_url()),
+ 'application/json': site_url(reverse('badges_api_profile',
+ kwargs={'username': user.username}))
+ },
+ 'awards': {
+ 'text/html': site_url(user.get_absolute_url()+'#awards'),
+ 'application/json': site_url(reverse('badges_api_profile_awards',
+ kwargs={'claimed_by__username': user.username}))
+ },
+ }
+
+
+class AnonymousProfileHandler(ProfileHandler, AnonymousBaseHandler):
+ pass
+
+
+class BadgeHandler(BaseHandler):
+ anonymous = 'AnonymousBadgeHandler'
+ model = Badge
+ allowed_methods = ('GET','POST',)
+ fields = (
+ 'links', 'title', 'image', 'description',
+ ('creator', ('username','image', 'links')),
+ 'created_at', 'updated_at',
+ )
+
+ def create(self, request):
+ """POST to create a new badge"""
+ data = request.data
+
+ if 'title' not in data:
+ resp = rc.BAD_REQUEST
+ resp.write('title required')
+ return resp
+
+ new_badge = Badge(
+ creator=request.user,
+ updated_at=datetime.now(),
+ title = data['title'],
+ slug = slugify(data['title']),
+ description = data.get('description', ''),
+ autoapprove = data.get('autoapprove', False),
+ only_creator_can_nominate =
+ data.get('only_creator_can_nominate', False),
+ )
+
+ try:
+ new_badge.validate_unique()
+ except ValidationError, e:
+ return rc.DUPLICATE_ENTRY
+
+ try:
+ new_badge.full_clean()
+ new_badge.save()
+ return render_created(self, request, [ new_badge ],
+ site_url(reverse('badges_api_badge',
+ kwargs={'slug': new_badge.slug})))
+ except ValidationError, e:
+ resp = rc.BAD_REQUEST
+ resp.write('%s' % e)
+ return resp
+
+ @classmethod
+ def image(handler, badge):
+ return {
+ 'href': site_url(badge.main_image_url(BADGE_DEFAULT_SIZE)),
+ 'width': BADGE_DEFAULT_SIZE, 'height': BADGE_DEFAULT_SIZE,
+ }
+
+ @classmethod
+ def links(handler, badge):
+ return {
+ 'self': {
+ 'text/html': site_url(badge.get_absolute_url()),
+ 'application/json': site_url(reverse('badges_api_badge',
+ kwargs={'slug': badge.slug}))
+ },
+ 'nominations': {
+ 'text/html': site_url(badge.get_absolute_url()+'#nominations'),
+ 'application/json': site_url(
+ reverse('badges_api_nomination_collection',
+ kwargs={'badge__slug': badge.slug}))
+ },
+ 'awards': {
+ 'text/html': site_url(badge.get_absolute_url()+'#awards'),
+ 'application/json': site_url(reverse('badges_api_award_collection',
+ kwargs={'badge__slug': badge.slug}))
+ },
+ }
+
+
+class AnonymousBadgeHandler(BadgeHandler, AnonymousBaseHandler):
+ allowed_methods = ('GET',)
+
+
+class NominationHandler(BaseHandler):
+ model = BadgeNomination
+ fields = (
+ 'id',
+ 'links',
+ ('badge', ('title', 'links', 'image',)),
+ ('nominee', ('username','image',)),
+ 'nominator',
+ 'reason_why',
+ 'approved',
+ ('approved_by', ('username','image',)),
+ 'approved_why',
+ 'created_at',
+ 'updated_at',
+ )
+
+ def create(self, request, badge__slug):
+ badge = Badge.objects.get(slug = badge__slug)
+
+ data = request.data
+
+ if 'nominee' not in data:
+ resp = rc.BAD_REQUEST
+ resp.write('nominee required')
+ return resp
+
+ if 'username' in data['nominee']:
+ nom_user = User.objects.get(username=data['nominee']['username'])
+ nominee, created = BadgeAwardee.objects.get_or_create(user=nom_user)
+ else:
+ resp = rc.BAD_REQUEST
+ resp.write('valid nominee required')
+ return resp
+
+ new_nomination = badge.nominate(request.user, nominee,
+ data.get('reason_why', ''))
+
+ return render_created(self, request, [ new_nomination ],
+ site_url(reverse('badges_api_nomination', kwargs={
+ 'badge__slug': badge.slug,
+ 'id': new_nomination.id,
+ })))
+
+ @classmethod
+ def resource_uri(handler, obj=None):
+ return ('badges_api_nomination', ['slug', 'id'])
+
+ @classmethod
+ def links(handler, nomination):
+ return {
+ 'text/html': site_url(nomination.get_absolute_url()),
+ }
+
+
+class BadgeAwardHandler(BaseHandler):
+ model = BadgeAward
+
+ @classmethod
+ def resource_uri(handler, obj=None):
+ return ('badges_api_award', [ 'badge__slug', 'id' ])
+
+
+class ProfileAwardHandler(BaseHandler):
+ model = BadgeAward
+
+ @classmethod
+ def resource_uri(handler, obj=None):
+ return ('badges_api_profile_awards', [ 'username' ])
+
+
View
407 apps/badges/api/tests.py
@@ -0,0 +1,407 @@
+"""Tests for badge API"""
+import logging
+import re
+import urlparse
+import urllib
+import StringIO
+import time
+import random
+from oauth import oauth
+
+from oauth.oauth import ( OAuthToken, OAuthConsumer, OAuthRequest,
+ OAuthSignatureMethod_HMAC_SHA1, OAuthSignatureMethod_PLAINTEXT )
+
+from lxml import etree
+from pyquery import PyQuery
+
+from django.utils import simplejson as json
+
+from django.http import HttpRequest
+from django.test import TestCase
+from django.test.client import FakePayload, Client
+from django.template.defaultfilters import slugify
+
+from nose.tools import assert_equal, with_setup, assert_false, eq_, ok_
+from nose.plugins.attrib import attr
+
+from django.contrib.sites.models import Site
+from django.contrib.auth.models import User
+from pinax.apps.profiles.models import Profile
+from pinax.apps.account.models import Account
+
+from badges.models import ( Badge, BadgeNomination, BadgeAward,
+ BadgeAwardee, badge_file_path )
+
+# HACK: This is probably a mistake, but the site_url utility func seems just as
+# useful in tests.
+from badges.api.handlers import site_url
+
+from piston.models import Consumer, Token
+
+
+class TestAPI(TestCase):
+
+ API_BASE_PATH = '/badges/api'
+
+ def setUp(self):
+ self.log = logging.getLogger('nose.badger')
+ self.browser = Client()
+
+ for x in User.objects.all(): x.delete()
+ for x in Badge.objects.all(): x.delete()
+ for x in Consumer.objects.all(): x.delete()
+
+ self.site = Site.objects.get_current()
+ self.site.domain = 'testserver'
+ self.site.save()
+
+ self.users = {}
+ for name in ( 'user1', 'user2', 'user3'):
+ self.users[name] = self.get_user(name)
+
+ badge_awards = (
+ ( 'badge1', 'user1', 'user2', 'user3', True ),
+ ( 'badge2', 'user2', 'user1', 'user3', False ),
+ ( 'badge3', 'user1', 'user2', 'user3', True ),
+ ( 'badge4', 'user2', 'user1', 'user3', False ),
+ )
+ self.badges, self.awards = self.build_awards(badge_awards)
+
+ self.consumer_key = 'keykeykey'
+ self.consumer_secret = 'secretsecretsecret'
+
+ self.authorize_oauth_app()
+
+ def tearDown(self):
+ pass
+
+ def test_index(self):
+ """Exercise the index API resource"""
+ path = '/badges/api/'
+ c = Client()
+
+ for use_oauth in ( True, False ):
+ data = self.api_GET('/', {}, use_oauth)[0]
+
+ # This stuff should appear regardless of auth.
+ ok_('profiles' in data)
+ eq_(site_url('/badges/api/profiles/'), data['profiles'])
+ ok_('badges' in data)
+ eq_(site_url('/badges/api/badges/'), data['badges'])
+ ok_('docs' in data)
+ eq_(site_url('/badges/api/docs/'), data['docs'])
+
+ if use_oauth:
+ # If auth accepted, there should be links to the auth user.
+ ok_('authenticated' in data)
+ ok_('application/json' in data['authenticated'])
+ eq_(site_url('/badges/api/profiles/user1/'),
+ data['authenticated']['application/json'])
+
+ def test_get_profiles(self):
+ """Exercise the profile and profile collection API resources"""
+ users = sorted(self.users.values(), key=lambda x: x.username)
+ for use_oauth in ( True, False ):
+ # Both auth and non-auth cases should be the same.
+ collections = [
+ self.api_GET('/profiles/', {}, use_oauth)[0],
+ [ self.api_GET('/profiles/%s/' % u.username, {}, use_oauth)[0][0]
+ for u in users ]
+ ]
+ for items in collections:
+ eq_(len(users), len(items))
+ items.sort(key=lambda b: b['username'])
+ for idx in range(0, len(items)):
+ e_user, r_user = users[idx], items[idx]
+ # TODO: Compare more attributes?
+ eq_(e_user.username, r_user['username'])
+ eq_(site_url(e_user.get_absolute_url()),
+ r_user['links']['self']['text/html'])
+ eq_(site_url('/badges/api/profiles/%s/' % e_user.username),
+ r_user['links']['self']['application/json'])
+
+
+ def test_get_badges(self):
+ """Exercise the badge and badge collection API resources"""
+ badges = sorted(self.badges.values(), key=lambda b: b.title)
+ for use_oauth in ( True, False ):
+ # Both auth and non-auth cases should be the same.
+ collections = [
+ self.api_GET('/badges/', {}, use_oauth)[0],
+ [ self.api_GET('/badges/%s/' % b.title, {}, use_oauth)[0][0]
+ for b in badges ]
+ ]
+ for items in collections:
+ eq_(len(badges), len(items))
+ items.sort(key=lambda b: b['title'])
+ for idx in range(0, len(items)):
+ e_badge, r_badge = badges[idx], items[idx]
+ # TODO: Compare more attributes?
+ eq_(e_badge.title, r_badge['title'])
+ eq_(site_url(e_badge.get_absolute_url()),
+ r_badge['links']['self']['text/html'])
+ eq_(site_url('/badges/api/badges/%s/' % e_badge.slug),
+ r_badge['links']['self']['application/json'])
+
+ def test_create_badge(self):
+ """Exercise creating a badge via the API"""
+ props = {
+ 'title': 'New Sample Badge',
+ 'description': 'This is a badge created via API',
+ }
+ data, resp = self.api_POST('/badges/', body=props, use_oauth=True)
+
+ eq_(props['title'], data[0]['title'])
+ eq_(props['description'], data[0]['description'])
+ eq_(site_url('/badges/api/badges/%s/' % slugify(props['title'])),
+ resp['location'])
+
+ badge = Badge.objects.get(title=props['title'])
+ eq_(props['title'], badge.title)
+ eq_(props['description'], badge.description)
+
+ # TODO: implement and test image upload
+
+ @attr('current')
+ def test_create_nomination(self):
+ """Exercise nominating a user for a badge"""
+ badge = self.badges['badge4']
+ badge_url_path = '/badges/%s/' % badge.slug
+
+ data, resp = self.api_GET(badge_url_path)
+ ok_(data[0]['links']['nominations']['application/json'],
+ site_url('/badges/api/badges/%s/nominations/' % badge.slug))
+
+ nom_data = {
+ 'nominee': { 'username': 'user2' },
+ 'reason_why': 'Extreme awesomeness'
+ }
+ data, resp = self.api_POST('/badges/%s/nominations/' % badge.slug,
+ use_oauth=True, body = nom_data)
+
+ self.log.debug('%s' % resp)
+
+ nomination = BadgeNomination.objects.get(
+ nominee__user__username='user2', badge=badge)
+ eq_(nom_data['reason_why'], nomination.reason_why)
+
+ # TODO: nominate by email address
+
+
+ #######################################################################
+
+ def api_GET(self, path, params=None, use_oauth=False, parse_json=True):
+ c = Client()
+ params = params or {}
+ full_path = self.API_BASE_PATH + path
+ if use_oauth:
+ params.update(self.oauth_params(full_path, params))
+ response = c.get(full_path, params)
+
+ if parse_json:
+ try:
+ return ( json.loads(response.content), response )
+ except ValueError, e:
+ return ( None, response )
+ else:
+ return ( response.content, response )
+
+ def api_POST(self, path, params=None, body='', use_oauth=False,
+ extra=None, follow=True, parse_json=True):
+ c = Client()
+ params = params or {}
+ extra = extra or {}
+ body = body or ''
+
+ if type(body) != str:
+ body = json.dumps(body)
+
+ full_path = self.API_BASE_PATH + path
+ content_type = extra.get('CONTENT_TYPE', 'application/json')
+
+ r = {
+ 'CONTENT_LENGTH': len(body),
+ 'CONTENT_TYPE': content_type,
+ 'PATH_INFO': urllib.unquote(full_path),
+ 'QUERY_STRING': urllib.urlencode(params),
+ 'REQUEST_METHOD': 'POST',
+ 'wsgi.input': FakePayload(body),
+ }
+ r.update(extra)
+
+ if use_oauth:
+ o_params = self.oauth_params(full_path, params_in=params,
+ body=body, http_method='POST')
+ r['HTTP_AUTHORIZATION'] = 'OAuth realm="API", %s' % (', '.join(
+ '%s="%s"' % i for i in o_params.items() if i[0].startswith('oauth')
+ ))
+
+ response = c.request(**r)
+ if follow:
+ response = c._handle_redirects(response)
+
+ if parse_json:
+ try:
+ return ( json.loads(response.content), response )
+ except ValueError, e:
+ return ( None, response )
+ else:
+ return ( response.content, response )
+
+ def oauth_params(self, path, params_in=None, body='', http_method='GET'):
+ """Build parameters to use in authenticating via OAuth with test client"""
+ url = 'http://testserver%s' % (path)
+
+ params = {
+ 'oauth_consumer_key': self.consumer.key,
+ 'oauth_token': self.access_token.key,
+ 'oauth_signature_method': 'HMAC-SHA1',
+ 'oauth_timestamp': str(int(time.time())),
+ 'oauth_nonce': oauth.generate_nonce(),
+ 'oauth_version': '1.0',
+ }
+ if params_in is not None:
+ params.update(params_in)
+
+ parts = urlparse.urlparse(url)
+ if parts.query:
+ params.update(urlparse.parse_qs(parts.query))
+
+ oauth_request = OAuthRequest.from_token_and_callback(
+ self.access_token, parameters=params,
+ http_method=http_method,
+ http_url=urlparse.urlunparse((
+ parts.scheme, parts.netloc, parts.path,
+ '', '', parts.fragment,
+ )),
+ )
+ signature_method = OAuthSignatureMethod_HMAC_SHA1()
+ #signature_method = OAuthSignatureMethod_PLAINTEXT()
+ signature = signature_method.build_signature(
+ oauth_request, self.consumer, self.access_token
+ )
+ params['oauth_signature'] = signature
+
+ new_url = urlparse.urlunparse((
+ parts.scheme, parts.netloc, parts.path,
+ '', urllib.urlencode(params), parts.fragment,
+ ))
+ return params
+
+ def get_user(self, username, password=None, email=None):
+ """Get a user for the given username, creating it if necessary."""
+ if password is None: password = '%s_password' % username
+ if email is None: email = '%s@testserver' % username
+ try:
+ user = User.objects.get(username=username)
+ except User.DoesNotExist:
+ user = User.objects.create_user(username, email, password)
+ ok_(user is not None, "user should exist")
+ return user
+
+ def build_awards(self, badge_awards):
+ badges, awards = {}, {}
+
+ for details in badge_awards:
+ badge_name, creator_name, nominator_name, nominee_name, claimed = details
+
+ creator = self.users[creator_name]
+ nominator = self.users[nominator_name]
+ nominee_user = self.users[nominee_name]
+ nominee, c = BadgeAwardee.objects.get_or_create(user=nominee_user)
+
+ try:
+ badge = Badge.objects.get(title=badge_name)
+ except Badge.DoesNotExist:
+ badge = Badge(title=badge_name, creator=creator,
+ slug=slugify(badge_name),
+ description='%s description' % badge_name)
+ badge.save()
+ badges[badge_name] = badge
+
+ nomination = badge.nominate(nominator, nominee,
+ '%s nomination reason' % badge_name)
+ award = nomination.approve(creator,
+ '%s approval reason' % badge_name)
+ if claimed:
+ award.claim(nominee_user)
+
+ awards[details] = award
+
+ time.sleep(1)
+
+ return badges, awards
+
+ def authorize_oauth_app(self):
+
+ user = self.users['user1']
+
+ # Create a new consumer
+ # TODO: Make a test that does this through the (future) control panel
+ consumer = Consumer(name="test_app", description="Test App",
+ key=self.consumer_key, secret=self.consumer_secret, user=user)
+ consumer.save()
+
+ # Get a request token.
+ params = {
+ 'oauth_consumer_key': self.consumer_key,
+ 'oauth_signature_method': 'PLAINTEXT',
+ 'oauth_signature': '%s&' % self.consumer_secret,
+ 'oauth_timestamp': str(int(time.time())),
+ 'oauth_nonce': 'requestnonce',
+ 'oauth_version': '1.0',
+ 'scope': 'photos', # custom argument to specify Protected Resource
+ }
+ resp = self.browser.get("/badges/api/oauth/request_token/", params)
+ data = urlparse.parse_qs(resp.content)
+ token_key = data['oauth_token'][0]
+ token_secret = data['oauth_token_secret'][0]
+
+ # Get the form to approve access for the token.
+ self.browser.login(username='user1', password='user1_password')
+ params = {
+ 'oauth_token': token_key,
+ 'oauth_callback': 'http://testserver/callback',
+ 'authorize_access': '1'
+ }
+ resp = self.browser.get("/badges/api/oauth/authorize/", params)
+
+ # Dig out the form params and approve access for the token.
+ # This helps account for the inevitable CSRF crumb present.
+ page = PyQuery(resp.content)
+ form = page('form[action="/badges/api/oauth/authorize/"]')
+ params = dict( (i.name, i.value) for i in form('input') )
+ params['authorize_access'] = 1
+ resp = self.browser.post("/badges/api/oauth/authorize/", params)
+
+ # Parse out the verifier from the redirect
+ url_parts = urlparse.urlparse(resp['location'])
+ data = urlparse.parse_qs(url_parts.query)
+ eq_(token_key, data['oauth_token'][0])
+ token_verifier = data['oauth_verifier'][0]
+
+ # Make sure the token's been approved.
+ t = Token.objects.get(key=token_key)
+ ok_(t.is_approved)
+
+ # Make a request to trade token for access token.
+ params = {
+ 'oauth_consumer_key': self.consumer_key,
+ 'oauth_token': token_key,
+ 'oauth_signature_method': 'PLAINTEXT',
+ 'oauth_signature': '%s&%s' % (self.consumer_secret, token_secret),
+ 'oauth_timestamp': str(int(time.time())),
+ 'oauth_nonce': 'accessnonce',
+ 'oauth_version': '1.0',
+ 'oauth_verifier': token_verifier,
+ }
+ resp = self.browser.get("/badges/api/oauth/access_token/", params)
+ data = urlparse.parse_qs(resp.content)
+
+ access_token_key = data['oauth_token'][0]
+ access_token_secret = data['oauth_token_secret'][0]
+
+ # Build a request for a service
+ self.access_token = OAuthToken(access_token_key, access_token_secret)
+ self.consumer = OAuthConsumer(self.consumer_key, self.consumer_secret)
+
View
68 apps/badges/api/urls.py
@@ -0,0 +1,68 @@
+from django.conf.urls.defaults import *
+from piston.resource import Resource
+from piston.doc import documentation_view
+from piston.authentication import HttpBasicAuthentication, OAuthAuthentication
+
+import badges.api.handlers as handlers
+import badges.api.utils as utils
+
+
+def h(name, require_auth=True):
+ """Build a handler and mark it CSRF exempt"""
+ handler = getattr(handlers, '%sHandler' % name)
+ #auth = require_auth and HttpBasicAuthentication(realm='Badger API') or None
+ #auth = require_auth and OAuthAuthentication(realm='Badger API') or None
+ auth = require_auth and utils.API_AUTHENTICATION or None
+ resource = Resource(handler=handler, authentication=auth)
+ resource.csrf_exempt = True
+ return resource
+
+
+urlpatterns = patterns('',
+
+ url(r'^profiles/(?P<claimed_by__username>[^/]+)/awards/',
+ h('ProfileAward'),
+ name='badges_api_profile_awards'),
+ url(r'^profiles/(?P<username>[^/]+)/',
+ h('Profile'),
+ name='badges_api_profile'),
+ url(r'^profiles/',
+ h('Profile'),
+ name='badges_api_profile_collection'),
+ url(r'^badges/(?P<badge__slug>[^/]+)/awards/(?P<id>[^/]+)/',
+ h('BadgeAward'),
+ name='badges_api_award'),
+ url(r'^badges/(?P<badge__slug>[^/]+)/awards/',
+ h('BadgeAward'),
+ name='badges_api_award_collection'),
+ url(r'^badges/(?P<badge__slug>[^/]+)/nominations/(?P<id>[^/]+)/',
+ h('Nomination'),
+ name='badges_api_nomination'),
+ url(r'^badges/(?P<badge__slug>[^/]+)/nominations/',
+ h('Nomination'),
+ name='badges_api_nomination_collection'),
+ url(r'^badges/(?P<slug>[^/]+)/',
+ h('Badge'),
+ name='badges_api_badge'),
+ url(r'^badges/',
+ h('Badge'),
+ name='badges_api_collection'),
+
+ url(r'^docs/$', documentation_view, name='badges_api_docs'),
+ url(r'^$', h('Index'), name='badges_api_index'),
+)
+
+# This seems ugly, but CSRF middleware butts in otherwise...
+
+from piston.authentication import ( oauth_access_token, oauth_request_token,
+ oauth_user_auth )
+
+oauth_request_token.csrf_exempt = True
+oauth_user_auth.csrf_exempt = True
+oauth_access_token.csrf_exempt = True
+
+urlpatterns += patterns('',
+ url(r'^oauth/request_token/$', oauth_request_token),
+ url(r'^oauth/authorize/$', oauth_user_auth),
+ url(r'^oauth/access_token/$', oauth_access_token),
+)
View
109 apps/badges/api/utils.py
@@ -0,0 +1,109 @@
+"""
+"""
+from django.db import models
+from django.contrib.auth.models import User, AnonymousUser
+from django.http import HttpResponse
+from piston.authentication import OAuthAuthentication, HttpBasicAuthentication
+
+KEY_SIZE = 18
+PASSWORD_SIZE = 32
+
+
+class ValetKeyAuthentication(object):
+ pass
+
+
+class ValetKeyManager(models.Manager):
+ pass
+
+
+class ValetKey(models.Model):
+ objects = ValetKeyManager()
+
+ name = models.CharField(max_length=255)
+ description = models.TextField()
+ key = models.CharField(max_length=KEY_SIZE)
+ password = models.CharField(max_length=PASSWORD_SIZE)
+ user = models.ForeignKey(User, null=True, blank=True, related_name='valet_keys')
+
+
+# See also: http://djangosnippets.org/snippets/1871/
+
+class MultiValueHttpResponse(HttpResponse):
+ '''
+ A subclass of HttpResponse that is capable of representing multiple instances of a header.
+ Use 'add_header_value' to set or add a value for a header.
+ Use 'get_header_values' to get all values for a header.
+ 'items' returns an array containing each value for each header.
+ 'get' and '__getitem__' return the first value for the requested header.
+ '__setitem__' replaces all values for a header with the provided value.
+ '''
+ def __init__(self, *args, **kwargs):
+ super(MultiValueHttpResponse, self).__init__(*args, **kwargs)
+ self._multi_value_headers = {}
+ # the constructor may set some headers already
+ for item in super(MultiValueHttpResponse, self).items():
+ self[item[0]] = item[1]
+
+ def __str__(self):
+ return '\n'.join(['%s: %s' % (key, value)
+ for key, value in self.items()]) + '\n\n' + self.content
+
+ def __setitem__(self, header, value):
+ header, value = self._convert_to_ascii(header, value)
+ self._multi_value_headers[header.lower()] = [(header, value)]
+
+ def __getitem__(self, header):
+ return self._multi_value_headers[header.lower()][0][1]
+
+ def items(self):
+ items = []
+ for header_values in self._multi_value_headers.values():
+ for entry in header_values:
+ items.append((entry[0], entry[1]))
+
+ return items
+
+ def get(self, header, alternate):
+ return self._multi_value_headers.get(header.lower(), [(None, alternate)])[0][1]
+
+ def add_header_value(self, header, value):
+ header, value = self._convert_to_ascii(header, value)
+ lower_header = header.lower()
+ if not lower_header in self._multi_value_headers:
+ self._multi_value_headers[lower_header] = []
+ self._multi_value_headers[lower_header].append((header, value))
+
+ def get_header_values(self, header):
+ header = self._convert_to_ascii(header)
+
+ return [header[1] for header in self._multi_value_headers.get(header.lower(), [])]
+
+class MultipleAuthentication(object):
+ def __init__(self, **methods):
+ self.methods = methods
+
+ def is_authenticated(self, request):
+ auth_header = request.META.get('HTTP_AUTHORIZATION', None)
+ if not auth_header:
+ return False
+
+ (method, auth) = auth_header.split(" ", 1)
+ if method in self.methods:
+ return self.methods[method].is_authenticated(request)
+
+ return False
+
+ def challenge(self):
+ response = MultiValueHttpResponse('Authorization Required',
+ content_type="text/plain", status=401)
+ for method in self.methods.values():
+ challenge = method.challenge().get('WWW-Authenticate', None)
+ if challenge:
+ response.add_header_value('WWW-Authenticate', challenge)
+
+ return response
+
+API_AUTHENTICATION = MultipleAuthentication(Basic=HttpBasicAuthentication(),
+ OAuth=OAuthAuthentication())
+
View
9 apps/badges/models.py
@@ -23,7 +23,7 @@
from notification import models as notification
from mailer import send_mail
-from badges import BADGE_STORAGE_DIR, BADGE_RESIZE_METHOD
+from badges import BADGE_DEFAULT_URL, BADGE_STORAGE_DIR, BADGE_RESIZE_METHOD
try:
from cStringIO import StringIO
@@ -181,9 +181,14 @@ def create_thumbnail(self, size):
thumb = self.main_image.storage.save(self.main_image_name(size), thumb_file)
def main_image_url(self, size=256):
- return self.main_image.storage.url(self.main_image_name(size))
+ if not self.main_image:
+ return BADGE_DEFAULT_URL
+ name = self.main_image_name(size)
+ return self.main_image.storage.url(name)
def main_image_name(self, size):
+ if not self.main_image.name:
+ return os.path.basename(BADGE_DEFAULT_URL)
return os.path.join(BADGE_STORAGE_DIR, self.slug,
'resized', str(size), self.main_image.name)
View
1  apps/badges/templates/oauth/challenge.html
@@ -0,0 +1 @@
+CHALLENGE TODO
View
16 apps/badges/templates/piston/authorize_token.html
@@ -0,0 +1,16 @@
+{% extends "site_base.html" %}
+
+{% load badge_tags %}
+{% load i18n %}
+
+{% block head_title %}{% trans "Authorize Token" %}{% endblock %}
+
+{% block body %}
+ <div>
+ <h1>Authorize Token</h1>
+ <form action="{% url piston.authentication.oauth_user_auth %}" method="POST">
+ {{ form.as_table }}
+ <button type="submit">Confirm</button>
+ </form>
+ </div>
+{% endblock %}
View
18 apps/badges/templates/piston/request_token_ready.html
@@ -0,0 +1,18 @@
+{% extends "site_base.html" %}
+
+{% load badge_tags %}
+{% load i18n %}
+
+{% block head_title %}{% trans "Token Authorized" %}{% endblock %}
+
+{% block body %}
+ <div>
+ {% if error %}
+ <h1>Token NOT Authorizedd</h1>
+ <p>Error: <code>{{ error }}</code></p>
+ {% else %}
+ <h1>Token Authorized</h1>
+ <p>Verifier / PIN: <code>{{ token.verifier }}</code></p>
+ {% endif %}
+ </div>
+{% endblock %}
View
7 apps/badges/tests/test_badge_feeds.py
@@ -22,17 +22,14 @@
from pinax.apps.profiles.models import Profile
from pinax.apps.account.models import Account
-from badges.models import Badge, BadgeNomination, BadgeAward
-
from nose.tools import assert_equal, with_setup, assert_false, eq_, ok_
from nose.plugins.attrib import attr
-from django.contrib.auth.models import User
-from pinax.apps.profiles.models import Profile
-from pinax.apps.account.models import Account
from badges.models import Badge, BadgeNomination
from badges.models import BadgeAward, BadgeAwardee
+
from mailer.models import Message, MessageLog
+
from notification.models import NoticeType, Notice
class TestFeeds(TestCase):
View
2  apps/socialconnect/views.py
@@ -80,7 +80,7 @@ def do_signin(self, request):
def do_callback(self, request):
"""Handle response from OAuth permit/deny"""
# TODO: Handle OAuth denial!
- mode = request.session['socialconnect_mode']
+ mode = request.session.get('socialconnect_mode', None)
profile = self.get_profile_from_callback(request)
if not profile: return HttpResponse(status=400)
View
14 libs/csrf_context.py
@@ -0,0 +1,14 @@
+from django.core import context_processors
+from django.utils import encoding, functional, html
+
+
+def csrf(request):
+ # Django does it lazy like this. I don't know why.
+ def _get_val():
+ token = context_processors.csrf(request)['csrf_token']
+ # This should be an md5 string so any broken Unicode is an attacker.
+ try:
+ return html.escape(unicode(token))
+ except UnicodeDecodeError:
+ return ''
+ return {'csrf_token': functional.lazy(_get_val, unicode)()}
View
77 oauth_client.py
@@ -0,0 +1,77 @@
+import os
+import cgi
+import oauth2 as oauth
+
+# settings for the local test consumer
+CONSUMER_SERVER = os.environ.get("CONSUMER_SERVER") or 'localhost'
+CONSUMER_PORT = os.environ.get("CONSUMER_PORT") or '8000'
+print CONSUMER_SERVER , CONSUMER_PORT
+
+# fake urls for the test server (matches ones in server.py)
+REQUEST_TOKEN_URL = 'http://%s:%s/badges/api/oauth/request_token/' % (CONSUMER_SERVER, CONSUMER_PORT)
+ACCESS_TOKEN_URL = 'http://%s:%s/badges/api/oauth/access_token/' % (CONSUMER_SERVER, CONSUMER_PORT)
+AUTHORIZE_URL = 'http://%s:%s/badges/api/oauth/authorize/' % (CONSUMER_SERVER, CONSUMER_PORT)
+
+# key and secret granted by the service provider for this consumer application - same as the MockOAuthDataStore
+CONSUMER_KEY = 'XjJjy7gjeXWzcgRFqN'
+CONSUMER_SECRET = 'm4faR6zjZ4TZMumVKsBCrGPr5pGXKDDt'
+
+
+consumer = oauth.Consumer(CONSUMER_KEY, CONSUMER_SECRET)
+client = oauth.Client(consumer)
+
+# Step 1: Get a request token. This is a temporary token that is used for
+# having the user authorize an access token and to sign the request to obtain
+# said access token.
+
+resp, content = client.request(REQUEST_TOKEN_URL, "GET")
+if resp['status'] != '200':
+ raise Exception("Invalid response %s." % resp['status'])
+
+request_token = dict(cgi.parse_qsl(content))
+
+print "Request Token:"
+print " - oauth_token = %s" % request_token['oauth_token']
+print " - oauth_token_secret = %s" % request_token['oauth_token_secret']
+print
+
+# Step 2: Redirect to the provider. Since this is a CLI script we do not
+# redirect. In a web application you would redirect the user to the URL
+# below.
+
+print "Go to the following link in your browser:"
+print "%s?oauth_token=%s" % (AUTHORIZE_URL, request_token['oauth_token'])
+print
+
+# After the user has granted access to you, the consumer, the provider will
+# redirect you to whatever URL you have told them to redirect to. You can
+# usually define this in the oauth_callback argument as well.
+accepted = 'n'
+while accepted.lower() == 'n':
+ accepted = raw_input('Have you authorized me? (y/n) ')
+oauth_verifier = raw_input('What is the PIN? ')
+
+# Step 3: Once the consumer has redirected the user back to the oauth_callback
+# URL you can request the access token the user has approved. You use the
+# request token to sign this request. After this is done you throw away the
+# request token and use the access token returned. You should store this
+# access token somewhere safe, like a database, for future use.
+token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret'])
+token.set_verifier(oauth_verifier)
+client = oauth.Client(consumer, token)
+
+resp, content = client.request(ACCESS_TOKEN_URL, "POST")
+access_token = dict(cgi.parse_qsl(content))
+
+print "%s" % content
+print "%s" % access_token
+
+print "Access Token:"
+print " - oauth_token = %s" % access_token['oauth_token']
+print " - oauth_token_secret = %s" % access_token['oauth_token_secret']
+print
+print "You may now access protected resources using the access tokens above."
+print
+
+
+
View
2  requirements/base.txt
@@ -41,7 +41,7 @@ django-uni-form==0.7.0
django-bookmarks==0.1.0
django-gravatar==0.1.0
django-mailer==0.2a1.dev1
-#django-friends==0.1.5
+django-friends==0.1.5
django-locations==0.1.1
django-oembed==0.1.1
django-swaps==0.5.3
View
2  requirements/project.txt
@@ -12,5 +12,5 @@ oauth-python-twitter
-e git://github.com/jbalogh/schematic.git#egg=schematic
python-memcached==1.45
-e git://github.com/jbalogh/django-multidb-router.git#egg=django-multidb-router
-
+-e hg+http://bitbucket.org/jespern/django-piston#egg=django-piston
#-e git+http://github.com/fwenzel/django-cas-consumer.git#egg=django-cas-consumer
View
7 settings.py
@@ -105,6 +105,8 @@
MIDDLEWARE_CLASSES = [
#"django.middleware.cache.UpdateCacheMiddleware",
"django.middleware.common.CommonMiddleware",
+ #'piston.middleware.CommonMiddlewareCompatProxy',
+ #'piston.middleware.ConditionalMiddlewareCompatProxy',
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
@@ -137,6 +139,7 @@
"django.core.context_processors.i18n",
"django.core.context_processors.media",
"django.core.context_processors.request",
+ "csrf_context.csrf",
"django.contrib.messages.context_processors.messages",
"pinax.core.context_processors.pinax_settings",
@@ -310,3 +313,7 @@ def write(*args, **kwargs):
FACEBOOK_CONSUMER_KEY = "GET A KEY FROM http://developers.facebook.com/setup"
FACEBOOK_CONSUMER_KEY = "GET A SECRET FROM http://developers.facebook.com/setup"
+
+OAUTH_AUTH_VIEW = "piston.authentication.oauth_auth_view"
+OAUTH_CALLBACK_VIEW = "badges.api.handlers.request_token_ready"
+
View
2  settings_local.py-dist-prod
@@ -4,9 +4,11 @@ from settings import *
# Make this unique, and don't share it with anybody.
SECRET_KEY = "BADGER BADGER BADGER BADGER MUSHROOM MUSHROOM"
+# create an app at: http://dev.twitter.com/apps
TWITTER_CONSUMER_KEY = "OAUTH KEY NEEDED"
TWITTER_CONSUMER_SECRET = "OAUTH SECRET NEEDED"
+# create an app at: http://www.facebook.com/developers/apps.php
FACEBOOK_CONSUMER_KEY = "OAUTH KEY NEEDED"
FACEBOOK_CONSUMER_SECRET = "OAUTH SECRET NEEDED"
View
55 templates/documentation.html
@@ -0,0 +1,55 @@
+{% load markup %}
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+"http://www.w3.org/TR/html4/strict.dtd">
+<html>
+ <head>
+ <title>
+ Piston generated documentation
+ </title>
+ <style type="text/css">
+ body {
+ background: #fffff0;
+ font: 1em "Helvetica Neue", Verdana;
+ padding: 0 0 0 25px;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>API Documentation</h1>
+
+ {% for doc in docs %}
+
+ <h3>{{ doc.name|cut:"Handler" }}:</h3>
+
+ <p>
+ {{ doc.get_doc|default:""|restructuredtext }}
+ </p>
+
+ <p>
+ URL: <b>{{ doc.get_resource_uri_template }}</b>
+ </p>
+
+ <p>
+ Accepted methods: {% for meth in doc.allowed_methods %}<b>{{ meth }}</b>{% if not forloop.last %}, {% endif %}{% endfor %}
+ </p>
+
+ <dl>
+ {% for method in doc.get_all_methods %}
+
+ <dt>
+ method <i>{{ method.name }}</i>({{ method.signature }}){% if method.stale %} <i>- inherited</i>{% else %}:{% endif %}
+
+ </dt>
+
+ {% if method.get_doc %}
+ <dd>
+ {{ method.get_doc|default:""|restructuredtext }}
+ <dd>
+ {% endif %}
+
+ {% endfor %}
+ </dl>
+
+ {% endfor %}
+ </body>
+</html>
Please sign in to comment.
Something went wrong with that request. Please try again.