Badges app update

Status

IN DEVELOPMENT, PROPOSAL READY FOR REVIEW

Migrations

YES


Description

The main impetus for this is to fix the broken integration with Badgr.io backends.

The integration with Badgr on Badgr.io is now broken.  Badge awarding won't work for most use cases.   It should still work with the open source version of badgr-server but Concentric Sky tells me that a new release of this is forthcoming in December 2017 and will bring the badgr-server API in line with the Badgr.io site.  (Badgr.io seems to have implemented some kind of "forwarding" from existing Badge Classes with old style slugs to the new slug.  A new API call to create a Badge Class for slug 'openedx__coursefooslug' will not create a Badge Class addressable by that slug, but older Badge Classes will still be found by a GET to an old-style slug).

goals: 

  1. Decouple backend ids from BadgeClass slugs in Open edX
  2. Supports these use cases for creating and awarding badges:
    1. (new) Admin creates a BadgeClass in Django admin, matching existing BadgeClass in Badgr.  Same Badge image is uploaded to Badgr and LMS admin.  Course id specified. Mode optional.  When learner earns certificate in specified course and mode, learner will automatically be awarded a badge for this BadgeClass.  If mode not specified, when learner earns certificate in specified course, and no other BadgeClass exists specifying both the course and mode matching the learner, the learner will automatically be awarded a badge for this BadgeClass.
    2. (new) Admin creates a BadgeClass in Django admin, matching existing Badge Class in Badgr.  Same Badge image is uploaded to Badgr and LMS admin.  Course id not specified.  Mode is not specified.  BadgeClass can be used for course group, enrollment, or completion number badges.  
    3. (bugfix) Admin creates a BadgeClass in Django admin.  No existing backend Badge Class exists.  When first learner earns a certificate in this course, Badge Class created in Badgr with matching name, criteria, description and image.  New backend id retrieved and stored on BadgeClass object in Open edX.   Learner awarded badge in new Badge Class.  
    4. (bugfix) Admin enables badges on a course but does not create a BadgeClass in Django admin.  When first learner earns a certificate in this course, BadgeClass created in Django with course id, course mode matching learner's certificate, and default course mode badge image.  Description and criteria auto-generated as current functionality from course name/dates and about page URL respectively.  Matching Badge Class created in Badgr backend.  Backend id stored on Django BadgeClass.  Learner is awarded a badge in Badgr.  
    5. (bugfix) Admin creates an entry in  enrollment, course group, or course completion number badge event configurations using Open edX BadgeClass slug as the key.  Learner enrolls in or completes x number of courses, or earns a certificate in all courses in course group.  Badge awarded in backend for Badgr Badge Class with backend id paired with Open edX slug in configuration entry. 

Related PRs

I've started with implementation of this plan, and will post PRs when I am farther along, but I am looking for architectural input now.


List related PRs:


| branch      | PR   |

| ----------- | ---- |

| repo/branch | link |

| repo/branch | link |


Todos


Do you think edX will need to perform the following?


* [ X] Product manager review

* [ ] UX/UI review

* [ ] Accessibility review

* [ X] Technical design review

* [ X] Documentation review  - documentation of changes to Badging will be outside of the PR



Steps to Test or Reproduce Current Problems

The following steps will demonstrate the current problem with generating and awarding badges via Badgr.io.  Some of the expected results may not have been original intentions of the badges app creator, but are likely expectations of users.


