Commit 25958feb by Jonathan Piacenti

Add XBlock Badging Service.

parent 112a1435
"""
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
...@@ -17,11 +17,7 @@ from certificates.tests.test_models import TEST_DATA_ROOT ...@@ -17,11 +17,7 @@ from certificates.tests.test_models import TEST_DATA_ROOT
from student.tests.factories import UserFactory 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.
"""
def get_image(self, name):
""" """
Get one of the test images from the test data directory. Get one of the test images from the test data directory.
""" """
...@@ -29,7 +25,7 @@ class ImageFetchingMixin(object): ...@@ -29,7 +25,7 @@ class ImageFetchingMixin(object):
@attr('shard_1') @attr('shard_1')
class BadgeImageConfigurationTest(TestCase, ImageFetchingMixin): class BadgeImageConfigurationTest(TestCase):
""" """
Test the validation features of BadgeImageConfiguration. Test the validation features of BadgeImageConfiguration.
""" """
...@@ -38,10 +34,10 @@ class BadgeImageConfigurationTest(TestCase, ImageFetchingMixin): ...@@ -38,10 +34,10 @@ class BadgeImageConfigurationTest(TestCase, ImageFetchingMixin):
""" """
Verify that creating two configurations as default is not permitted. 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( self.assertRaises(
ValidationError, 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): def test_runs_validator(self):
...@@ -50,7 +46,7 @@ class BadgeImageConfigurationTest(TestCase, ImageFetchingMixin): ...@@ -50,7 +46,7 @@ class BadgeImageConfigurationTest(TestCase, ImageFetchingMixin):
""" """
self.assertRaises( self.assertRaises(
ValidationError, 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): ...@@ -60,7 +56,7 @@ class DummyBackend(object):
""" """
class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin): class BadgeClassTest(ModuleStoreTestCase):
""" """
Test BadgeClass functionality Test BadgeClass functionality
""" """
...@@ -80,7 +76,7 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin): ...@@ -80,7 +76,7 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin):
# Ignore additional parameters. This class already exists. # Ignore additional parameters. This class already exists.
badge_class = BadgeClass.get_badge_class( badge_class = BadgeClass.get_badge_class(
slug='test_slug', issuing_component='test_component', description='Attempted override', 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. # These defaults are set on the factory.
self.assertEqual(badge_class.criteria, 'https://example.com/syllabus') self.assertEqual(badge_class.criteria, 'https://example.com/syllabus')
...@@ -97,11 +93,11 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin): ...@@ -97,11 +93,11 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin):
premade_badge_class = BadgeClassFactory.create(course_id=course_key) premade_badge_class = BadgeClassFactory.create(course_id=course_key)
badge_class = BadgeClass.get_badge_class( badge_class = BadgeClass.get_badge_class(
slug='test_slug', issuing_component='test_component', description='Attempted override', 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( course_badge_class = BadgeClass.get_badge_class(
slug='test_slug', issuing_component='test_component', description='Attempted override', 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, course_id=course_key,
) )
self.assertNotEqual(badge_class.id, course_badge_class.id) self.assertNotEqual(badge_class.id, course_badge_class.id)
...@@ -114,7 +110,7 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin): ...@@ -114,7 +110,7 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin):
badge_class = BadgeClass.get_badge_class( badge_class = BadgeClass.get_badge_class(
slug='new_slug', issuing_component='new_component', description='This is a test', slug='new_slug', issuing_component='new_component', description='This is a test',
criteria='https://example.com/test_criteria', display_name='Super Badge', 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. # This should have been saved before being passed back.
self.assertTrue(badge_class.id) self.assertTrue(badge_class.id)
...@@ -152,7 +148,7 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin): ...@@ -152,7 +148,7 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin):
BadgeClass.get_badge_class, BadgeClass.get_badge_class,
slug='new_slug', issuing_component='new_component', description='This is a test', slug='new_slug', issuing_component='new_component', description='This is a test',
criteria='https://example.com/test_criteria', display_name='Super Badge', 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): def test_get_for_user(self):
...@@ -185,7 +181,7 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin): ...@@ -185,7 +181,7 @@ class BadgeClassTest(ModuleStoreTestCase, ImageFetchingMixin):
ValidationError, ValidationError,
BadgeClass( BadgeClass(
slug='test', issuing_component='test2', criteria='test3', slug='test', issuing_component='test2', criteria='test3',
description='test4', image=self.get_image('unbalanced') description='test4', image=get_image('unbalanced')
).full_clean ).full_clean
) )
...@@ -221,7 +217,7 @@ class BadgeAssertionTest(ModuleStoreTestCase): ...@@ -221,7 +217,7 @@ class BadgeAssertionTest(ModuleStoreTestCase):
self.assertEqual(course_scoped_assertions, course_assertions) self.assertEqual(course_scoped_assertions, course_assertions)
class ValidBadgeImageTest(TestCase, ImageFetchingMixin): class ValidBadgeImageTest(TestCase):
""" """
Tests the badge image field validator. Tests the badge image field validator.
""" """
...@@ -229,18 +225,18 @@ class ValidBadgeImageTest(TestCase, ImageFetchingMixin): ...@@ -229,18 +225,18 @@ class ValidBadgeImageTest(TestCase, ImageFetchingMixin):
""" """
Verify that saving a valid badge image is no problem. 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): def test_unbalanced_image(self):
""" """
Verify that setting an image with an uneven width and height raises an error. 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) self.assertRaises(ValidationError, validate_badge_image, unbalanced)
def test_large_image(self): def test_large_image(self):
""" """
Verify that setting an image that is too big raises an error. 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) self.assertRaises(ValidationError, validate_badge_image, large)
...@@ -6,6 +6,7 @@ import re ...@@ -6,6 +6,7 @@ import re
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings 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 openedx.core.djangoapps.user_api.course_tag import api as user_course_tag_api
from request_cache.middleware import RequestCache from request_cache.middleware import RequestCache
import xblock.reference.plugins import xblock.reference.plugins
...@@ -213,6 +214,8 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method ...@@ -213,6 +214,8 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method
) )
services['settings'] = SettingsService() services['settings'] = SettingsService()
services['user_tags'] = UserTagsService(self) services['user_tags'] = UserTagsService(self)
if settings.FEATURES["ENABLE_OPENBADGES"]:
services['badging'] = BadgingService()
self.request_token = kwargs.pop('request_token', None) self.request_token = kwargs.pop('request_token', None)
super(LmsModuleSystem, self).__init__(**kwargs) super(LmsModuleSystem, self).__init__(**kwargs)
......
...@@ -5,16 +5,23 @@ Tests of the LMS XBlock Runtime and associated utilities ...@@ -5,16 +5,23 @@ Tests of the LMS XBlock Runtime and associated utilities
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings from django.conf import settings
from ddt import ddt, data from ddt import ddt, data
from mock import Mock from django.test import TestCase
from unittest import TestCase from mock import Mock, patch
from urlparse import urlparse from urlparse import urlparse
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import BlockUsageLocator, CourseLocator, SlashSeparatedCourseKey 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 lms.djangoapps.lms_xblock.runtime import quote_slashes, unquote_slashes, LmsModuleSystem
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from xmodule.modulestore.django import ModuleI18nService from xmodule.modulestore.django import ModuleI18nService
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xblock.exceptions import NoSuchServiceError from xblock.exceptions import NoSuchServiceError
from student.tests.factories import UserFactory
TEST_STRINGS = [ TEST_STRINGS = [
'', '',
'foobar', 'foobar',
...@@ -141,9 +148,7 @@ class TestUserServiceAPI(TestCase): ...@@ -141,9 +148,7 @@ class TestUserServiceAPI(TestCase):
def setUp(self): def setUp(self):
super(TestUserServiceAPI, self).setUp() super(TestUserServiceAPI, self).setUp()
self.course_id = SlashSeparatedCourseKey("org", "course", "run") self.course_id = SlashSeparatedCourseKey("org", "course", "run")
self.user = UserFactory.create()
self.user = User(username='runtime_robot', email='runtime_robot@edx.org', password='test', first_name='Robot')
self.user.save()
def mock_get_real_user(_anon_id): def mock_get_real_user(_anon_id):
"""Just returns the test user""" """Just returns the test user"""
...@@ -186,6 +191,67 @@ class TestUserServiceAPI(TestCase): ...@@ -186,6 +191,67 @@ class TestUserServiceAPI(TestCase):
self.runtime.service(self.mock_block, 'user_tags').get_tag('fake_scope', self.key) 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): class TestI18nService(ModuleStoreTestCase):
""" Test ModuleI18nService """ """ Test ModuleI18nService """
......
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