Skip to content

Commit

Permalink
Fix mozilla#158: Updates to use latest OBI JSON specs
Browse files Browse the repository at this point in the history
  • Loading branch information
lmorchard committed Jan 24, 2014
1 parent 1426648 commit e17cd73
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 52 deletions.
78 changes: 35 additions & 43 deletions badger/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from datetime import datetime, timedelta, tzinfo
from time import time, gmtime, strftime
import calendar

import os.path
from os.path import dirname
Expand Down Expand Up @@ -84,10 +85,10 @@
IMG_MAX_SIZE = getattr(settings, "BADGER_IMG_MAX_SIZE", (256, 256))

SITE_ISSUER = getattr(settings, 'BADGER_SITE_ISSUER', {
"origin": "http://mozilla.org",
"name": "Badger",
"org": "Mozilla",
"contact": "lorchard@mozilla.com"
"name": "Example",
"url": "http://example.com",
"description": "This is an example organization",
"email": "me@example.com"
})

# Set up a file system for badge uploads that can be kept separate from the
Expand Down Expand Up @@ -443,7 +444,9 @@ class Meta:
def __unicode__(self):
return self.title

def get_absolute_url(self):
def get_absolute_url(self, format='html'):
if format == 'json':
return reverse('badger.detail_json', args=(self.slug,))
return reverse('badger.views.detail', args=(self.slug,))

def get_upload_meta(self):
Expand Down Expand Up @@ -650,32 +653,21 @@ def as_obi_serialization(self, request=None):
else:
base_url = 'http://%s' % (Site.objects.get_current().domain,)

# see: https://github.com/brianlovesdata/openbadges/wiki/Assertions
if not self.creator:
issuer = SITE_ISSUER
else:
issuer = {
# TODO: Get from user profile instead?
"origin": urljoin(base_url, self.creator.get_absolute_url()),
"name": self.creator.username,
"contact": self.creator.email
}

data = {
# The version of the spec/hub this manifest is compatible with. Use
# "0.5.0" for the beta.
"version": OBI_VERSION,
# TODO: truncate more intelligently
"name": self.title[:128],
# TODO: truncate more intelligently
"description": self.description[:128] or self.title[:128],
"criteria": urljoin(base_url, self.get_absolute_url()),
"issuer": issuer
"issuer": urljoin(base_url, reverse('badger.site_issuer'))
}

image_url = self.image and self.image.url or DEFAULT_BADGE_IMAGE_URL
data['image'] = urljoin(base_url, image_url)

# TODO: tags
# TODO: alignment

return data


Expand Down Expand Up @@ -717,7 +709,9 @@ def __unicode__(self):
return u'Award of %s to %s%s' % (self.badge, self.user, by)

@models.permalink
def get_absolute_url(self):
def get_absolute_url(self, format='html'):
if format == 'json':
return ('badger.award_detail_json', (self.badge.slug, self.pk))
return ('badger.views.award_detail', (self.badge.slug, self.pk))

def get_upload_meta(self):
Expand Down Expand Up @@ -785,40 +779,38 @@ def delete(self):
super(Award, self).delete()

def as_obi_assertion(self, request=None):
badge_data = self.badge.as_obi_serialization(request)

"""Build a representation of this award as an OBI assertion"""
if request:
base_url = request.build_absolute_uri('/')[:-1]
else:
base_url = 'http://%s' % (Site.objects.get_current().domain,)

# If this award has a creator (ie. not system-issued), tweak the issuer
# data to reflect award creator.
# TODO: Is this actually a good idea? Or should issuer be site-wide
if self.creator:
badge_data['issuer'] = {
# TODO: Get from user profile instead?
"origin": base_url,
"name": self.creator.username,
"contact": self.creator.email
}

# see: https://github.com/brianlovesdata/openbadges/wiki/Assertions
# TODO: This salt is stable, and the badge.pk is generally not
# disclosed anywhere, but is it obscured enough?
hash_salt = (hashlib.md5('%s-%s' % (self.badge.pk, self.pk))
hash_salt = (hashlib.md5('%s-%s-%s' % (self.badge.pk,
self.pk,
settings.SECRET_KEY))
.hexdigest())
recipient_text = '%s%s' % (self.user.email, hash_salt)
recipient_hash = ('sha256$%s' % hashlib.sha256(recipient_text)
.hexdigest())
assertion = {
"recipient": recipient_hash,
"salt": hash_salt,
"uid": '%s' % self.id,
"recipient": {
"type": "email",
"hashed": True,
"salt": hash_salt,
"identity": recipient_hash
},
"badge": urljoin(base_url,
self.badge.get_absolute_url(format='json')),
"verify": {
"type": "hosted",
"url": urljoin(base_url,
self.get_absolute_url(format='json'))
},
"evidence": urljoin(base_url, self.get_absolute_url()),
"issuedOn": calendar.timegm(self.created.utctimetuple()),
# TODO: implement award expiration
# "expires": self.expires.date().isoformat(),
"issued_on": self.created.date().isoformat(),
"badge": badge_data
# "expires": calendar.timegm(self.expires.utctimetuple()),
}
return assertion

Expand Down
60 changes: 59 additions & 1 deletion badger/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@
except ImportError:
import Image

from urlparse import urljoin
import hashlib

from django.conf import settings

from django.core.management import call_command
from django.db.models import loading
from django.contrib.sites.models import Site
from django.core.files.base import ContentFile
from django.http import HttpRequest
import json
Expand Down Expand Up @@ -44,7 +48,8 @@
NominationApproveNotAllowedException,
NominationAcceptNotAllowedException,
NominationRejectNotAllowedException,
SITE_ISSUER, slugify)
SITE_ISSUER, DEFAULT_BADGE_IMAGE_URL,
slugify)

from badger_example.models import GuestbookEntry

Expand Down Expand Up @@ -116,6 +121,59 @@ def test_award_unique_duplication(self):

class BadgerOBITest(BadgerTestCase):

def test_badge_class_data(self):

# Make a badge with a creator
user_creator = self._get_user(username="creator")
badge = self._get_badge(title="Badge with Creator",
creator=user_creator)

base_url = 'http://%s' % (Site.objects.get_current().domain,)

obi = badge.as_obi_serialization()
eq_(obi['name'], badge.title[:128])
eq_(obi['description'], badge.description[:128] or self.title[:128])
eq_(obi['image'], urljoin(base_url, DEFAULT_BADGE_IMAGE_URL))
eq_(obi['criteria'], urljoin(base_url, badge.get_absolute_url()))
eq_(obi['issuer'],
urljoin(base_url, reverse('badger.site_issuer')))
# TODO: tags
# TODO: alignment

def test_badge_assertion_data(self):
user_creator = self._get_user(username="creator")
user_awardee = self._get_user(username="awardee_1")

badge = self._get_badge(title="Badge with Creator",
creator=user_creator)
award = badge.award_to(awardee=user_awardee)

obi = award.as_obi_assertion()

base_url = 'http://%s' % (Site.objects.get_current().domain,)

eq_(obi['uid'], '%s' % award.id)

hash_salt = obi['recipient']['salt']
recipient_text = '%s%s' % (award.user.email, hash_salt)
recipient_hash = ('sha256$%s' % hashlib.sha256(recipient_text)
.hexdigest())
eq_(obi['recipient']['type'], 'email')
ok_(obi['recipient']['hashed'])
eq_(obi['recipient']['identity'], recipient_hash)

eq_(obi['badge'],
urljoin(base_url, badge.get_absolute_url(format='json')))

eq_(obi['verify']['type'], 'hosted')
eq_(obi['verify']['url'],
urljoin(base_url, award.get_absolute_url(format='json')))

eq_(type(obi['issuedOn']), int)

eq_(obi['evidence'],
urljoin(base_url, award.get_absolute_url()))