INITIAL SETUP

  • Create an account and Issuer on Badgr.io
  • In your Open edX install, enable issuance of Open Badges via /edx/app/edxapp/*.env.json
  • Retrieve a Badgr.io API auth token by making a request to  

    GET /v1/user/auth-token, passing Badgr.io username and password as Basic Auth credentials

  • In /edx/app/edxapp/*.env.json Set 
    BADGR_API_TOKEN: (to this token),
    BADGR_BASE_URL: "https://api.badgr.io"
    BADGR_ISSUER_SLUG: (your issue slug)
  • Create a course in Studio, 'course-v1:edX+MyCourse+2017', called "My Course".  Make sure Open Badges are enabled in Advanced Settings.  Allow honor certificates for this course.  Don't specify an end date.


AUTOMATIC COURSE COMPLETE BADGE CLASS CREATION

  1. As an LMS user, earn a certificate on a course with Badges enabled
  2. Return to your Issuer's page on Badgr.io.  


Expected:

A Badge Class exists called 'My Course'.  It has the default 'honor' course mode Image.  It has 1 recipient, which is your user's email address.  The recipient has the evidence URL matching the webview to the certificate you earned.


Actual:
A new Badge Class with the default course mode badge image, with the name of your course.  It has not been awarded -  "0 recipients".  


MANUAL BADGE CLASS CREATION STARTING FROM DJANGO ADMIN

  1. Browse to /admin/badges/badgeclass and Add a BadgeClass
  2. Specify a slug manually, like 'my-badge-slug'
  3. Specify an Issuing Component: 'openedx__course'
  4. Specify a course id, like 'course-v1:edx+MyCourse+2017'
  5. Add any Description, 'foo'
  6. Add the course About page as Criteria, e.g., 'http://mylms.com/courses/course-v1:edx+MyCourse+2017/about'
  7. Add a mode, 'honor'
  8. Upload a square PNG image as the badge image
  9. save your new badge class
  10. Verify that Badges are enabled on course-v1:edx+MyCourse+2017
  11. Earn a certificate in course-v1:edx+MyCourse+2017
  12. Return to your Issuer's page on Badgr.io 

Expected:
A Badge Class exists called 'My Course', with the same PNG graphic as the Image you chose in Django admin, the Description 'foo', and the Criteria 'http://mylms.com/courses/course-v1:edx+MyCourse+2017/about'.  It has 1 recipient, which is your LMS user's email address.  The recipient award has the evidence URL matching the webview to the certificate you earned.

Actual:

You will find a new Badge Class has been created in Badgr, with the name of your course, the criteria 'http://mylms.com/courses/course-v1:edx+MyCourse+2017/about', the  Description 'Completed the course "My Course" (honor) '.  It has no recipients and usees the default course mode badge image


MANUAL BADGE CLASS CREATION VIA BADGR.IO INTERFACE, MATCHING IN OPEN EDX

  1. Create a new Badge Class in Badgr.  
  2. Use the name of your test course as Name, 'foo' as Description, your course's About page url as the Criteria URL, and a randomly generated Badge image. 
  3. Download the generated badge image.
  4. Copy the slug of the Badge Class, which is the text after the final forward slash in its URL
  5. Go to /admin/badges/badgeclass in the LMS django admin
  6. Add a BadgeClass.  Set the same Name, Description, Criteria URL.  Upload the badge image you downloaded.  Set course id to course-v1:edx+MyCourse+2017.  Set mode 'honor'.   Set slug to the slug you copied from Badgr.io.
  7. Earn a certificate in your course.
  8. On Badgr.io, return to the Badge Class you created 

Expected:

Your Badge Class has 1 recipient, which is your user's email address.  The recipient award has the evidence URL matching the webview to the certificate you earned.

Actual:

Your Badge Class has 0 recipients.

Impacted Areas in Application


Badges app, student Accomplishments, course module field default change for Issue Open Badges field.


Development and Design Notes


(new) Course completion badges awarding turned off by default

In our experience (at Appsembler), end users do not wish to award badges for completion of all courses.  Other changes proposed here would make BadgeClass creation and badge issuance automatic for all courses with "Issue Open Badges" set to True.  This should be changed to default to False to support the more likely common use case.  Site-wide feature flag ENABLE_OPENBADGES should continue to default to False.  


(bugfix) Changes to BadgeClass slugs

Currently, BadgeClass slugs are not unique.  Uniqueness of BadgeClasses is determined by a combination of slug, course id, and issuing component.  I propose making slug unique, and using AutoSlugField to generate a sensible value from the combination of course id, mode, and issuing component if used (This would involve adding AutoSlugField as a new requirement).  BadgeClasses would be unique by slug, as well as by course id and mode together.  


(bugfix) Separate Open edX BadgeClass slug from backend Badge Class id 

The main issue is that Badgr.io will no longer accept a custom slug when creating a Badge Class through the API.  It now populates the 'slug' AutoSlugField and related Id field with a UUID-based identifier.  The LMS badges app currently "slugifies" the BadgeClass Name and Issuing Component values and passes that to the Badgr API as the requested backend Badge Class slug, expecting that this will be a way to look up the slug in the backend.  This no longer works.  The API request, will, however, return the assigned Badgr slug and id (IRI) in the Response so we can store this value on the BadgeClass.

I'm proposing to decouple Open edX BadgeClass slugs and backend Badge Class ids.  In the case of Badgr, the backend identifier is a Django slug storing a UUID, but to support other backends, we should just call this an 'id'.    ('Slug' is not a part of the Open Badges spec—it only specifies that Badge Classes have an IRI id.)   It does still make sense to maintain a "human-friendly" Django slug for the BadgeClass object, to make it easier to reference using the Django admin, especially for creating course group event configurations.  

The backend implementation (Badgr only for now) should expect an API call for creation of a backend Badge Class to return the Id.  We can store this backend Id in a new field on Badge Class.  For the Badgr backend we will store the value of Badgr slug.  

(I think it should be possible to specify separate backends/issuers per BadgeClass, but this could be done in a future phase.  You could argue that the BadgeClass object should be completely independent of backend, but I'm not sure the complexity of storing a lookup from BadgeClass to backend in another persistent object is worthwhile overhead.  ).

We will need to implement a data migration for existing BadgeClass objects to populate the new backend id field since the slug field is no longer a reliable future-proof identifier.   The migration will involve a call to the backend API.  


(new) Change how course completion badges are found

Currently, when a user is granted a certificate, a course complete badge is looked up by querying by a slug that must be in a very specific format derived from a hash of the course key and mode.  If a user tries to set up a BadgeClass manually via the Django Admin and picks a common sense slug, course completion badge awarding will fail.   The only way a Badge Class would be created with a slug in that format currently needed is if it is automatically created after a certificate event, in which case the Badge image used will be a generic image for an enrollment mode.  Most users outside of edX will wish to manually create their Badge Classes and use their own badge image related to a course or educational goal.  Many will wish to first create a Badge Class in the backend, then copy over the details to the LMS Django admin.   

I propose several things to address these use cases and the current problems.

  1. Currently, BadgeClass objects are unique by combination of course id, issuing component, and slug.  Instead I propose that a slug should be unique on its own, and that one BadgeClass per combination of course id and course mode should be allowed.  It makes sense Course A could have a separate badge for honor students and verified students.  Or, with a blank mode field, all Course A certificate earners should be awarded the same badge.  
  2. When a certificate is awarded for a course, the matching BadgeClass should be found by the course id combined with the student's enrollment mode.  Slug would not be used as a lookup unless passed explicitly.

(new) Change how course group, completion, and enrollment badges are found

Currently, lookups to the course group, courses completed and enrollment configuration objects are filtered on Issuing Component = 'openedx__course'.  I'm not sure that's necessary.  With my proposed changes, a Badge Class slug will be unique and can be used as the key for which slug to award.  Requiring a specific Issuing Component value would be redundant in terms of ensuring uniqueness.

(new) Store Issuing Component as tag on backend Badge Class

The Open edX badges app has the concept of Issuing Component.  This will no longer be part of a backend id, but is a potentially useful concept for distinguishing between course-scoped (Issuing Component="openedx__course") BadgeClasses and possible other scopes (e.g., Issuing Component="openedx__xseries").  A potential future Studio UI for Badge Class creation might support the automatic assignment of appropriate Issuing Component values. 

To encourage the use of Issuing Component as a useful cross-Issuer BadgeClass identifier, I propose to append its value to the 'tags' field value in the backend.  The Open Badges spec v 2.0 supports a tags field which is an array of text values.

Has edX proposed a broader list of possible Issuing Component values?  If so, what are the possible values?



Deployment Notes


Deployment will involve a mandatory data migration.  App requirements will have to be updated to use the AutoSlugField package.