Commit 61c76771 by Jonathan Piacenti

Add completion and enrollment badges.

parent 25958feb
......@@ -22,6 +22,7 @@ from urllib import urlencode
import uuid
import analytics
from config_models.models import ConfigurationModel
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
......@@ -1212,6 +1213,10 @@ class CourseEnrollment(models.Model):
# User is allowed to enroll if they've reached this point.
enrollment = cls.get_or_create_enrollment(user, course_key)
enrollment.update_enrollment(is_active=True, mode=mode)
if settings.FEATURES.get("ENABLE_OPENBADGES"):
from lms.djangoapps.badges.events.course_meta import award_enrollment_badge
award_enrollment_badge(user)
return enrollment
@classmethod
......
......@@ -2,6 +2,9 @@
Admin registration for Badge Models
"""
from django.contrib import admin
from badges.models import CourseCompleteImageConfiguration
from badges.models import CourseCompleteImageConfiguration, CourseEventBadgesConfiguration, BadgeClass
from config_models.admin import ConfigurationModelAdmin
admin.site.register(CourseCompleteImageConfiguration)
admin.site.register(BadgeClass)
admin.site.register(CourseEventBadgesConfiguration, ConfigurationModelAdmin)
......@@ -2,17 +2,23 @@
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.utils import site_prefix
from badges.models import CourseCompleteImageConfiguration, BadgeClass, BadgeAssertion
from badges.utils import site_prefix, requires_badges_enabled
from xmodule.modulestore.django import modulestore
LOGGER = logging.getLogger(__name__)
# NOTE: As these functions are carry-overs from the initial badging implementation, they are used in
# 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
......@@ -61,3 +67,49 @@ def criteria(course_key):
"""
about_path = reverse('about_course', kwargs={'course_id': unicode(course_key)})
return u'{}{}'.format(site_prefix(), about_path)
def get_completion_badge(course_id, user):
"""
Given a course key and a user, find the user's enrollment mode
and get the Course Completion badge.
"""
from student.models import CourseEnrollment
badge_classes = CourseEnrollment.objects.filter(
user=user, course_id=course_id
).order_by('-is_active')
if not badge_classes:
return None
mode = badge_classes[0].mode
course = modulestore().get_course(course_id)
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,
mode=mode,
display_name=course.display_name,
image_file_handle=CourseCompleteImageConfiguration.image_for_mode(mode)
)
@requires_badges_enabled
def course_badge_check(user, course_key):
"""
Takes a GeneratedCertificate instance, and checks to see if a badge exists for this course, creating
it if not, should conditions be right.
"""
if not modulestore().get_course(course_key).issue_badges:
LOGGER.info("Course is not configured to issue badges.")
return
badge_class = get_completion_badge(course_key, user)
if not badge_class:
# We're not configured to make a badge for this course mode.
return
if BadgeAssertion.objects.filter(user=user, badge_class=badge_class):
LOGGER.info("Completion badge already exists for this user on this course.")
# Badge already exists. Skip.
return
evidence = evidence_url(user.id, course_key)
badge_class.award(user, evidence_url=evidence)
"""
Events which have to do with a user doing something with more than one course, such
as enrolling in a certain number, completing a certain number, or completing a specific set of courses.
"""
from badges.models import CourseEventBadgesConfiguration, BadgeClass
from badges.utils import requires_badges_enabled
def award_badge(config, count, user):
"""
Given one of the configurations for enrollments or completions, award
the appropriate badge if one is configured.
config is a dictionary with integer keys and course keys as values.
count is the key to retrieve from this dictionary.
user is the user to award the badge to.
"""
slug = config.get(count)
if not slug:
return
badge_class = BadgeClass.get_badge_class(
slug=slug, issuing_component='edx__course', create=False,
)
if not badge_class:
return
if not badge_class.get_for_user(user):
badge_class.award(user)
def award_enrollment_badge(user):
"""
Awards badges based on the number of courses a user is enrolled in.
"""
config = CourseEventBadgesConfiguration.current().enrolled_settings
enrollments = user.courseenrollment_set.filter(is_active=True).count()
award_badge(config, enrollments, user)
@requires_badges_enabled
def completion_check(user):
"""
Awards badges based upon the number of courses a user has 'completed'.
Courses are never truly complete, but they can be closed.
For this reason we use checks on certificates to find out if a user has
completed courses. This badge will not work if certificate generation isn't
enabled and run.
"""
from certificates.models import CertificateStatuses
config = CourseEventBadgesConfiguration.current().completed_settings
certificates = user.generatedcertificate_set.filter(status__in=CertificateStatuses.PASSED_STATUSES).count()
award_badge(config, certificates, user)
@requires_badges_enabled
def course_group_check(user, course_key):
"""
Awards a badge if a user has completed every course in a defined set.
"""
from certificates.models import CertificateStatuses
config = CourseEventBadgesConfiguration.current().course_group_settings
awards = []
for slug, keys in config.items():
if course_key in keys:
certs = user.generatedcertificate_set.filter(
status__in=CertificateStatuses.PASSED_STATUSES,
course_id__in=keys,
)
if len(certs) == len(keys):
awards.append(slug)
for slug in awards:
badge_class = BadgeClass.get_badge_class(
slug=slug, issuing_component='edx__course', create=False,
)
if badge_class and not badge_class.get_for_user(user):
badge_class.award(user)
"""
Tests the course meta badging events
"""
from django.test.utils import override_settings
from mock import patch
from django.conf import settings
from badges.backends.base import BadgeBackend
from badges.tests.factories import RandomBadgeClassFactory, CourseEventBadgesConfigurationFactory, BadgeAssertionFactory
from certificates.models import GeneratedCertificate, CertificateStatuses
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
class DummyBackend(BadgeBackend):
"""
Dummy backend that creates assertions without contacting any real-world backend.
"""
def award(self, badge_class, user, evidence_url=None):
return BadgeAssertionFactory(badge_class=badge_class, user=user)
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.events.tests.test_course_meta.DummyBackend')
class CourseEnrollmentBadgeTest(ModuleStoreTestCase):
"""
Tests the event which awards badges based on number of courses a user is enrolled in.
"""
def setUp(self):
super(CourseEnrollmentBadgeTest, self).setUp()
self.badge_classes = [
RandomBadgeClassFactory(
issuing_component='edx__course'
),
RandomBadgeClassFactory(
issuing_component='edx__course'
),
RandomBadgeClassFactory(
issuing_component='edx__course'
),
]
nums = ['3', '5', '8']
entries = [','.join(pair) for pair in zip(nums, [badge.slug for badge in self.badge_classes])]
enrollment_config = '\r'.join(entries)
self.config = CourseEventBadgesConfigurationFactory(courses_enrolled=enrollment_config)
def test_no_match(self):
"""
Make sure a badge isn't created before a user's reached any checkpoint.
"""
user = UserFactory()
course = CourseFactory()
# pylint: disable=no-member
CourseEnrollment.enroll(user, course_key=course.location.course_key)
self.assertFalse(user.badgeassertion_set.all())
def test_checkpoint_matches(self):
"""
Make sure the proper badges are awarded at the right checkpoints.
"""
user = UserFactory()
courses = [CourseFactory() for _i in range(3)]
for course in courses:
CourseEnrollment.enroll(user, course_key=course.location.course_key)
# pylint: disable=no-member
assertions = user.badgeassertion_set.all()
self.assertEqual(user.badgeassertion_set.all().count(), 1)
self.assertEqual(assertions[0].badge_class, self.badge_classes[0])
courses = [CourseFactory() for _i in range(2)]
for course in courses:
# pylint: disable=no-member
CourseEnrollment.enroll(user, course_key=course.location.course_key)
# pylint: disable=no-member
assertions = user.badgeassertion_set.all().order_by('id')
# pylint: disable=no-member
self.assertEqual(user.badgeassertion_set.all().count(), 2)
self.assertEqual(assertions[1].badge_class, self.badge_classes[1])
courses = [CourseFactory() for _i in range(3)]
for course in courses:
# pylint: disable=no-member
CourseEnrollment.enroll(user, course_key=course.location.course_key)
# pylint: disable=no-member
assertions = user.badgeassertion_set.all().order_by('id')
# pylint: disable=no-member
self.assertEqual(user.badgeassertion_set.all().count(), 3)
self.assertEqual(assertions[2].badge_class, self.badge_classes[2])
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.events.tests.test_course_meta.DummyBackend')
class CourseCompletionBadgeTest(ModuleStoreTestCase):
"""
Tests the event which awards badges based on the number of courses completed.
"""
def setUp(self, **kwargs):
super(CourseCompletionBadgeTest, self).setUp()
self.badge_classes = [
RandomBadgeClassFactory(
issuing_component='edx__course'
),
RandomBadgeClassFactory(
issuing_component='edx__course'
),
RandomBadgeClassFactory(
issuing_component='edx__course'
),
]
nums = ['2', '6', '9']
entries = [','.join(pair) for pair in zip(nums, [badge.slug for badge in self.badge_classes])]
completed_config = '\r'.join(entries)
self.config = CourseEventBadgesConfigurationFactory.create(courses_completed=completed_config)
self.config.clean_fields()
def test_no_match(self):
"""
Make sure a badge isn't created before a user's reached any checkpoint.
"""
user = UserFactory()
course = CourseFactory()
GeneratedCertificate(
# pylint: disable=no-member
user=user, course_id=course.location.course_key, status=CertificateStatuses.downloadable
).save()
# pylint: disable=no-member
self.assertFalse(user.badgeassertion_set.all())
def test_checkpoint_matches(self):
"""
Make sure the proper badges are awarded at the right checkpoints.
"""
user = UserFactory()
courses = [CourseFactory() for _i in range(2)]
for course in courses:
GeneratedCertificate(
# pylint: disable=no-member
user=user, course_id=course.location.course_key, status=CertificateStatuses.downloadable
).save()
# pylint: disable=no-member
assertions = user.badgeassertion_set.all()
# pylint: disable=no-member
self.assertEqual(user.badgeassertion_set.all().count(), 1)
self.assertEqual(assertions[0].badge_class, self.badge_classes[0])
courses = [CourseFactory() for _i in range(6)]
for course in courses:
GeneratedCertificate(
user=user, course_id=course.location.course_key, status=CertificateStatuses.downloadable
).save()
# pylint: disable=no-member
assertions = user.badgeassertion_set.all().order_by('id')
self.assertEqual(user.badgeassertion_set.all().count(), 2)
self.assertEqual(assertions[1].badge_class, self.badge_classes[1])
courses = [CourseFactory() for _i in range(9)]
for course in courses:
GeneratedCertificate(
user=user, course_id=course.location.course_key, status=CertificateStatuses.downloadable
).save()
# pylint: disable=no-member
assertions = user.badgeassertion_set.all().order_by('id')
# pylint: disable=no-member
self.assertEqual(user.badgeassertion_set.all().count(), 3)
self.assertEqual(assertions[2].badge_class, self.badge_classes[2])
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.events.tests.test_course_meta.DummyBackend')
class CourseGroupBadgeTest(ModuleStoreTestCase):
"""
Tests the event which awards badges when a user completes a set of courses.
"""
def setUp(self):
super(CourseGroupBadgeTest, self).setUp()
self.badge_classes = [
RandomBadgeClassFactory(
issuing_component='edx__course'
),
RandomBadgeClassFactory(
issuing_component='edx__course'
),
RandomBadgeClassFactory(
issuing_component='edx__course'
),
]
self.courses = []
for _badge_class in self.badge_classes:
# pylint: disable=no-member
self.courses.append([CourseFactory().location.course_key for _i in range(3)])
lines = [badge_class.slug + ',' + ','.join([unicode(course_key) for course_key in keys])
for badge_class, keys in zip(self.badge_classes, self.courses)]
config = '\r'.join(lines)
self.config = CourseEventBadgesConfigurationFactory(course_groups=config)
self.config_map = dict(zip(self.badge_classes, self.courses))
def test_no_match(self):
"""
Make sure a badge isn't created before a user's completed any course groups.
"""
user = UserFactory()
course = CourseFactory()
GeneratedCertificate(
# pylint: disable=no-member
user=user, course_id=course.location.course_key, status=CertificateStatuses.downloadable
).save()
# pylint: disable=no-member
self.assertFalse(user.badgeassertion_set.all())
def test_group_matches(self):
"""
Make sure the proper badges are awarded when groups are completed.
"""
user = UserFactory()
items = list(self.config_map.items())
for badge_class, course_keys in items:
for i, key in enumerate(course_keys):
GeneratedCertificate(
user=user, course_id=key, status=CertificateStatuses.downloadable
).save()
# We don't award badges until all three are set.
if i + 1 == len(course_keys):
self.assertTrue(badge_class.get_for_user(user))
else:
self.assertFalse(badge_class.get_for_user(user))
# pylint: disable=no-member
classes = [badge.badge_class.id for badge in user.badgeassertion_set.all()]
source_classes = [badge.id for badge in self.badge_classes]
self.assertEqual(classes, source_classes)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('badges', '0002_data__migrate_assertions'),
]
operations = [
migrations.CreateModel(
name='CourseEventBadgesConfiguration',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('courses_completed', models.TextField(default=b'', help_text="On each line, put the number of completed courses to award a badge for, a comma, and the slug of a badge class you have created with the issuing component 'edx__course'. For example: 3,course-v1:edx/Demo/DemoX", blank=True)),
('courses_enrolled', models.TextField(default=b'', help_text="On each line, put the number of enrolled courses to award a badge for, a comma, and the slug of a badge class you have created with the issuing component 'edx__course'. For example: 3,course-v1:edx/Demo/DemoX", blank=True)),
('course_groups', models.TextField(default=b'', help_text="On each line, put the slug of a badge class you have created with the issuing component 'edx__course' to award, a comma, and a comma separated list of course keys that the user will need to complete to get this badge. For example: slug_for_compsci_courses_group_badge,course-v1:CompSci+Course+First,course-v1:CompsSci+Course+Second", blank=True)),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
),
migrations.AlterModelOptions(
name='badgeclass',
options={'verbose_name_plural': 'Badge Classes'},
),
]
......@@ -9,7 +9,11 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _
from lazy import lazy
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from config_models.models import ConfigurationModel
from xmodule.modulestore.django import modulestore
from xmodule_django.models import CourseKeyField
from jsonfield import JSONField
......@@ -53,7 +57,7 @@ class BadgeClass(models.Model):
@classmethod
def get_badge_class(
cls, slug, issuing_component, display_name, description, criteria, image_file_handle,
cls, slug, issuing_component, display_name=None, description=None, criteria=None, image_file_handle=None,
mode='', course_id=None, create=True
):
"""
......@@ -115,6 +119,7 @@ class BadgeClass(models.Model):
class Meta(object):
app_label = "badges"
unique_together = (('slug', 'issuing_component', 'course_id'),)
verbose_name_plural = "Badge Classes"
class BadgeAssertion(models.Model):
......@@ -199,3 +204,112 @@ class CourseCompleteImageConfiguration(models.Model):
class Meta(object):
app_label = "badges"
class CourseEventBadgesConfiguration(ConfigurationModel):
"""
Determines the settings for meta course awards-- such as completing a certain
number of courses or enrolling in a certain number of them.
"""
courses_completed = models.TextField(
blank=True, default='',
help_text=_(
u"On each line, put the number of completed courses to award a badge for, a comma, and the slug of a "
u"badge class you have created with the issuing component 'edx__course'. "
u"For example: 3,course-v1:edx/Demo/DemoX"
)
)
courses_enrolled = models.TextField(
blank=True, default='',
help_text=_(
u"On each line, put the number of enrolled courses to award a badge for, a comma, and the slug of a "
u"badge class you have created with the issuing component 'edx__course'. "
u"For example: 3,course-v1:edx/Demo/DemoX"
)
)
course_groups = models.TextField(
blank=True, default='',
help_text=_(
u"Each line is a comma-separated list. The first item in each line is the slug of a badge class to award, "
u"with an issuing component of 'edx__course'. The remaining items in each line are the course keys the "
u"user will need to complete to get the badge. For example: slug_for_compsci_courses_group_badge,course-v1"
u":CompSci+Course+First,course-v1:CompsSci+Course+Second"
)
)
def __unicode__(self):
return u"<CourseEventBadgesConfiguration ({})>".format(u"Enabled" if self.enabled else u"Disabled")
@staticmethod
def get_specs(text):
"""
Takes a string in the format of:
int,course_key
int,course_key
And returns a dictionary with the keys as the numbers and the values as the course keys.
"""
specs = text.splitlines()
specs = [line.split(',') for line in specs if line.strip()]
return {int(num): slug.strip().lower() for num, slug in specs}
@property
def completed_settings(self):
"""
Parses the settings from the courses_completed field.
"""
return self.get_specs(self.courses_completed)
@property
def enrolled_settings(self):
"""
Parses the settings from the courses_completed field.
"""
return self.get_specs(self.courses_enrolled)
@property
def course_group_settings(self):
"""
Parses the course group settings. In example, the format is:
slug_for_compsci_courses_group_badge,course-v1:CompSci+Course+First,course-v1:CompsSci+Course+Second
"""
specs = self.course_groups.strip()
if not specs:
return {}
specs = [line.split(',', 1) for line in specs.splitlines()]
return {
slug.strip().lower(): [CourseKey.from_string(key.strip()) for key in keys.strip().split(',')]
for slug, keys in specs
}
def clean_fields(self, exclude=tuple()):
"""
Verify the settings are parseable.
"""
errors = {}
error_message = _(u"Please check the syntax of your entry.")
if 'courses_completed' not in exclude:
try:
self.completed_settings
except (ValueError, InvalidKeyError):
errors['courses_completed'] = [unicode(error_message)]
if 'courses_enrolled' not in exclude:
try:
self.enrolled_settings
except (ValueError, InvalidKeyError):
errors['courses_enrolled'] = [unicode(error_message)]
if 'course_groups' not in exclude:
store = modulestore()
try:
for key_list in self.course_group_settings.values():
for course_key in key_list:
if not store.get_course(course_key):
ValueError(u"The course {course_key} does not exist.".format(course_key=course_key))
except (ValueError, InvalidKeyError):
errors['course_groups'] = [unicode(error_message)]
if errors:
raise ValidationError(errors)
class Meta(object):
app_label = "badges"
......@@ -8,7 +8,7 @@ from django.core.files.base import ContentFile
from factory import DjangoModelFactory
from factory.django import ImageField
from badges.models import BadgeAssertion, CourseCompleteImageConfiguration, BadgeClass
from badges.models import BadgeAssertion, CourseCompleteImageConfiguration, BadgeClass, CourseEventBadgesConfiguration
from student.tests.factories import UserFactory
......@@ -69,3 +69,13 @@ class BadgeAssertionFactory(DjangoModelFactory):
data = {}
assertion_url = 'http://example.com/example.json'
image_url = 'http://example.com/image.png'
class CourseEventBadgesConfigurationFactory(DjangoModelFactory):
"""
Factory for CourseEventsBadgesConfiguration
"""
class Meta(object):
model = CourseEventBadgesConfiguration
enabled = True
......@@ -3,9 +3,10 @@ Tests for the Badges app models.
"""
from django.core.exceptions import ValidationError
from django.core.files.images import ImageFile
from django.db.utils import IntegrityError
from django.test import TestCase
from django.test.utils import override_settings
from mock import patch
from mock import patch, Mock
from nose.plugins.attrib import attr
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -54,6 +55,7 @@ class DummyBackend(object):
"""
Dummy badge backend, used for testing.
"""
award = Mock()
class BadgeClassTest(ModuleStoreTestCase):
......@@ -126,9 +128,7 @@ class BadgeClassTest(ModuleStoreTestCase):
Test returns None if the badge class does not exist.
"""
badge_class = BadgeClass.get_badge_class(
slug='new_slug', issuing_component='new_component', description=None,
criteria=None, display_name=None,
image_file_handle=None, create=False
slug='new_slug', issuing_component='new_component', create=False
)
self.assertIsNone(badge_class)
# Run this twice to verify there wasn't a background creation of the badge.
......@@ -139,7 +139,7 @@ class BadgeClassTest(ModuleStoreTestCase):
)
self.assertIsNone(badge_class)
def test_get_badge_class_validate(self):
def test_get_badge_class_image_validate(self):
"""
Verify handing a broken image to get_badge_class raises a validation error upon creation.
"""
......@@ -151,6 +151,17 @@ class BadgeClassTest(ModuleStoreTestCase):
image_file_handle=get_image('unbalanced')
)
def test_get_badge_class_data_validate(self):
"""
Verify handing incomplete data for required fields when making a badge class raises an Integrity error.
"""
self.assertRaises(
IntegrityError,
BadgeClass.get_badge_class,
slug='new_slug', issuing_component='new_component',
image_file_handle=get_image('good')
)
def test_get_for_user(self):
"""
Make sure we can get an assertion for a user if there is one.
......
......@@ -10,3 +10,17 @@ def site_prefix():
"""
scheme = u"https" if settings.HTTPS == "on" else u"http"
return u'{}://{}'.format(scheme, settings.SITE_NAME)
def requires_badges_enabled(function):
"""
Decorator that bails a function out early if badges aren't enabled.
"""
def wrapped(*args, **kwargs):
"""
Wrapped function which bails out early if bagdes aren't enabled.
"""
if not settings.FEATURES.get('ENABLE_OPENBADGES', False):
return
return function(*args, **kwargs)
return wrapped
......@@ -9,7 +9,7 @@ from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from certificates.models import get_completion_badge
from badges.events.course_complete import get_completion_badge
from xmodule.modulestore.django import modulestore
from certificates.api import regenerate_user_certificates
......
......@@ -64,12 +64,11 @@ from model_utils.models import TimeStampedModel
from openedx.core.djangoapps.signals.signals import COURSE_CERT_AWARDED
from badges.events import course_complete
from badges.models import BadgeAssertion, CourseCompleteImageConfiguration, BadgeClass
from badges.events.course_complete import course_badge_check
from badges.events.course_meta import completion_check, course_group_check
from config_models.models import ConfigurationModel
from instructor_task.models import InstructorTask
from util.milestones_helpers import fulfill_course_milestone, is_prerequisite_courses_enabled
from xmodule.modulestore.django import modulestore
from xmodule_django.models import CourseKeyField, NoneToEmptyManager
LOGGER = logging.getLogger(__name__)
......@@ -1012,47 +1011,26 @@ class CertificateTemplateAsset(TimeStampedModel):
app_label = "certificates"
def get_completion_badge(course_id, user):
@receiver(COURSE_CERT_AWARDED, sender=GeneratedCertificate)
# pylint: disable=unused-argument
def create_course_badge(sender, user, course_key, status, **kwargs):
"""
Given a course key and a user, find the user's enrollment mode
and get the Course Completion badge.
Standard signal hook to create course badges when a certificate has been generated.
"""
from student.models import CourseEnrollment
mode = CourseEnrollment.objects.filter(
user=user, course_id=course_id
).order_by('-is_active')[0].mode
course = modulestore().get_course(course_id)
return BadgeClass.get_badge_class(
slug=course_complete.course_slug(course_id, mode),
issuing_component='',
criteria=course_complete.criteria(course_id),
description=course_complete.badge_description(course, mode),
course_id=course_id,
mode=mode,
display_name=course.display_name,
image_file_handle=CourseCompleteImageConfiguration.image_for_mode(mode)
)
course_badge_check(user, course_key)
@receiver(COURSE_CERT_AWARDED, sender=GeneratedCertificate)
def create_completion_badge(sender, user, course_key, status, **kwargs): # pylint: disable=unused-argument
"""
Standard signal hook to create 'x courses completed' badges when a certificate has been generated.
"""
completion_check(user)
@receiver(COURSE_CERT_AWARDED, sender=GeneratedCertificate)
#pylint: disable=unused-argument
def create_badge(sender, user, course_key, status, **kwargs):
def create_course_group_badge(sender, user, course_key, status, **kwargs): # pylint: disable=unused-argument
"""
Standard signal hook to create badges when a certificate has been generated.
Standard signal hook to create badges when a user has completed a prespecified set of courses.
"""
if not settings.FEATURES.get('ENABLE_OPENBADGES', False):
return
if not modulestore().get_course(course_key).issue_badges:
LOGGER.info("Course is not configured to issue badges.")
return
badge_class = get_completion_badge(course_key, user)
if BadgeAssertion.objects.filter(user=user, badge_class=badge_class):
LOGGER.info("Completion badge already exists for this user on this course.")
# Badge already exists. Skip.
return
# Don't bake a badge until the certificate is available. Prevents user-facing requests from being paused for this
# by making sure it only gets run on the callback during normal workflow.
if not status == CertificateStatuses.downloadable:
return
evidence = course_complete.evidence_url(user.id, course_key)
badge_class.award(user, evidence_url=evidence)
course_group_check(user, course_key)
......@@ -9,13 +9,14 @@ from mock import patch
from course_modes.models import CourseMode
from opaque_keys.edx.locator import CourseLocator
from badges.events.course_complete import get_completion_badge
from badges.models import BadgeAssertion
from badges.tests.factories import BadgeAssertionFactory, CourseCompleteImageConfigurationFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from certificates.management.commands import resubmit_error_certificates, regenerate_user, ungenerated_certs
from certificates.models import GeneratedCertificate, CertificateStatuses, get_completion_badge
from certificates.models import GeneratedCertificate, CertificateStatuses
class CertificateManagementTest(ModuleStoreTestCase):
......
......@@ -14,7 +14,11 @@ from django.core.urlresolvers import reverse
from django.test.client import Client
from django.test.utils import override_settings
<<<<<<< HEAD
from course_modes.models import CourseMode
=======
from badges.events.course_complete import get_completion_badge
>>>>>>> a248c5a... Add completion and enrollment badges.
from badges.tests.factories import BadgeAssertionFactory, CourseCompleteImageConfigurationFactory
from openedx.core.lib.tests.assertions.events import assert_event_matches
from student.tests.factories import UserFactory, CourseEnrollmentFactory
......@@ -31,7 +35,6 @@ from certificates.models import (
CertificateTemplate,
CertificateHtmlViewConfiguration,
CertificateTemplateAsset,
get_completion_badge
)
from certificates.tests.factories import (
......
......@@ -14,6 +14,7 @@ from django.template import RequestContext
from django.utils.translation import ugettext as _
from django.utils.encoding import smart_str
from badges.events.course_complete import get_completion_badge
from courseware.access import has_access
from edxmako.shortcuts import render_to_response
from edxmako.template import Template
......@@ -41,8 +42,7 @@ from certificates.models import (
GeneratedCertificate,
CertificateStatuses,
CertificateHtmlViewConfiguration,
CertificateSocialNetworks,
get_completion_badge)
CertificateSocialNetworks)
log = logging.getLogger(__name__)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment