diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 765a75b3a0..cf5fa7bac0 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -46,6 +46,7 @@ from simple_history.models import HistoricalRecords from track import contexts from xmodule_django.models import CourseKeyField, NoneToEmptyManager +from lms.djangoapps.badges.utils import badges_enabled from certificates.models import GeneratedCertificate from course_modes.models import CourseMode from enrollment.api import _default_course_mode @@ -1213,7 +1214,7 @@ 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"): + if badges_enabled(): from lms.djangoapps.badges.events.course_meta import award_enrollment_badge award_enrollment_badge(user) diff --git a/common/static/common/js/spec_helpers/ajax_helpers.js b/common/static/common/js/spec_helpers/ajax_helpers.js index 5cf3f0cb1b..056328e07e 100644 --- a/common/static/common/js/spec_helpers/ajax_helpers.js +++ b/common/static/common/js/spec_helpers/ajax_helpers.js @@ -73,7 +73,8 @@ define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) { expect(request.url).toEqual(url); expect(request.method).toEqual(method); if (typeof body === 'undefined') { - // The contents if this call may not be germane to the current test. + // The body of the request may not be germane to the current test-- like some call by a library, + // so allow it to be ignored. return; } expect(request.requestBody).toEqual(body); diff --git a/common/test/acceptance/pages/lms/learner_profile.py b/common/test/acceptance/pages/lms/learner_profile.py index 2b22bcbd3e..16177cfefb 100644 --- a/common/test/acceptance/pages/lms/learner_profile.py +++ b/common/test/acceptance/pages/lms/learner_profile.py @@ -1,6 +1,8 @@ """ Bok-Choy PageObject class for learner profile page. """ +from bok_choy.query import BrowserQuery + from . import BASE_URL from bok_choy.page_object import PageObject from .fields import FieldsMixin @@ -16,6 +18,35 @@ FIELD_ICONS = { } +class Badge(PageObject): + """ + Represents a single badge displayed on the learner profile page. + """ + url = None + + def __init__(self, element, browser): + self.full_view = browser + # Element API is similar to browser API, should allow subqueries. + super(Badge, self).__init__(element) + + def is_browser_on_page(self): + return self.q(css=".badge-details").visible + + def modal_displayed(self): + """ + Verifies that the share modal is diplayed. + """ + # The modal is on the page at large, and not a subelement of the badge div. + return BrowserQuery(self.full_view, css=".badges-modal").visible + + def display_modal(self): + """ + Click the share button to display the sharing modal for the badge. + """ + self.q(css=".share-button").click() + EmptyPromise(self.modal_displayed, "Share modal displayed").fulfill() + + class LearnerProfilePage(FieldsMixin, PageObject): """ PageObject methods for Learning Profile Page. @@ -58,6 +89,27 @@ class LearnerProfilePage(FieldsMixin, PageObject): """ return 'all_users' if self.q(css=PROFILE_VISIBILITY_SELECTOR.format('all_users')).selected else 'private' + def accomplishments_available(self): + """ + Verify that the accomplishments tab is available. + """ + return self.q(css="button[data-url='accomplishments']").visible + + def display_accomplishments(self): + """ + Click the accomplishments tab and wait for the accomplishments to load. + """ + EmptyPromise(self.accomplishments_available, "Accomplishments tab is displayed").fulfill() + self.q(css="button[data-url='accomplishments']").click() + self.wait_for_element_visibility(".badge-list", "Badge list displayed") + + @property + def badges(self): + """ + Get all currently listed badges. + """ + return [Badge(element, self.browser) for element in self.q(css=".badge-display:not(.badge-placeholder)")] + @privacy.setter def privacy(self, privacy): """ diff --git a/common/test/acceptance/tests/lms/test_learner_profile.py b/common/test/acceptance/tests/lms/test_learner_profile.py index 01d0159c99..6c69662f7f 100644 --- a/common/test/acceptance/tests/lms/test_learner_profile.py +++ b/common/test/acceptance/tests/lms/test_learner_profile.py @@ -800,3 +800,22 @@ class LearnerProfileA11yTest(LearnerProfileTestMixin, WebAppTest): }) profile_page.a11y_audit.check_for_accessibility_errors() + + def test_badges_accessibility(self): + """ + Test the accessibility of the badge listings and sharing modal. + """ + username = 'testcert' + AutoAuthPage(self.browser, username=username).visit() + profile_page = self.visit_profile_page(username) + + profile_page.a11y_audit.config.set_rules({ + "ignore": [ + 'skip-link', # TODO: AC-179 + 'link-href', # TODO: AC-231 + ], + }) + profile_page.display_accomplishments() + profile_page.a11y_audit.check_for_accessibility_errors() + profile_page.badges[0].display_modal() + profile_page.a11y_audit.check_for_accessibility_errors() diff --git a/common/test/db_fixtures/certificates_web_view.json b/common/test/db_fixtures/certificates_web_view.json index fe7bd48829..3ef1912536 100644 --- a/common/test/db_fixtures/certificates_web_view.json +++ b/common/test/db_fixtures/certificates_web_view.json @@ -110,5 +110,504 @@ "company_identifier": "7nTFLiuDkkQkdELSpruCwD4F6jzqtTFsx3PfJUIT2qHqXRLG1", "trk_partner_name": "edx" } + }, + { + "fields": { + "display_name": "Test Badge", + "description": "Yay! It's a test badge.", + "image": "badge_classes/test_Nln8nhu.png", + "issuing_component": "test_component", + "mode": "honor", + "criteria": "https://example.com/syllabus", + "course_id": null, + "slug": "test_slug_0_0768205214811" + }, + "model": "badges.badgeclass", + "pk": 1 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.401Z", + "modified": "2016-03-22T15:35:21.403Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 1 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.412Z", + "modified": "2016-03-22T15:35:21.413Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 2 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.422Z", + "modified": "2016-03-22T15:35:21.423Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 3 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.437Z", + "modified": "2016-03-22T15:35:21.437Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 4 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.444Z", + "modified": "2016-03-22T15:35:21.445Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 5 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.451Z", + "modified": "2016-03-22T15:35:21.451Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 6 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.459Z", + "modified": "2016-03-22T15:35:21.459Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 7 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.467Z", + "modified": "2016-03-22T15:35:21.467Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 8 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.475Z", + "modified": "2016-03-22T15:35:21.476Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 9 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.488Z", + "modified": "2016-03-22T15:35:21.489Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 10 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.495Z", + "modified": "2016-03-22T15:35:21.496Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 11 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.503Z", + "modified": "2016-03-22T15:35:21.504Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 12 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.511Z", + "modified": "2016-03-22T15:35:21.512Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 13 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.519Z", + "modified": "2016-03-22T15:35:21.519Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 14 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.532Z", + "modified": "2016-03-22T15:35:21.534Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 15 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.542Z", + "modified": "2016-03-22T15:35:21.544Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 16 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.551Z", + "modified": "2016-03-22T15:35:21.552Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 17 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.559Z", + "modified": "2016-03-22T15:35:21.560Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 18 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.566Z", + "modified": "2016-03-22T15:35:21.566Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 19 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.573Z", + "modified": "2016-03-22T15:35:21.574Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 20 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.592Z", + "modified": "2016-03-22T15:35:21.592Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 21 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.600Z", + "modified": "2016-03-22T15:35:21.600Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 22 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.607Z", + "modified": "2016-03-22T15:35:21.608Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 23 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.617Z", + "modified": "2016-03-22T15:35:21.618Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 24 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.624Z", + "modified": "2016-03-22T15:35:21.625Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 25 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.632Z", + "modified": "2016-03-22T15:35:21.632Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 26 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.645Z", + "modified": "2016-03-22T15:35:21.646Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 27 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.653Z", + "modified": "2016-03-22T15:35:21.653Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 28 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.659Z", + "modified": "2016-03-22T15:35:21.659Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 29 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.667Z", + "modified": "2016-03-22T15:35:21.667Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 30 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.675Z", + "modified": "2016-03-22T15:35:21.676Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 31 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.693Z", + "modified": "2016-03-22T15:35:21.695Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 32 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.704Z", + "modified": "2016-03-22T15:35:21.705Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 33 + }, + { + "fields": { + "assertion_url": "http://example.com/example.json", + "created": "2016-03-22T15:35:21.715Z", + "modified": "2016-03-22T15:35:21.716Z", + "image_url": "http://example.com/image.png", + "user": 99, + "badge_class": 1, + "data": "{}", + "backend": "" + }, + "model": "badges.badgeassertion", + "pk": 34 + }, + { + "fields": { + "default": true, + "mode": "honor", + "icon": "course_complete_badges/honor.png" + }, + "model": "badges.coursecompleteimageconfiguration", + "pk": 1 } ] diff --git a/lms/djangoapps/badges/admin.py b/lms/djangoapps/badges/admin.py index 08b0c59da3..cced329c07 100644 --- a/lms/djangoapps/badges/admin.py +++ b/lms/djangoapps/badges/admin.py @@ -7,4 +7,5 @@ from config_models.admin import ConfigurationModelAdmin admin.site.register(CourseCompleteImageConfiguration) admin.site.register(BadgeClass) +# Use the standard Configuration Model Admin handler for this model. admin.site.register(CourseEventBadgesConfiguration, ConfigurationModelAdmin) diff --git a/lms/djangoapps/badges/api/tests.py b/lms/djangoapps/badges/api/tests.py index 7b45902502..6d5bcfd842 100644 --- a/lms/djangoapps/badges/api/tests.py +++ b/lms/djangoapps/badges/api/tests.py @@ -1,6 +1,7 @@ """ Tests for the badges API views. """ +from ddt import ddt, data, unpack from django.conf import settings from django.test.utils import override_settings @@ -20,8 +21,6 @@ class UserAssertionTestCase(UrlResetMixin, ModuleStoreTestCase, ApiTestCase): """ Mixin for badge API tests. """ - WILDCARD = False - CHECK_COURSE = False def setUp(self, *args, **kwargs): super(UserAssertionTestCase, self).setUp(*args, **kwargs) @@ -55,24 +54,24 @@ class UserAssertionTestCase(UrlResetMixin, ModuleStoreTestCase, ApiTestCase): self.assertEqual(assertion.assertion_url, json_assertion['assertion_url']) self.check_class_structure(assertion.badge_class, json_assertion['badge_class']) - def get_course_id(self, badge_class): + def get_course_id(self, wildcard, badge_class): """ Used for tests which may need to test for a course_id or a wildcard. """ - if self.WILDCARD: + if wildcard: return '*' else: return unicode(badge_class.course_id) - def create_badge_class(self, **kwargs): + def create_badge_class(self, check_course, **kwargs): """ Create a badge class, using a course id if it's relevant to the URL pattern. """ - if self.CHECK_COURSE: + if check_course: return RandomBadgeClassFactory.create(course_id=self.course.location.course_key, **kwargs) return RandomBadgeClassFactory.create(**kwargs) - def get_qs_args(self, badge_class): + def get_qs_args(self, check_course, wildcard, badge_class): """ Get a dictionary to be serialized into querystring params based on class settings. """ @@ -80,8 +79,8 @@ class UserAssertionTestCase(UrlResetMixin, ModuleStoreTestCase, ApiTestCase): 'issuing_component': badge_class.issuing_component, 'slug': badge_class.slug, } - if self.CHECK_COURSE: - qs_args['course_id'] = self.get_course_id(badge_class) + if check_course: + qs_args['course_id'] = self.get_course_id(wildcard, badge_class) return qs_args @@ -100,13 +99,13 @@ class TestUserBadgeAssertions(UserAssertionTestCase): BadgeAssertionFactory(user=self.user, badge_class=BadgeClassFactory(course_id=self.course.location.course_key)) # Should not be included. for dummy in range(3): - self.create_badge_class() + self.create_badge_class(False) response = self.get_json(self.url()) # pylint: disable=no-member self.assertEqual(len(response['results']), 4) def test_assertion_structure(self): - badge_class = self.create_badge_class() + badge_class = self.create_badge_class(False) assertion = BadgeAssertionFactory.create(user=self.user, badge_class=badge_class) response = self.get_json(self.url()) # pylint: disable=no-member @@ -117,7 +116,6 @@ class TestUserCourseBadgeAssertions(UserAssertionTestCase): """ Test the Badge Assertions view with the course_id filter. """ - CHECK_COURSE = True def test_get_assertions(self): """ @@ -127,10 +125,10 @@ class TestUserCourseBadgeAssertions(UserAssertionTestCase): badge_class = BadgeClassFactory.create(course_id=course_key) for dummy in range(3): BadgeAssertionFactory.create(user=self.user, badge_class=badge_class) - # Should not be included. + # Should not be included, as they don't share the target badge class. for dummy in range(3): BadgeAssertionFactory.create(user=self.user) - # Also should not be included + # Also should not be included, as they don't share the same user. for dummy in range(6): BadgeAssertionFactory.create(badge_class=badge_class) response = self.get_json(self.url(), data={'course_id': course_key}) @@ -143,7 +141,7 @@ class TestUserCourseBadgeAssertions(UserAssertionTestCase): def test_assertion_structure(self): """ - Verify the badge assertion structure is not mangled in this mode. + Verify the badge assertion structure is as expected when a course is involved. """ course_key = self.course.location.course_key badge_class = BadgeClassFactory.create(course_id=course_key) @@ -153,16 +151,19 @@ class TestUserCourseBadgeAssertions(UserAssertionTestCase): self.check_assertion_structure(assertion, response['results'][0]) +@ddt class TestUserBadgeAssertionsByClass(UserAssertionTestCase): """ Test the Badge Assertions view with the badge class filter. """ - def test_get_assertions(self): + @unpack + @data((False, False), (True, False), (True, True)) + def test_get_assertions(self, check_course, wildcard): """ Verify we can get assertions via the badge class and username. """ - badge_class = self.create_badge_class() + badge_class = self.create_badge_class(check_course) for dummy in range(3): BadgeAssertionFactory.create(user=self.user, badge_class=badge_class) if badge_class.course_id: @@ -172,62 +173,52 @@ class TestUserBadgeAssertionsByClass(UserAssertionTestCase): course_id=CourseFactory.create().location.course_key ) BadgeAssertionFactory.create(user=self.user, badge_class=alt_class) - # Should not be in list. + # Same badge class, but different user. Should not show up in the list. for dummy in range(5): BadgeAssertionFactory.create(badge_class=badge_class) - # Also should not be in list. + # Different badge class AND different user. Certainly shouldn't show up in the list! for dummy in range(6): BadgeAssertionFactory.create() response = self.get_json( self.url(), - data=self.get_qs_args(badge_class), + data=self.get_qs_args(check_course, wildcard, badge_class), ) - if self.WILDCARD: + if wildcard: expected_length = 4 else: expected_length = 3 # pylint: disable=no-member self.assertEqual(len(response['results']), expected_length) - unused_class = self.create_badge_class(slug='unused_slug', issuing_component='unused_component') + unused_class = self.create_badge_class(check_course, slug='unused_slug', issuing_component='unused_component') response = self.get_json( self.url(), - data=self.get_qs_args(unused_class), + data=self.get_qs_args(check_course, wildcard, unused_class), ) # pylint: disable=no-member self.assertEqual(len(response['results']), 0) - def check_badge_class_assertion(self, badge_class): + def check_badge_class_assertion(self, check_course, wildcard, badge_class): """ Given a badge class, create an assertion for the current user and fetch it, checking the structure. """ assertion = BadgeAssertionFactory.create(badge_class=badge_class, user=self.user) response = self.get_json( self.url(), - data=self.get_qs_args(badge_class), + data=self.get_qs_args(check_course, wildcard, badge_class), ) # pylint: disable=no-member self.check_assertion_structure(assertion, response['results'][0]) - def test_assertion_structure(self): - self.check_badge_class_assertion(self.create_badge_class()) + @unpack + @data((False, False), (True, False), (True, True)) + def test_assertion_structure(self, check_course, wildcard): + self.check_badge_class_assertion(check_course, wildcard, self.create_badge_class(check_course)) - def test_empty_issuing_component(self): - self.check_badge_class_assertion(self.create_badge_class(issuing_component='')) - - -# pylint: disable=test-inherits-tests -class TestUserBadgeAssertionsByClassCourse(TestUserBadgeAssertionsByClass): - """ - Test searching all assertions for a user with a course bound badge class. - """ - CHECK_COURSE = True - - -# pylint: disable=test-inherits-tests -class TestUserBadgeAssertionsByClassWildCard(TestUserBadgeAssertionsByClassCourse): - """ - Test searching slugs/issuing_components across all course IDs. - """ - WILDCARD = True + @unpack + @data((False, False), (True, False), (True, True)) + def test_empty_issuing_component(self, check_course, wildcard): + self.check_badge_class_assertion( + check_course, wildcard, self.create_badge_class(check_course, issuing_component='') + ) diff --git a/lms/djangoapps/badges/api/views.py b/lms/djangoapps/badges/api/views.py index 4e99b85f69..060d11d05f 100644 --- a/lms/djangoapps/badges/api/views.py +++ b/lms/djangoapps/badges/api/views.py @@ -11,17 +11,18 @@ from openedx.core.lib.api.authentication import ( OAuth2AuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser ) -from badges.models import BadgeAssertion -from .serializers import BadgeAssertionSerializer from xmodule_django.models import CourseKeyField +from badges.models import BadgeAssertion +from .serializers import BadgeAssertionSerializer -class CourseKeyError(APIException): + +class InvalidCourseKeyError(APIException): """ Raised the course key given isn't valid. """ status_code = 400 - default_detail = "The course key provided could not be parsed." + default_detail = "The course key provided was invalid." class UserBadgeAssertions(generics.ListAPIView): @@ -118,11 +119,13 @@ class UserBadgeAssertions(generics.ListAPIView): try: course_id = CourseKey.from_string(provided_course_id) except InvalidKeyError: - raise CourseKeyError + raise InvalidCourseKeyError elif 'slug' not in self.request.query_params: # Need to get all badges for the user. course_id = None else: + # Django won't let us use 'None' for querying a ForeignKey field. We have to use this special + # 'Empty' value to indicate we're looking only for badges without a course key set. course_id = CourseKeyField.Empty if course_id is not None: diff --git a/lms/djangoapps/badges/backends/badgr.py b/lms/djangoapps/badges/backends/badgr.py index 265b524c51..d40efcf1c3 100644 --- a/lms/djangoapps/badges/backends/badgr.py +++ b/lms/djangoapps/badges/backends/badgr.py @@ -175,5 +175,8 @@ class BadgrBackend(BadgeBackend): BadgrBackend.badges.append(slug) def award(self, badge_class, user, evidence_url=None): + """ + Make sure the badge class has been created on the backend, and then award the badge class to the user. + """ self._ensure_badge_created(badge_class) return self._create_assertion(badge_class, user, evidence_url) diff --git a/lms/djangoapps/badges/backends/tests/dummy_backend.py b/lms/djangoapps/badges/backends/tests/dummy_backend.py new file mode 100644 index 0000000000..828cb9bcdc --- /dev/null +++ b/lms/djangoapps/badges/backends/tests/dummy_backend.py @@ -0,0 +1,13 @@ +""" +Dummy backend, for use in testing. +""" +from lms.djangoapps.badges.backends.base import BadgeBackend +from lms.djangoapps.badges.tests.factories import BadgeAssertionFactory + + +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) diff --git a/lms/djangoapps/badges/backends/tests/test_badgr_backend.py b/lms/djangoapps/badges/backends/tests/test_badgr_backend.py index 455a0faeeb..ca79bf6c2b 100644 --- a/lms/djangoapps/badges/backends/tests/test_badgr_backend.py +++ b/lms/djangoapps/badges/backends/tests/test_badgr_backend.py @@ -24,6 +24,9 @@ BADGR_SETTINGS = { 'BADGR_ISSUER_SLUG': 'test-issuer', } +# Should be the hashed result of test_slug as the slug, and test_component as the component +EXAMPLE_SLUG = '15bb687e0c59ef2f0a49f6838f511bf4ca6c566dd45da6293cabbd9369390e1a' + # pylint: disable=protected-access @ddt.ddt @@ -104,7 +107,7 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): kwargs['data'], { 'name': 'Test Badge', - 'slug': '15bb687e0c59ef2f0a49f6838f511bf4ca6c566dd45da6293cabbd9369390e1a', + 'slug': EXAMPLE_SLUG, 'criteria': 'https://example.com/syllabus', 'description': "Yay! It's a test badge.", } @@ -114,14 +117,14 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): """ Make sure ensure_badge_created doesn't call create_badge if we know the badge is already there. """ - BadgrBackend.badges.append('15bb687e0c59ef2f0a49f6838f511bf4ca6c566dd45da6293cabbd9369390e1a') + BadgrBackend.badges.append(EXAMPLE_SLUG) self.handler._create_badge = Mock() self.handler._ensure_badge_created(self.badge_class) self.assertFalse(self.handler._create_badge.called) @ddt.unpack @ddt.data( - ('badge_class', '15bb687e0c59ef2f0a49f6838f511bf4ca6c566dd45da6293cabbd9369390e1a'), + ('badge_class', EXAMPLE_SLUG), ('legacy_badge_class', 'test_slug'), ('no_course_badge_class', 'test_componenttest_slug') ) @@ -140,11 +143,11 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): args, kwargs = get.call_args self.assertEqual( args[0], - 'https://example.com/v1/issuer/issuers/test-issuer/badges/' - '15bb687e0c59ef2f0a49f6838f511bf4ca6c566dd45da6293cabbd9369390e1a' + 'https://example.com/v1/issuer/issuers/test-issuer/badges/' + + EXAMPLE_SLUG ) self.check_headers(kwargs['headers']) - self.assertIn('15bb687e0c59ef2f0a49f6838f511bf4ca6c566dd45da6293cabbd9369390e1a', BadgrBackend.badges) + self.assertIn(EXAMPLE_SLUG, BadgrBackend.badges) self.assertFalse(self.handler._create_badge.called) @patch('requests.get') @@ -152,12 +155,12 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): response = Mock() response.status_code = 404 get.return_value = response - self.assertNotIn('15bb687e0c59ef2f0a49f6838f511bf4ca6c566dd45da6293cabbd9369390e1a', BadgrBackend.badges) + self.assertNotIn(EXAMPLE_SLUG, BadgrBackend.badges) self.handler._create_badge = Mock() self.handler._ensure_badge_created(self.badge_class) self.assertTrue(self.handler._create_badge.called) self.assertEqual(self.handler._create_badge.call_args, call(self.badge_class)) - self.assertIn('15bb687e0c59ef2f0a49f6838f511bf4ca6c566dd45da6293cabbd9369390e1a', BadgrBackend.badges) + self.assertIn(EXAMPLE_SLUG, BadgrBackend.badges) @patch('requests.post') def test_badge_creation_event(self, post): @@ -175,8 +178,9 @@ class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase): args, kwargs = post.call_args self.assertEqual( args[0], - 'https://example.com/v1/issuer/issuers/test-issuer/badges/' - '15bb687e0c59ef2f0a49f6838f511bf4ca6c566dd45da6293cabbd9369390e1a/assertions' + 'https://example.com/v1/issuer/issuers/test-issuer/badges/' + + EXAMPLE_SLUG + + '/assertions' ) self.check_headers(kwargs['headers']) assertion = BadgeAssertion.objects.get(user=self.user, badge_class__course_id=self.course.location.course_key) diff --git a/lms/djangoapps/badges/events/course_meta.py b/lms/djangoapps/badges/events/course_meta.py index 5b1dd20a87..e5bbb99c33 100644 --- a/lms/djangoapps/badges/events/course_meta.py +++ b/lms/djangoapps/badges/events/course_meta.py @@ -15,6 +15,9 @@ def award_badge(config, count, user): 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. + + Example config: + {3: 'slug_for_badge_for_three_enrollments', 5: 'slug_for_badge_with_five_enrollments'} """ slug = config.get(count) if not slug: diff --git a/lms/djangoapps/badges/events/tests/test_course_meta.py b/lms/djangoapps/badges/events/tests/test_course_meta.py index d1a79c118f..2183fd02c6 100644 --- a/lms/djangoapps/badges/events/tests/test_course_meta.py +++ b/lms/djangoapps/badges/events/tests/test_course_meta.py @@ -1,14 +1,13 @@ """ Tests the course meta badging events """ - +from ddt import ddt, unpack, data 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 badges.tests.factories import RandomBadgeClassFactory, CourseEventBadgesConfigurationFactory from certificates.models import GeneratedCertificate, CertificateStatuses from student.models import CourseEnrollment from student.tests.factories import UserFactory @@ -16,16 +15,9 @@ 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) - - +@ddt @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True}) -@override_settings(BADGING_BACKEND='lms.djangoapps.badges.events.tests.test_course_meta.DummyBackend') +@override_settings(BADGING_BACKEND='lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend') class CourseEnrollmentBadgeTest(ModuleStoreTestCase): """ Tests the event which awards badges based on number of courses a user is enrolled in. @@ -58,42 +50,25 @@ class CourseEnrollmentBadgeTest(ModuleStoreTestCase): CourseEnrollment.enroll(user, course_key=course.location.course_key) self.assertFalse(user.badgeassertion_set.all()) - def test_checkpoint_matches(self): + @unpack + @data((1, 3), (2, 5), (3, 8)) + def test_checkpoint_matches(self, checkpoint, required_badges): """ Make sure the proper badges are awarded at the right checkpoints. """ user = UserFactory() - courses = [CourseFactory() for _i in range(3)] + courses = [CourseFactory() for _i in range(required_badges)] 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]) + self.assertEqual(user.badgeassertion_set.all().count(), checkpoint) + self.assertEqual(assertions[checkpoint - 1].badge_class, self.badge_classes[checkpoint - 1]) +@ddt @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True}) -@override_settings(BADGING_BACKEND='lms.djangoapps.badges.events.tests.test_course_meta.DummyBackend') +@override_settings(BADGING_BACKEND='lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend') class CourseCompletionBadgeTest(ModuleStoreTestCase): """ Tests the event which awards badges based on the number of courses completed. @@ -130,47 +105,28 @@ class CourseCompletionBadgeTest(ModuleStoreTestCase): # pylint: disable=no-member self.assertFalse(user.badgeassertion_set.all()) - def test_checkpoint_matches(self): + @unpack + @data((1, 2), (2, 6), (3, 9)) + def test_checkpoint_matches(self, checkpoint, required_badges): """ Make sure the proper badges are awarded at the right checkpoints. """ user = UserFactory() - courses = [CourseFactory() for _i in range(2)] + courses = [CourseFactory() for _i in range(required_badges)] 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]) + self.assertEqual(user.badgeassertion_set.all().count(), checkpoint) + self.assertEqual(assertions[checkpoint - 1].badge_class, self.badge_classes[checkpoint - 1]) @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True}) -@override_settings(BADGING_BACKEND='lms.djangoapps.badges.events.tests.test_course_meta.DummyBackend') +@override_settings(BADGING_BACKEND='lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend') class CourseGroupBadgeTest(ModuleStoreTestCase): """ Tests the event which awards badges when a user completes a set of courses. diff --git a/lms/djangoapps/badges/migrations/0002_data__migrate_assertions.py b/lms/djangoapps/badges/migrations/0002_data__migrate_assertions.py index ae30c367e8..1bf50ea559 100644 --- a/lms/djangoapps/badges/migrations/0002_data__migrate_assertions.py +++ b/lms/djangoapps/badges/migrations/0002_data__migrate_assertions.py @@ -39,10 +39,9 @@ def forwards(apps, schema_editor): mode=image_config.mode, course_id=badge.course_id, ) - file_content = ContentFile(icon.read()) badge_class._meta.get_field('image').generate_filename = \ lambda inst, fn: os.path.join('badge_classes', fn) - badge_class.image.save(icon.name, file_content) + badge_class.image.name = icon.name badge_class.save() classes[(badge.course_id, badge.mode)] = badge_class if isinstance(badge.data, basestring): @@ -66,17 +65,15 @@ def forwards(apps, schema_editor): assertion.save() for configuration in BadgeImageConfiguration.objects.all(): - file_content = ContentFile(configuration.icon.read()) new_conf = CourseCompleteImageConfiguration( default=configuration.default, mode=configuration.mode, ) - new_conf.icon.save(configuration.icon.name, file_content) + new_conf.icon.name = configuration.icon.name new_conf.save() # def backwards(apps, schema_editor): - from django.core.files.base import ContentFile OldBadgeAssertion = apps.get_model("certificates", "BadgeAssertion") BadgeAssertion = apps.get_model("badges", "BadgeAssertion") BadgeImageConfiguration = apps.get_model("certificates", "BadgeImageConfiguration") @@ -97,12 +94,11 @@ def backwards(apps, schema_editor): ).save() for configuration in CourseCompleteImageConfiguration.objects.all(): - file_content = ContentFile(configuration.icon.read()) new_conf = BadgeImageConfiguration( default=configuration.default, mode=configuration.mode, ) - new_conf.icon.save(configuration.icon.name, file_content) + new_conf.icon.name = configuration.icon.name new_conf.save() diff --git a/lms/djangoapps/badges/models.py b/lms/djangoapps/badges/models.py index 3258e73a29..5f4b5a4d04 100644 --- a/lms/djangoapps/badges/models.py +++ b/lms/djangoapps/badges/models.py @@ -8,20 +8,21 @@ from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import ugettext_lazy as _ +from jsonfield import JSONField from lazy import lazy from model_utils.models import TimeStampedModel from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey +from badges.utils import deserialize_count_specs from config_models.models import ConfigurationModel from xmodule.modulestore.django import modulestore from xmodule_django.models import CourseKeyField -from jsonfield import JSONField def validate_badge_image(image): """ - Validates that a particular image is small enough, of the right type, and square to be a badge. + Validates that a particular image is small enough to be a badge and square. """ if image.width != image.height: raise ValidationError(_(u"The badge image must be square.")) @@ -70,6 +71,11 @@ class BadgeClass(models.Model): """ Looks up a badge class by its slug, issuing component, 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 + or not a badge class has already been created, but should just feed all requirements to this function + 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() @@ -253,32 +259,19 @@ class CourseEventBadgesConfiguration(ConfigurationModel): def __unicode__(self): return u"".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) + return deserialize_count_specs(self.courses_completed) @property def enrolled_settings(self): """ Parses the settings from the courses_completed field. """ - return self.get_specs(self.courses_enrolled) + return deserialize_count_specs(self.courses_enrolled) @property def course_group_settings(self): diff --git a/lms/djangoapps/badges/utils.py b/lms/djangoapps/badges/utils.py index 4c069d3a72..83c831cb84 100644 --- a/lms/djangoapps/badges/utils.py +++ b/lms/djangoapps/badges/utils.py @@ -20,7 +20,27 @@ def requires_badges_enabled(function): """ Wrapped function which bails out early if bagdes aren't enabled. """ - if not settings.FEATURES.get('ENABLE_OPENBADGES', False): + if not badges_enabled(): return return function(*args, **kwargs) return wrapped + + +def badges_enabled(): + """ + returns a boolean indicating whether or not openbadges are enabled. + """ + return settings.FEATURES.get('ENABLE_OPENBADGES', False) + + +def deserialize_count_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} diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py index 96349f5e34..bd3a656548 100644 --- a/lms/djangoapps/certificates/views/webview.py +++ b/lms/djangoapps/certificates/views/webview.py @@ -15,6 +15,7 @@ from django.utils.translation import ugettext as _ from django.utils.encoding import smart_str from badges.events.course_complete import get_completion_badge +from badges.utils import badges_enabled from courseware.access import has_access from edxmako.shortcuts import render_to_response from edxmako.template import Template @@ -445,7 +446,7 @@ def _update_badge_context(context, course, user): Updates context with badge info. """ badge = None - if settings.FEATURES.get('ENABLE_OPENBADGES') and course.issue_badges: + if badges_enabled() and course.issue_badges: badges = get_completion_badge(course.location.course_key, user).get_for_user(user) if badges: badge = badges[0] diff --git a/lms/djangoapps/lms_xblock/runtime.py b/lms/djangoapps/lms_xblock/runtime.py index cee5aceaf6..68a8604599 100644 --- a/lms/djangoapps/lms_xblock/runtime.py +++ b/lms/djangoapps/lms_xblock/runtime.py @@ -7,6 +7,7 @@ from django.core.urlresolvers import reverse from django.conf import settings from badges.service import BadgingService +from badges.utils import badges_enabled from openedx.core.djangoapps.user_api.course_tag import api as user_course_tag_api from request_cache.middleware import RequestCache import xblock.reference.plugins @@ -215,7 +216,7 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method store = modulestore() services['settings'] = SettingsService() services['user_tags'] = UserTagsService(self) - if settings.FEATURES["ENABLE_OPENBADGES"]: + if badges_enabled(): services['badging'] = BadgingService(course_id=kwargs.get('course_id'), modulestore=store) self.request_token = kwargs.pop('request_token', None) super(LmsModuleSystem, self).__init__(**kwargs) diff --git a/lms/djangoapps/student_profile/views.py b/lms/djangoapps/student_profile/views.py index 751a683a4d..5cd0ad95f4 100644 --- a/lms/djangoapps/student_profile/views.py +++ b/lms/djangoapps/student_profile/views.py @@ -9,6 +9,7 @@ from django.views.decorators.http import require_http_methods from django_countries import countries from django.contrib.staticfiles.storage import staticfiles_storage +from badges.utils import badges_enabled from edxmako.shortcuts import render_to_response, marketing_link from microsite_configuration import microsite from openedx.core.djangoapps.user_api.accounts.api import get_account_settings @@ -96,7 +97,7 @@ def learner_profile_context(request, profile_username, user_is_staff): 'disable_courseware_js': True, } - if settings.FEATURES.get("ENABLE_OPENBADGES"): + if badges_enabled(): context['data']['badges_api_url'] = reverse("badges_api:user_assertions", kwargs={'username': profile_username}) return context diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py index e72cdf034d..c0d801a556 100644 --- a/lms/envs/bok_choy.py +++ b/lms/envs/bok_choy.py @@ -161,6 +161,9 @@ FEATURES['ENABLE_COURSEWARE_SEARCH'] = True # Enable dashboard search for tests FEATURES['ENABLE_DASHBOARD_SEARCH'] = True +# Enable support for OpenBadges accomplishments +FEATURES['ENABLE_OPENBADGES'] = True + # Use MockSearchEngine as the search engine for test scenario SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine" # Path at which to store the mock index @@ -184,6 +187,8 @@ PROFILE_IMAGE_BACKEND = { FEATURES['ENABLE_CSMH_EXTENDED'] = True INSTALLED_APPS += ('coursewarehistoryextended',) +BADGING_BACKEND = 'lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend' + ##################################################################### # Lastly, see if the developer has any local overrides. try: diff --git a/lms/static/js/student_profile/views/share_modal_view.js b/lms/static/js/student_profile/views/share_modal_view.js index 21744ed793..81220fe3a5 100644 --- a/lms/static/js/student_profile/views/share_modal_view.js +++ b/lms/static/js/student_profile/views/share_modal_view.js @@ -8,6 +8,7 @@ attributes: { 'class': 'badges-overlay' }, + template: _.template(badgeModalTemplate), events: { 'click .badges-modal': function (event) {event.stopPropagation();}, 'click .badges-modal .close': 'close', @@ -38,7 +39,7 @@ this.$el.find('.badges-modal').focus(); }, render: function () { - this.$el.html(_.template(badgeModalTemplate, this.model.toJSON())); + this.$el.html(this.template(this.model.toJSON())); return this; } }); diff --git a/lms/static/sass/views/_learner-profile.scss b/lms/static/sass/views/_learner-profile.scss index 732d9a870e..a9846cff78 100644 --- a/lms/static/sass/views/_learner-profile.scss +++ b/lms/static/sass/views/_learner-profile.scss @@ -317,6 +317,14 @@ .badge-set-display { @extend .container; padding: 0 0; + + .badge-list { + // We're using a div instead of ul for accessibility, so we have to match the style + // used by ul. + margin: 1em 0; + padding: 0 0 0 40px; + } + .badge-display { width: 50%; display: inline-block; diff --git a/lms/templates/student_profile/badge_list.underscore b/lms/templates/student_profile/badge_list.underscore index 55f161b55a..23e722f9bc 100644 --- a/lms/templates/student_profile/badge_list.underscore +++ b/lms/templates/student_profile/badge_list.underscore @@ -1,4 +1,4 @@
- +
diff --git a/openedx/core/djangoapps/user_api/accounts/serializers.py b/openedx/core/djangoapps/user_api/accounts/serializers.py index b9952a817d..fdee8b046b 100644 --- a/openedx/core/djangoapps/user_api/accounts/serializers.py +++ b/openedx/core/djangoapps/user_api/accounts/serializers.py @@ -6,6 +6,7 @@ from django.contrib.auth.models import User from django.conf import settings from django.core.urlresolvers import reverse +from lms.djangoapps.badges.utils import badges_enabled from . import ( NAME_MIN_LENGTH, ACCOUNT_VISIBILITY_PREF_KEY, PRIVATE_VISIBILITY, ALL_USERS_VISIBILITY, @@ -63,7 +64,7 @@ class UserReadOnlySerializer(serializers.Serializer): :return: Dict serialized account """ profile = user.profile - accomplishments_shared = settings.FEATURES.get('ENABLE_OPENBADGES') or False + accomplishments_shared = badges_enabled() data = { "username": user.username, diff --git a/test_root/uploads/course_complete_badges/honor.png b/test_root/uploads/course_complete_badges/honor.png new file mode 100644 index 0000000000..7dbe80ed65 Binary files /dev/null and b/test_root/uploads/course_complete_badges/honor.png differ