badge_handler.py 7.24 KB
Newer Older
1 2 3 4 5 6
"""
BadgeHandler object-- used to award Badges to users who have completed courses.
"""
import hashlib
import logging
import mimetypes
7 8
from eventtracking import tracker
import requests
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
from django.template.defaultfilters import slugify
from django.utils.translation import ugettext as _

from django.conf import settings
from django.core.urlresolvers import reverse
from lazy import lazy
from requests.packages.urllib3.exceptions import HTTPError
from certificates.models import BadgeAssertion, BadgeImageConfiguration
from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore

LOGGER = logging.getLogger(__name__)


class BadgeHandler(object):
    """
    The only properly public method of this class is 'award'. If an alternative object is created for a different
    badging service, the other methods don't need to be reproduced.
    """
    # Global caching dict
    badges = {}

    def __init__(self, course_key):
        self.course_key = course_key
        assert settings.BADGR_API_TOKEN

    @lazy
    def base_url(self):
        """
        Base URL for all API requests.
        """
        return "{}/v1/issuer/issuers/{}".format(settings.BADGR_BASE_URL, settings.BADGR_ISSUER_SLUG)

    @lazy
    def badge_create_url(self):
        """
        URL for generating a new Badge specification
        """
        return "{}/badges".format(self.base_url)

    def badge_url(self, mode):
        """
        Get the URL for a course's badge in a given mode.
        """
        return "{}/{}".format(self.badge_create_url, self.course_slug(mode))

    def assertion_url(self, mode):
        """
        URL for generating a new assertion.
        """
        return "{}/assertions".format(self.badge_url(mode))

    def course_slug(self, mode):
        """
        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(self.course_key), unicode(mode))).hexdigest()[:7]
        base_slug = slugify(unicode(self.course_key) + u'_{}_'.format(mode))[:248]
        return base_slug + digest

    def log_if_raised(self, response, data):
        """
        Log server response if there was an error.
        """
        try:
            response.raise_for_status()
        except HTTPError:
            LOGGER.error(
                u"Encountered an error when contacting the Badgr-Server. Request sent to %s with headers %s.\n"
                u"and data values %s\n"
                u"Response status was %s.\n%s",
                repr(response.request.url), repr(response.request.headers),
                repr(data),
                response.status_code, response.body
            )
            raise

    def get_headers(self):
        """
        Headers to send along with the request-- used for authentication.
        """
        return {'Authorization': 'Token {}'.format(settings.BADGR_API_TOKEN)}

    def ensure_badge_created(self, mode):
        """
        Verify a badge has been created for this mode of the course, and, if not, create it
        """
        if self.course_slug(mode) in BadgeHandler.badges:
            return
        response = requests.get(self.badge_url(mode), headers=self.get_headers())
        if response.status_code != 200:
            self.create_badge(mode)
        BadgeHandler.badges[self.course_slug(mode)] = True

    @staticmethod
    def badge_description(course, mode):
        """
        Returns a description for the earned badge.
        """
        if course.end:
            return _(u'Completed the course "{course_name}" ({course_mode}, {start_date} - {end_date})').format(
                start_date=course.start.date(),
                end_date=course.end.date(),
                course_name=course.display_name,
                course_mode=mode,
            )
        else:
            return _(u'Completed the course "{course_name}" ({course_mode})').format(
120
                course_name=course.display_name,
121 122 123
                course_mode=mode,
            )

124 125 126 127 128 129 130
    def site_prefix(self):
        """
        Get the prefix for the site URL-- protocol and server name.
        """
        scheme = u"https" if settings.HTTPS == "on" else u"http"
        return u'{}://{}'.format(scheme, settings.SITE_NAME)

131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
    def create_badge(self, mode):
        """
        Create the badge spec for a course's mode.
        """
        course = modulestore().get_course(self.course_key)
        image = BadgeImageConfiguration.image_for_mode(mode)
        # We don't want to bother validating the file any further than making sure we can detect its MIME type,
        # for HTTP. The Badgr-Server should tell us if there's anything in particular wrong with it.
        content_type, __ = mimetypes.guess_type(image.name)
        if not content_type:
            raise ValueError(
                "Could not determine content-type of image! Make sure it is a properly named .png file."
            )
        files = {'image': (image.name, image, content_type)}
        about_path = reverse('about_course', kwargs={'course_id': unicode(self.course_key)})
        data = {
            'name': course.display_name,
148
            'criteria': u'{}{}'.format(self.site_prefix(), about_path),
149 150 151 152 153 154
            'slug': self.course_slug(mode),
            'description': self.badge_description(course, mode)
        }
        result = requests.post(self.badge_create_url, headers=self.get_headers(), data=data, files=files)
        self.log_if_raised(result, data)

155 156 157 158 159
    def send_assertion_created_event(self, user, assertion):
        """
        Send an analytics event to record the creation of a badge assertion.
        """
        tracker.emit(
160
            'edx.badge.assertion.created', {
161 162 163 164 165 166 167 168 169 170
                'user_id': user.id,
                'course_id': unicode(self.course_key),
                'enrollment_mode': assertion.mode,
                'assertion_id': assertion.id,
                'assertion_image_url': assertion.data['image'],
                'assertion_json_url': assertion.data['json']['id'],
                'issuer': assertion.data['issuer'],
            }
        )

171 172 173 174 175
    def create_assertion(self, user, mode):
        """
        Register an assertion with the Badgr server for a particular user in a particular course mode for
        this course.
        """
176 177 178
        data = {
            'email': user.email,
            'evidence': self.site_prefix() + reverse(
179
                'certificates:html_view', kwargs={'user_id': user.id, 'course_id': unicode(self.course_key)}
180 181
            ) + '?evidence_visit=1'
        }
182 183
        response = requests.post(self.assertion_url(mode), headers=self.get_headers(), data=data)
        self.log_if_raised(response, data)
184
        assertion, __ = BadgeAssertion.objects.get_or_create(course_id=self.course_key, user=user, mode=mode)
185 186
        assertion.data = response.json()
        assertion.save()
187
        self.send_assertion_created_event(user, assertion)
188 189 190 191 192 193 194 195

    def award(self, user):
        """
        Award a user a badge for their work on the course.
        """
        mode = CourseEnrollment.objects.get(user=user, course_id=self.course_key).mode
        self.ensure_badge_created(mode)
        self.create_assertion(user, mode)