Skip to content
Browse files

Bake Open Badge Infrastructure assertion into image attached to an Award

* Add ImageField to Award model
* Bake OBI assertion into PNG on Award save
* Badge issuer in OBI assertion changes to awarder when available
* Tweaks to images displayed for Badge and Award templates
* Tests
  • Loading branch information...
1 parent 2ab2a93 commit f1bba1b86dd3302004b4d6185ea93d4c47282c2e @lmorchard committed
View
17 TODO.md
@@ -6,31 +6,18 @@
### Core
-* Badge image by URL or admin image upload
-* Badge expiration dateime
+* Badge expiration datetime
* Templates
* jinja helpers
* django templatetags?
* Activity streams - JSON and Atom
* Re-work feeds to be AS compliant
* Come up with some templates that aren't totally ugly
-
-### Open Badge Infrastructure
-
-* Badge baker
- * Bake crypto assertion into PNG metadata
- * https://github.com/brianlovesdata/openbadges
+* Option in update_badges management command to overwrite existing.
### Multiplayer
* Nomination - create, delete, update, approve, reject
-* Badge images by upload or URL
- * URL accomodates badges in code
- * Restrict base URL of images?
-* Badge image upload
- * validate, scale, thumbnail, date-based filename
- * steal Demo Studio code for this?
-* Option in update_badges management command to overwrite existing.
* Conditional nominations
* Models
* Condition definition
View
BIN badger/fixtures/default-badge.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
93 badger/models.py
@@ -1,6 +1,10 @@
+import logging
+
from datetime import datetime
from time import time, gmtime, strftime
+from urlparse import urljoin
+
from django.conf import settings
from django.db import models
@@ -12,6 +16,7 @@
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import simplejson as json
from django.contrib.auth.models import User, AnonymousUser
+from django.contrib.sites.models import Site
from django.template.defaultfilters import slugify
@@ -184,7 +189,7 @@ class Badge(models.Model):
description = models.TextField(blank=True)
image = models.ImageField(blank=True, null=True,
storage=badge_uploads_fs,
- upload_to=mk_upload_to('badge_image.png'))
+ upload_to=mk_upload_to('image.png'))
prerequisites = models.ManyToManyField('self', symmetrical=False,
blank=True, null=True)
unique = models.BooleanField(default=False)
@@ -275,16 +280,20 @@ def progress_for(self, user):
p = Progress(user=user, badge=self)
return p
- def as_obi_serialization(self, request):
+ def as_obi_serialization(self, request=None):
"""Produce an Open Badge Infrastructure serialization of this badge"""
+ if request:
+ base_url = request.build_absolute_uri('/')
+ 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": request.build_absolute_uri(
- self.creator.get_absolute_url()),
+ "origin": urljoin(base_url, self.creator.get_absolute_url()),
"name": self.creator.username,
"contact": self.creator.email
}
@@ -294,14 +303,13 @@ def as_obi_serialization(self, request):
"version": OBI_VERSION,
# TODO: truncate more intelligently
"name": self.title[:128],
- "image": request.build_absolute_uri(
- "/img/html5-basic.png"),
# TODO: truncate more intelligently
"description": self.description[:128],
- "criteria": request.build_absolute_uri(
- self.get_absolute_url()),
+ "criteria": urljoin(base_url, self.get_absolute_url()),
"issuer": issuer
}
+ if self.image:
+ data['image'] = urljoin(base_url, self.image.url)
return data
@@ -318,6 +326,9 @@ class Award(models.Model):
objects = AwardManager()
badge = models.ForeignKey(Badge)
+ image = models.ImageField(blank=True, null=True,
+ storage=badge_uploads_fs,
+ upload_to=mk_upload_to('image.png'))
user = models.ForeignKey(User, related_name="award_user")
creator = models.ForeignKey(User, related_name="award_creator",
blank=True, null=True)
@@ -336,7 +347,7 @@ def get_absolute_url(self):
return ('badger.views.award_detail', (self.badge.slug, self.pk))
def get_upload_root(self):
- return "badge"
+ return "award"
def save(self, *args, **kwargs):
@@ -352,6 +363,7 @@ def save(self, *args, **kwargs):
badge_will_be_awarded.send(sender=self.__class__, award=self)
super(Award, self).save(*args, **kwargs)
+ self.bake_assertion_into_image(save=False)
if is_new:
# Only fire was-awarded signal on a new award.
@@ -365,18 +377,75 @@ def save(self, *args, **kwargs):
# Reset any progress for this user & badge upon award.
Progress.objects.filter(user=self.user, badge=self.badge).delete()
- def as_obi_assertion(self, request):
+ def as_obi_assertion(self, request=None, use_baked_image=True):
+ badge_data = self.badge.as_obi_serialization(request)
+
+ if request:
+ base_url = request.build_absolute_uri('/')
+ else:
+ base_url = 'http://%s' % (Site.objects.get_current().domain,)
+
+ # TODO: Award should have separate, assertion-baked image
+ if self.image and use_baked_image:
+ badge_data['image'] = urljoin(base_url, self.image.url)
+
+ # If this award has a creator (ie. not system-issued), tweak the issuer
+ # data to reflect award creator.
+ if self.creator:
+ badge_data['issuer'] = {
+ # TODO: Get from user profile instead?
+ "origin": urljoin(base_url, self.creator.get_absolute_url()),
+ "name": self.creator.username,
+ "contact": self.creator.email
+ }
+
# see: https://github.com/brianlovesdata/openbadges/wiki/Assertions
assertion = {
# TODO: Get email from profile? alternate identifier?
"recipient": self.user.email,
- "evidence": self.get_absolute_url(),
+ "evidence": urljoin(base_url, self.get_absolute_url()),
# "expires": "2013-06-01",
"issued_on": self.created.isoformat(),
- "badge": self.badge.as_obi_serialization()
+ "badge": badge_data
}
return assertion
+ def bake_assertion_into_image(self, request=None, save=True):
+ """Bake the OBI JSON badge award assertion into a copy of the original
+ badge's image, if one exists."""
+
+ if not self.badge.image:
+ # If there's no image to bake, bail.
+ # TODO: Bake a copy of a default badge image
+ return False
+
+ # Make a duplicate of the badge image
+ self.badge.image.open()
+ img_copy_fh = StringIO(self.badge.image.file.read())
+
+ try:
+ # Try processing the image copy, bail if the image is bad.
+ img = Image.open(img_copy_fh)
+ except IOError, e:
+ return False
+
+ # Here's where the baking gets done. JSON representation of the OBI
+ # assertion gets written into the "openbadges" metadata field
+ # see: http://blog.client9.com/2007/08/python-pil-and-png-metadata-take-2.html
+ # see: https://github.com/brianlovesdata/openbadges/blob/master/lib/baker.js
+ # see: https://github.com/brianlovesdata/openbadges/blob/master/controllers/baker.js
+ from PIL import PngImagePlugin
+ meta = PngImagePlugin.PngInfo()
+ assertion = self.as_obi_assertion(request, use_baked_image=False)
+ meta.add_text('openbadges', json.dumps(assertion))
+
+ # And, finally save out the baked image.
+ new_img = StringIO()
+ img.save(new_img, "PNG", pnginfo=meta)
+ img_data = new_img.getvalue()
+ self.image.save('', ContentFile(img_data), save)
+ return True
+
class ProgressManager(models.Manager):
pass
View
9 badger/templates/badger/includes/awards_list.html
@@ -1,6 +1,15 @@
<ul class="awards">
{% for award in award_list %}
+ {% if award.image %}
+ {% set img_url = award.image.url %}
+ {% elif award.badge.image %}
+ {% set img_url = award.badge.image.url %}
+ {% else %}
+ {% set img_url = "/media/img/default-badge.png" %}
+ {% endif %}
<li class="award">
+ <a href="{{ award.get_absolute_url() }}"><img src="{{ img_url }}"
+ alt="{{ award.badge.title }}" width="64" height="64" /></a>
<a href="{{ award.get_absolute_url() }}">
<span class="badge_title">{{ award.badge.title }}</span>
<span class="relation">awarded to</span>
View
5 badger/templates/badger/includes/badge_full.html
@@ -1,5 +1,8 @@
<dl class="badge" data-slug="{{ badge.slug }}">
- {% if badge.image %}
+ {% if award and award.image %}
+ <dt>Image:</dt>
+ <dd class="image"><img src="{{ award.image.url }}" width="256" height="256" /></dd>
+ {% elif badge.image %}
<dt>Image:</dt>
<dd class="image"><img src="{{ badge.image.url }}" width="256" height="256" /></dd>
{% endif %}
View
7 badger/templates/badger/includes/badges_list.html
@@ -1,6 +1,13 @@
<ul class="badges">
{% for badge in badge_list %}
+ {% if badge.image %}
+ {% set img_url = badge.image.url %}
+ {% else %}
+ {% set img_url = "/media/img/default-badge.png" %}
+ {% endif %}
<li class="badge">
+ <a href="{{ badge.get_absolute_url() }}"><img src="{{ img_url }}"
+ alt="{{ badge.title }}" width="64" height="64" /></a>
<a href="{{ badge.get_absolute_url() }}"><span class="title">{{ badge.title }}</span></a>
</li>
{% endfor %}
View
97 badger/tests/test_models.py
@@ -1,11 +1,18 @@
+from os.path import dirname
import logging
+try:
+ from PIL import Image
+except ImportError:
+ import Image
+
from django.conf import settings
from django.core.management import call_command
from django.db.models import loading
-
+from django.core.files.base import ContentFile
from django.http import HttpRequest
+from django.utils import simplejson as json
from django.test.client import Client
from nose.tools import assert_equal, with_setup, assert_false, eq_, ok_
@@ -21,10 +28,14 @@
from badger.models import (Badge, Award, Progress,
BadgeAwardNotAllowedException,
- BadgeAlreadyAwardedException)
+ BadgeAlreadyAwardedException,
+ SITE_ISSUER)
from badger.tests.badger_example.models import GuestbookEntry
+BASE_URL = 'http://example.com'
+BADGE_IMG_FN = "%s/fixtures/default-badge.png" % dirname(dirname(__file__))
+
class BadgerBadgeTest(BadgerTestCase):
@@ -56,6 +67,79 @@ def test_award_badge(self):
badge.award_to(awardee=user, awarder=badge.creator)
ok_(badge.is_awarded_to(user))
+ @attr('baked')
+ def test_baked_award_image(self):
+ """Award gets image baked with OBI assertion"""
+ # Get the source for a sample badge image
+ img_data = open(BADGE_IMG_FN, 'r').read()
+
+ # Make a badge with a creator
+ user_creator = self._get_user(username="creator")
+ badge = self._get_badge(title="Badge with Creator",
+ creator=user_creator)
+ badge.image.save('', ContentFile(img_data), True)
+
+ # Get some users who can award any badge
+ user_1 = self._get_user(username="superuser_1", is_superuser=True)
+ user_2 = self._get_user(username="superuser_2", is_superuser=True)
+
+ # Get some users who can receive badges
+ user_awardee_1 = self._get_user(username="awardee_1")
+ user_awardee_2 = self._get_user(username="awardee_1")
+
+ # Award a badge, and try to extract the badge assertion baked in
+ award_1 = badge.award_to(awardee=user_awardee_1)
+ ok_(award_1.image)
+ img = Image.open(award_1.image.file)
+ assertion = json.loads(img.info['openbadges'])
+
+ # Check the top-level award assertion data
+ eq_(award_1.user.email, assertion['recipient'])
+ eq_('%s%s' % (BASE_URL, award_1.get_absolute_url()),
+ assertion['evidence'])
+
+ # Check some of the badge details in the assertion
+ a_badge = assertion['badge']
+ eq_('0.5.0', a_badge['version'])
+ eq_(badge.title, a_badge['name'])
+ eq_(badge.description, a_badge['description'])
+ eq_('%s%s' % (BASE_URL, badge.get_absolute_url()),
+ a_badge['criteria'])
+
+ # Check the badge issuer details
+ b_issuer = a_badge['issuer']
+ eq_(badge.creator.username, b_issuer['name'])
+ eq_(badge.creator.email, b_issuer['contact'])
+ eq_('%s%s' % (BASE_URL, badge.creator.get_absolute_url()),
+ b_issuer['origin'])
+
+ # Award a badge, and check that the awarder appears as issuer
+ award_2 = badge.award_to(awardee=user_awardee_2, awarder=user_1)
+ ok_(award_2.image)
+ img = Image.open(award_2.image.file)
+ assertion = json.loads(img.info['openbadges'])
+ b_issuer = assertion['badge']['issuer']
+ eq_(user_1.username, b_issuer['name'])
+ eq_(user_1.email, b_issuer['contact'])
+ eq_('%s%s' % (BASE_URL, user_1.get_absolute_url()),
+ b_issuer['origin'])
+
+ # Make a badge with no creator
+ badge_no_creator = self._get_badge(title="Badge no Creator",
+ creator=False)
+ badge_no_creator.image.save('', ContentFile(img_data), True)
+
+ # Award a badge, and check that the site issuer is used
+ award_3 = badge_no_creator.award_to(awardee=user_awardee_1)
+ ok_(award_3.image)
+ img = Image.open(award_3.image.file)
+ assertion = json.loads(img.info['openbadges'])
+ logging.debug("ASSS %s" % assertion)
+ b_issuer = assertion['badge']['issuer']
+ eq_(SITE_ISSUER['name'], b_issuer['name'])
+ eq_(SITE_ISSUER['contact'], b_issuer['contact'])
+ eq_(SITE_ISSUER['origin'], b_issuer['origin'])
+
def test_award_unique_duplication(self):
"""Only one award for a unique badge can be created"""
user = self._get_user()
@@ -94,17 +178,22 @@ def test_progress_badge_already_awarded(self):
eq_(0, Progress.objects.filter(badge=b, user=user).count())
def _get_user(self, username="tester", email="tester@example.com",
- password="trustno1"):
+ password="trustno1", is_staff=False, is_superuser=False):
(user, created) = User.objects.get_or_create(username=username,
defaults=dict(email=email))
if created:
+ user.is_superuser = is_superuser
+ user.is_staff = is_staff
user.set_password(password)
user.save()
return user
def _get_badge(self, title="Test Badge",
description="This is a test badge", creator=None):
- creator = creator or self.user_1
+ if creator is None:
+ creator = self.user_1
+ elif creator is False:
+ creator = None
(badge, created) = Badge.objects.get_or_create(title=title,
defaults=dict(description=description, creator=creator))
return badge
View
9 badger/tests/test_views.py
@@ -64,7 +64,8 @@ def test_badge_detail(self):
data = simplejson.loads(r.content)
eq_(badge.title, data['name'])
eq_(badge.description, data['description'])
- eq_(badge.get_absolute_url(), data['criteria'])
+ eq_('http://testserver%s' % badge.get_absolute_url(),
+ data['criteria'])
@attr('json')
def test_award_detail(self):
@@ -89,10 +90,12 @@ def test_award_detail(self):
data = simplejson.loads(r.content)
eq_(award.user.email, data['recipient'])
- eq_(award.get_absolute_url(), data['evidence'])
+ 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_(award.badge.get_absolute_url(), data['badge']['criteria'])
+ eq_('http://testserver%s' % award.badge.get_absolute_url(),
+ data['badge']['criteria'])
def test_awards_by_user(self):

0 comments on commit f1bba1b

Please sign in to comment.
Something went wrong with that request. Please try again.