diff --git a/lms/djangoapps/badges/service.py b/lms/djangoapps/badges/service.py new file mode 100644 index 0000000000..05cc520795 --- /dev/null +++ b/lms/djangoapps/badges/service.py @@ -0,0 +1,11 @@ +""" +Badging service for XBlocks +""" +from badges.models import BadgeClass + + +class BadgingService(object): + """ + A class that provides functions for managing badges which XBlocks can use. + """ + get_badge_class = BadgeClass.get_badge_class diff --git a/lms/djangoapps/badges/tests/test_models.py b/lms/djangoapps/badges/tests/test_models.py index 0a17c92b53..a59ec41e2c 100644 --- a/lms/djangoapps/badges/tests/test_models.py +++ b/lms/djangoapps/badges/tests/test_models.py @@ -17,19 +17,15 @@ from certificates.tests.test_models import TEST_DATA_ROOT from student.tests.factories import UserFactory -class ImageFetchingMixin(object): +def get_image(name): """ - Provides the ability to grab a badge image from the test data root. + Get one of the test images from the test data directory. """ - def get_image(self, name): - """ - Get one of the test images from the test data directory. - """ - return ImageFile(open(TEST_DATA_ROOT / 'badges' / name + '.png')) + return ImageFile(open(TEST_DATA_ROOT / 'badges' / name + '.png')) @attr('shard_1') -class BadgeImageConfigurationTest(TestCase, ImageFetchingMixin): +class BadgeImageConfigurationTest(TestCase): """ Test the validation features of BadgeImageConfiguration. """ @@ -38,10 +34,10 @@ class BadgeImageConfigurationTest(TestCase, ImageFetchingMixin): """ Verify that creating two configurations as default is not permitted. """ - CourseCompleteImageConfiguration(mode='test', icon=self.get_image('good'), default=True).save() + CourseCompleteImageConfiguration(mode='test', icon=get_image('good'), default=True).save() self.assertRaises( ValidationError, - CourseCompleteImageConfiguration(mode='test2', icon=self.get_image('good'), default=True).full_clean + CourseCompleteImageConfiguration(mode='test2', icon=get_image('good'), default=True).full_clean ) def test_runs_validator(self): @@ -50,7 +46,7 @@ class BadgeImageConfigurationTest(TestCase, ImageFetchingMixin): """ self.assertRaises( ValidationError, - CourseCompleteImageConfiguration(mode='test2', icon=self.get_image('unbalanced')).full_clean + CourseCompleteImageConfiguration(mode='test2', icon=get_image('unbalanced')).full_clean ) @@ -60,7 +56,7 @@ class DummyBackend(object): """ -class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin): +class BadgeClassTest(ModuleStoreTestCase): """ Test BadgeClass functionality """ @@ -80,7 +76,7 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin): # Ignore additional parameters. This class already exists. badge_class = BadgeClass.get_badge_class( slug='test_slug', issuing_component='test_component', description='Attempted override', - criteria='test', display_name='Testola', image_file_handle=self.get_image('good') + criteria='test', display_name='Testola', image_file_handle=get_image('good') ) # These defaults are set on the factory. self.assertEqual(badge_class.criteria, 'https://example.com/syllabus') @@ -97,11 +93,11 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin): premade_badge_class = BadgeClassFactory.create(course_id=course_key) badge_class = BadgeClass.get_badge_class( slug='test_slug', issuing_component='test_component', description='Attempted override', - criteria='test', display_name='Testola', image_file_handle=self.get_image('good') + criteria='test', display_name='Testola', image_file_handle=get_image('good') ) course_badge_class = BadgeClass.get_badge_class( slug='test_slug', issuing_component='test_component', description='Attempted override', - criteria='test', display_name='Testola', image_file_handle=self.get_image('good'), + criteria='test', display_name='Testola', image_file_handle=get_image('good'), course_id=course_key, ) self.assertNotEqual(badge_class.id, course_badge_class.id) @@ -114,7 +110,7 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin): badge_class = BadgeClass.get_badge_class( slug='new_slug', issuing_component='new_component', description='This is a test', criteria='https://example.com/test_criteria', display_name='Super Badge', - image_file_handle=self.get_image('good') + image_file_handle=get_image('good') ) # This should have been saved before being passed back. self.assertTrue(badge_class.id) @@ -152,7 +148,7 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin): BadgeClass.get_badge_class, slug='new_slug', issuing_component='new_component', description='This is a test', criteria='https://example.com/test_criteria', display_name='Super Badge', - image_file_handle=self.get_image('unbalanced') + image_file_handle=get_image('unbalanced') ) def test_get_for_user(self): @@ -185,7 +181,7 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin): ValidationError, BadgeClass( slug='test', issuing_component='test2', criteria='test3', - description='test4', image=self.get_image('unbalanced') + description='test4', image=get_image('unbalanced') ).full_clean ) @@ -221,7 +217,7 @@ class BadgeAssertionTest(ModuleStoreTestCase): self.assertEqual(course_scoped_assertions, course_assertions) -class ValidBadgeImageTest(TestCase, ImageFetchingMixin): +class ValidBadgeImageTest(TestCase): """ Tests the badge image field validator. """ @@ -229,18 +225,18 @@ class ValidBadgeImageTest(TestCase, ImageFetchingMixin): """ Verify that saving a valid badge image is no problem. """ - validate_badge_image(self.get_image('good')) + validate_badge_image(get_image('good')) def test_unbalanced_image(self): """ Verify that setting an image with an uneven width and height raises an error. """ - unbalanced = ImageFile(self.get_image('unbalanced')) + unbalanced = ImageFile(get_image('unbalanced')) self.assertRaises(ValidationError, validate_badge_image, unbalanced) def test_large_image(self): """ Verify that setting an image that is too big raises an error. """ - large = self.get_image('large') + large = get_image('large') self.assertRaises(ValidationError, validate_badge_image, large) diff --git a/lms/djangoapps/lms_xblock/runtime.py b/lms/djangoapps/lms_xblock/runtime.py index 1206eef977..c06784115f 100644 --- a/lms/djangoapps/lms_xblock/runtime.py +++ b/lms/djangoapps/lms_xblock/runtime.py @@ -6,6 +6,7 @@ import re from django.core.urlresolvers import reverse from django.conf import settings +from badges.service import BadgingService 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 @@ -213,6 +214,8 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method ) services['settings'] = SettingsService() services['user_tags'] = UserTagsService(self) + if settings.FEATURES["ENABLE_OPENBADGES"]: + services['badging'] = BadgingService() self.request_token = kwargs.pop('request_token', None) super(LmsModuleSystem, self).__init__(**kwargs) diff --git a/lms/djangoapps/lms_xblock/test/test_runtime.py b/lms/djangoapps/lms_xblock/test/test_runtime.py index 5b852cc859..70f3fc56d1 100644 --- a/lms/djangoapps/lms_xblock/test/test_runtime.py +++ b/lms/djangoapps/lms_xblock/test/test_runtime.py @@ -5,16 +5,23 @@ Tests of the LMS XBlock Runtime and associated utilities from django.contrib.auth.models import User from django.conf import settings from ddt import ddt, data -from mock import Mock -from unittest import TestCase +from django.test import TestCase +from mock import Mock, patch from urlparse import urlparse + +from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locations import BlockUsageLocator, CourseLocator, SlashSeparatedCourseKey + +from badges.tests.factories import BadgeClassFactory +from badges.tests.test_models import get_image from lms.djangoapps.lms_xblock.runtime import quote_slashes, unquote_slashes, LmsModuleSystem from xblock.fields import ScopeIds from xmodule.modulestore.django import ModuleI18nService from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xblock.exceptions import NoSuchServiceError +from student.tests.factories import UserFactory + TEST_STRINGS = [ '', 'foobar', @@ -141,9 +148,7 @@ class TestUserServiceAPI(TestCase): def setUp(self): super(TestUserServiceAPI, self).setUp() self.course_id = SlashSeparatedCourseKey("org", "course", "run") - - self.user = User(username='runtime_robot', email='runtime_robot@edx.org', password='test', first_name='Robot') - self.user.save() + self.user = UserFactory.create() def mock_get_real_user(_anon_id): """Just returns the test user""" @@ -186,6 +191,67 @@ class TestUserServiceAPI(TestCase): self.runtime.service(self.mock_block, 'user_tags').get_tag('fake_scope', self.key) +class TestBadgingService(TestCase): + """Test the badging service interface""" + + def setUp(self): + super(TestBadgingService, self).setUp() + self.course_id = CourseKey.from_string('course-v1:org+course+run') + + self.user = User(username='test_robot', email='test_robot@edx.org', password='test', first_name='Test') + self.user.save() + + self.mock_block = Mock() + self.mock_block.service_declaration.return_value = 'needs' + + def create_runtime(self): + """ + Create the testing runtime. + """ + def mock_get_real_user(_anon_id): + """Just returns the test user""" + return self.user + + return LmsModuleSystem( + static_url='/static', + track_function=Mock(), + get_module=Mock(), + render_template=Mock(), + replace_urls=str, + course_id=self.course_id, + get_real_user=mock_get_real_user, + descriptor_runtime=Mock(), + ) + + @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True}) + def test_service_rendered(self): + runtime = self.create_runtime() + self.assertTrue(runtime.service(self.mock_block, 'badging')) + + @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': False}) + def test_no_service_rendered(self): + runtime = self.create_runtime() + self.assertFalse(runtime.service(self.mock_block, 'badging')) + + @patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True}) + def test_get_badge_class(self): + runtime = self.create_runtime() + badge_service = runtime.service(self.mock_block, 'badging') + premade_badge_class = BadgeClassFactory.create() + # Ignore additional parameters. This class already exists. + # We should get back the first class we created, rather than a new one. + badge_class = badge_service.get_badge_class( + slug='test_slug', issuing_component='test_component', description='Attempted override', + criteria='test', display_name='Testola', image_file_handle=get_image('good') + ) + # These defaults are set on the factory. + self.assertEqual(badge_class.criteria, 'https://example.com/syllabus') + self.assertEqual(badge_class.display_name, 'Test Badge') + self.assertEqual(badge_class.description, "Yay! It's a test badge.") + # File name won't always be the same. + self.assertEqual(badge_class.image.path, premade_badge_class.image.path) + + class TestI18nService(ModuleStoreTestCase): """ Test ModuleI18nService """