def test_baked_award_image(self):
"""Award gets image baked with OBI assertion"""
# Get the source for a sample badge image
Expand Down
21 changes: 14 additions & 7 deletions badger/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ def tearDown(self):
Award.objects.all().delete()
Badge.objects.all().delete()

def test_site_issuer(self):
"""Can fetch site issuer details"""
url = reverse('badger.site_issuer')
r = self.client.get(url, follow=True)
data = json.loads(r.content)
eq_(data, settings.BADGER_SITE_ISSUER)

@attr('json')
def test_badge_detail(self):
"""Can view badge detail"""
Expand Down Expand Up @@ -96,19 +103,19 @@ def test_award_detail(self):

data = json.loads(r.content)

hash_salt = (hashlib.md5('%s-%s' % (award.badge.pk, award.pk))
.hexdigest())
hash_salt = data['recipient']['salt']
recipient_text = '%s%s' % (award.user.email, hash_salt)
recipient_hash = ('sha256$%s' % hashlib.sha256(recipient_text)
.hexdigest())

eq_(recipient_hash, data['recipient'])
eq_('email', data['recipient']['type'])
ok_(data['recipient']['hashed'])
eq_(recipient_hash, data['recipient']['identity'])
eq_('http://testserver%s' % award.get_absolute_url(),
data['evidence'])
eq_(award.badge.title, data['badge']['name'])
eq_(award.badge.description, data['badge']['description'])
eq_('http://testserver%s' % award.badge.get_absolute_url(),
data['badge']['criteria'])
eq_('http://testserver%s' %
award.badge.get_absolute_url(format='json'),
data['badge'])

def test_awards_by_user(self):
"""Can view awards by user"""
Expand Down
1 change: 1 addition & 0 deletions badger/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

urlpatterns = patterns('badger.views',
url(r'^$', 'badges_list', name='badger.badges_list'),
url(r'^issuer.json$', 'site_issuer', name='badger.site_issuer'),
url(r'^staff_tools$', 'staff_tools',
name='badger.staff_tools'),
url(r'^tag/(?P<tag_name>.+)/?$', 'badges_list',
Expand Down
1 change: 1 addition & 0 deletions badger/urls_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

urlpatterns = patterns('badger.views',
url(r'^$', 'badges_list', name='badger.badges_list'),
url(r'^issuer.json$', 'site_issuer', name='badger.site_issuer'),
url(r'^tag/(?P<tag_name>.+)/?$', 'badges_list',
name='badger.badges_list'),
url(r'^awards/?', 'awards_list',
Expand Down
7 changes: 6 additions & 1 deletion badger/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
Progress, BadgeAwardNotAllowedException,
BadgeAlreadyAwardedException,
NominationApproveNotAllowedException,
NominationAcceptNotAllowedException)
NominationAcceptNotAllowedException,
SITE_ISSUER)
from .forms import (BadgeAwardForm, DeferredAwardGrantForm,
DeferredAwardMultipleGrantForm, BadgeNewForm,
BadgeEditForm, BadgeSubmitNominationForm)
Expand All @@ -61,6 +62,10 @@ def home(request):
badge_list=badge_list, award_list=award_list, badge_tags=badge_tags
), context_instance=RequestContext(request))

def site_issuer(request):
resp = HttpResponse(json.dumps(SITE_ISSUER))
resp['Content-Type'] = 'application/json'
return resp

class BadgesListView(ListView):
"""Badges list page"""
Expand Down
9 changes: 9 additions & 0 deletions test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,12 @@
}

BADGER_TEMPLATE_BASE = 'badger'

BADGER_SITE_ISSUER = {
'name': 'Badger Tests',
'url': 'http://badges.mozilla.org',
'description': 'This is a testing organization',
'image': 'http://mozorg.cdn.mozilla.net/media/img/home/firefox-sm.png',
'email': '[email protected]',
'revocationList': 'http://badges.mozilla.org/revoke.json',
}

0 comments on commit e17cd73

Please sign in to comment.