Add XBlock Badging Service.
This commit is contained in:
11
lms/djangoapps/badges/service.py
Normal file
11
lms/djangoapps/badges/service.py
Normal file
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 """
|
||||
|
||||
|
||||
Reference in New Issue
Block a user