diff --git a/lms/djangoapps/badges/backends/badgr.py b/lms/djangoapps/badges/backends/badgr.py index d40efcf1c340..f01bb3bd6a6b 100644 --- a/lms/djangoapps/badges/backends/badgr.py +++ b/lms/djangoapps/badges/backends/badgr.py @@ -1,13 +1,14 @@ """ Badge Awarding backend for Badgr-Server. """ -import hashlib import logging import mimetypes import requests from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError from lazy import lazy from requests.packages.urllib3.exceptions import HTTPError @@ -57,20 +58,6 @@ def _assertion_url(self, slug): """ return "{}/assertions".format(self._badge_url(slug)) - def _slugify(self, badge_class): - """ - Get a compatible badge slug from the specification. - """ - slug = badge_class.issuing_component + badge_class.slug - if badge_class.issuing_component and badge_class.course_id: - # Make this unique to the course, and down to 64 characters. - # We don't do this to badges without issuing_component set for backwards compatibility. - slug = hashlib.sha256(slug + unicode(badge_class.course_id)).hexdigest() - if len(slug) > MAX_SLUG_LENGTH: - # Will be 64 characters. - slug = hashlib.sha256(slug).hexdigest() - return slug - def _log_if_raised(self, response, data): """ Log server response if there was an error. @@ -102,10 +89,14 @@ def _create_badge(self, badge_class): u"Filename was: {}".format(image.name) ) files = {'image': (image.name, image, content_type)} + try: # TODO: eventually we should pass both + URLValidator(badge_class.criteria) + criteria_type = 'criteria_url' + except ValidationError: + criteria_type = 'criteria_text' data = { 'name': badge_class.display_name, - 'criteria': badge_class.criteria, - 'slug': self._slugify(badge_class), + criteria_type: badge_class.criteria, 'description': badge_class.description, } result = requests.post( @@ -142,7 +133,7 @@ def _create_assertion(self, badge_class, user, evidence_url): 'evidence': evidence_url, } response = requests.post( - self._assertion_url(self._slugify(badge_class)), headers=self._get_headers(), data=data, + self._assertion_url(badge_class.slug), headers=self._get_headers(), data=data, timeout=settings.BADGR_TIMEOUT ) self._log_if_raised(response, data) @@ -166,7 +157,7 @@ def _ensure_badge_created(self, badge_class): """ Verify a badge has been created for this badge class, and create it if not. """ - slug = self._slugify(badge_class) + slug = badge_class.slug if slug in BadgrBackend.badges: return response = requests.get(self._badge_url(slug), headers=self._get_headers(), timeout=settings.BADGR_TIMEOUT) diff --git a/lms/djangoapps/badges/events/course_complete.py b/lms/djangoapps/badges/events/course_complete.py index 6f531b2e6461..e5397b01c33c 100644 --- a/lms/djangoapps/badges/events/course_complete.py +++ b/lms/djangoapps/badges/events/course_complete.py @@ -1,11 +1,9 @@ """ Helper functions for the course complete event that was originally included with the Badging MVP. """ -import hashlib import logging from django.core.urlresolvers import reverse -from django.template.defaultfilters import slugify from django.utils.translation import ugettext_lazy as _ from badges.models import CourseCompleteImageConfiguration, BadgeClass, BadgeAssertion @@ -19,21 +17,6 @@ # migrations. Please check the badge migrations when changing any of these functions. -def course_slug(course_key, mode): - """ - Legacy: Not to be used as a model for constructing badge slugs. Included for compatibility with the original badge - type, awarded on course completion. - - Slug ought to be deterministic and limited in size so it's not too big for Badgr. - - Badgr's max slug length is 255. - """ - # Seven digits should be enough to realistically avoid collisions. That's what git services use. - digest = hashlib.sha256(u"{}{}".format(unicode(course_key), unicode(mode))).hexdigest()[:7] - base_slug = slugify(unicode(course_key) + u'_{}_'.format(mode))[:248] - return base_slug + digest - - def badge_description(course, mode): """ Returns a description for the earned badge. @@ -85,8 +68,6 @@ def get_completion_badge(course_id, user): if not course.issue_badges: return None return BadgeClass.get_badge_class( - slug=course_slug(course_id, mode), - issuing_component='', criteria=criteria(course_id), description=badge_description(course, mode), course_id=course_id, diff --git a/lms/djangoapps/badges/events/course_meta.py b/lms/djangoapps/badges/events/course_meta.py index e5bbb99c338d..fa49bc0830c1 100644 --- a/lms/djangoapps/badges/events/course_meta.py +++ b/lms/djangoapps/badges/events/course_meta.py @@ -5,6 +5,7 @@ from badges.models import CourseEventBadgesConfiguration, BadgeClass from badges.utils import requires_badges_enabled +from badges.events.course_complete import evidence_url def award_badge(config, count, user): @@ -23,7 +24,7 @@ def award_badge(config, count, user): if not slug: return badge_class = BadgeClass.get_badge_class( - slug=slug, issuing_component='openedx__course', create=False, + slug=slug, create=False, ) if not badge_class: return @@ -71,11 +72,24 @@ def course_group_check(user, course_key): course_id__in=keys, ) if len(certs) == len(keys): - awards.append(slug) + # course_complete Assertions are not working correctly + # yet with Badgr.io, while course group is working. + # so we use course groups with a single course, + # in which case we can provide an evidence URL + # to the HTML cert for the one coursee + if len(keys) == 1: + evidence = evidence_url(user.id, course_key) + awards.append((slug, evidence)) + else: + awards.append(slug) - for slug in awards: + for award in awards: badge_class = BadgeClass.get_badge_class( - slug=slug, issuing_component='openedx__course', create=False, + slug=award[0], create=False, ) if badge_class and not badge_class.get_for_user(user): - badge_class.award(user) + if award[1]: + badge_class.award(user, evidence_url=award[1]) + else: + badge_class.award(user) + diff --git a/lms/djangoapps/badges/models.py b/lms/djangoapps/badges/models.py index 484e7cd11064..bb84cf1d5e3f 100644 --- a/lms/djangoapps/badges/models.py +++ b/lms/djangoapps/badges/models.py @@ -48,28 +48,28 @@ class BadgeClass(models.Model): """ Specifies a badge class to be registered with a backend. """ - slug = models.SlugField(max_length=255, validators=[validate_lowercase]) + slug = models.SlugField(max_length=255, unique=True) issuing_component = models.SlugField(max_length=50, default='', blank=True, validators=[validate_lowercase]) display_name = models.CharField(max_length=255) course_id = CourseKeyField(max_length=255, blank=True, default=None) description = models.TextField() - criteria = models.TextField() - # Mode a badge was awarded for. Included for legacy/migration purposes. + criteria = models.TextField() # TODO: Badgr and Open Badges spec can take both text and url criteria mode = models.CharField(max_length=100, default='', blank=True) image = models.ImageField(upload_to='badge_classes', validators=[validate_badge_image]) def __unicode__(self): - return u"".format( - slug=self.slug, issuing_component=self.issuing_component + return u"".format( + slug=self.slug, issuing_component=self.issuing_component, + course_id = unicode(self.course_id), mode=self.mode ) @classmethod def get_badge_class( - cls, slug, issuing_component, display_name=None, description=None, criteria=None, image_file_handle=None, + cls, slug=None, issuing_component=None, display_name=None, description=None, criteria=None, image_file_handle=None, mode='', course_id=None, create=True ): """ - Looks up a badge class by its slug, issuing component, and course_id and returns it should it exist. + Looks up a badge class by its slug, or combination of mode and course_id and returns it should it exist. If it does not exist, and create is True, creates it according to the arguments. Otherwise, returns None. The expectation is that an XBlock or platform developer should not need to concern themselves with whether @@ -77,14 +77,18 @@ def get_badge_class( and it will 'do the right thing'. It should be the exception, rather than the common case, that a badge class would need to be looked up without also being created were it missing. """ - slug = slug.lower() - issuing_component = issuing_component.lower() if course_id and not modulestore().get_course(course_id).issue_badges: raise CourseBadgesDisabledError("This course does not have badges enabled.") if not course_id: course_id = CourseKeyField.Empty try: - return cls.objects.get(slug=slug, issuing_component=issuing_component, course_id=course_id) + if slug: + return cls.objects.get(slug=slug) + else: + if mode: + return cls.objects.get(mode=mode, course_id=course_id) + else: # allow setting a BadgeClass with no mode, can be used for all modes + return cls.objects.get(course_id=course_id) except cls.DoesNotExist: if not create: return None @@ -123,17 +127,15 @@ def award(self, user, evidence_url=None): """ return self.backend.award(self, user, evidence_url=evidence_url) - def save(self, **kwargs): - """ - Slugs must always be lowercase. - """ - self.slug = self.slug and self.slug.lower() - self.issuing_component = self.issuing_component and self.issuing_component.lower() - super(BadgeClass, self).save(**kwargs) + # def save(self, **kwargs): + # #""" + # # Slugs must always be lowercase. + # #""" + # super(BadgeClass, self).save(**kwargs) class Meta(object): app_label = "badges" - unique_together = (('slug', 'issuing_component', 'course_id'),) + unique_together = (('mode', 'course_id'),) verbose_name_plural = "Badge Classes"