diff --git a/badger/models.py b/badger/models.py index f7b3bd6..2d78fe1 100644 --- a/badger/models.py +++ b/badger/models.py @@ -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 @@ -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 @@ -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): @@ -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 @@ -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): @@ -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 diff --git a/badger/tests/test_models.py b/badger/tests/test_models.py index cd4a4ac..8947699 100644 --- a/badger/tests/test_models.py +++ b/badger/tests/test_models.py @@ -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 @@ -44,7 +48,8 @@ NominationApproveNotAllowedException, NominationAcceptNotAllowedException, NominationRejectNotAllowedException, - SITE_ISSUER, slugify) + SITE_ISSUER, DEFAULT_BADGE_IMAGE_URL, + slugify) from badger_example.models import GuestbookEntry @@ -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 diff --git a/badger/tests/test_views.py b/badger/tests/test_views.py index 262fd38..597daad 100644 --- a/badger/tests/test_views.py +++ b/badger/tests/test_views.py @@ -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""" @@ -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""" diff --git a/badger/urls.py b/badger/urls.py index a7941dc..0686132 100644 --- a/badger/urls.py +++ b/badger/urls.py @@ -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.+)/?$', 'badges_list', diff --git a/badger/urls_simple.py b/badger/urls_simple.py index cad44b4..7579ac5 100644 --- a/badger/urls_simple.py +++ b/badger/urls_simple.py @@ -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.+)/?$', 'badges_list', name='badger.badges_list'), url(r'^awards/?', 'awards_list', diff --git a/badger/views.py b/badger/views.py index 5830718..6c7c0d1 100644 --- a/badger/views.py +++ b/badger/views.py @@ -45,7 +45,8 @@ Progress, BadgeAwardNotAllowedException, BadgeAlreadyAwardedException, NominationApproveNotAllowedException, - NominationAcceptNotAllowedException) + NominationAcceptNotAllowedException, + SITE_ISSUER) from .forms import (BadgeAwardForm, DeferredAwardGrantForm, DeferredAwardMultipleGrantForm, BadgeNewForm, BadgeEditForm, BadgeSubmitNominationForm) @@ -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""" diff --git a/test_settings.py b/test_settings.py index 33fdd90..314580f 100644 --- a/test_settings.py +++ b/test_settings.py @@ -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': 'me@mozilla.org', + 'revocationList': 'http://badges.mozilla.org/revoke.json', +}