Difficulty selectbox in Studio (based on new XBlockAside functionality). Include:
- adaptation asides to be imported from the XML - updating SplitMongo to handle XBlockAsides (CRUD operations) - updating Studio to handle XBlockAsides handler calls - updating xblock/core.js to properly init XBlockAsides JavaScript
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
"""
|
||||
Example implementation of Structured Tagging based on XBlockAsides
|
||||
Structured Tagging based on XBlockAsides
|
||||
"""
|
||||
|
||||
from xblock.core import XBlockAside
|
||||
from xblock.core import XBlockAside, XBlock
|
||||
from xblock.fragment import Fragment
|
||||
from xblock.fields import Scope, Dict
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
from xmodule.capa_module import CapaModule
|
||||
from abc import ABCMeta, abstractproperty
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from django.conf import settings
|
||||
from webob import Response
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
_ = lambda text: text
|
||||
@@ -42,24 +45,24 @@ class AbstractTag(object):
|
||||
raise NotImplementedError('Subclasses must implement allowed_values')
|
||||
|
||||
|
||||
class LearningOutcomeTag(AbstractTag):
|
||||
class DifficultyTag(AbstractTag):
|
||||
"""
|
||||
Particular implementation tags for learning outcomes
|
||||
Particular implementation tags for difficulty
|
||||
"""
|
||||
@property
|
||||
def key(self):
|
||||
""" Identifier for the learning outcome selector """
|
||||
return 'learning_outcome_tag'
|
||||
""" Identifier for the difficulty selector """
|
||||
return 'difficulty_tag'
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Label for the learning outcome selector """
|
||||
return _('Learning outcomes')
|
||||
""" Label for the difficulty selector """
|
||||
return _('Difficulty')
|
||||
|
||||
@property
|
||||
def allowed_values(self):
|
||||
""" Allowed values for the learning outcome selector """
|
||||
return {'test1': 'Test 1', 'test2': 'Test 2', 'test3': 'Test 3'}
|
||||
""" Allowed values for the difficulty selector """
|
||||
return OrderedDict([('easy', 'Easy'), ('medium', 'Medium'), ('hard', 'Hard')])
|
||||
|
||||
|
||||
class StructuredTagsAside(XBlockAside):
|
||||
@@ -69,10 +72,16 @@ class StructuredTagsAside(XBlockAside):
|
||||
saved_tags = Dict(help=_("Dictionary with the available tags"),
|
||||
scope=Scope.content,
|
||||
default={},)
|
||||
available_tags = [LearningOutcomeTag()]
|
||||
available_tags = [DifficultyTag()]
|
||||
|
||||
def _get_studio_resource_url(self, relative_url):
|
||||
"""
|
||||
Returns the Studio URL to a static resource.
|
||||
"""
|
||||
return settings.STATIC_URL + relative_url
|
||||
|
||||
@XBlockAside.aside_for(STUDENT_VIEW)
|
||||
def student_view_aside(self, block, context):
|
||||
def student_view_aside(self, block, context): # pylint: disable=unused-argument
|
||||
"""
|
||||
Display the tag selector with specific categories and allowed values,
|
||||
depending on the context.
|
||||
@@ -86,7 +95,34 @@ class StructuredTagsAside(XBlockAside):
|
||||
'values': tag.allowed_values,
|
||||
'current_value': self.saved_tags.get(tag.key, None),
|
||||
})
|
||||
return Fragment(render_to_string('structured_tags_block.html', {'tags': tags}))
|
||||
#return Fragment(u'<div class="xblock-render">Hello world!!!</div>')
|
||||
fragment = Fragment(render_to_string('structured_tags_block.html', {'tags': tags}))
|
||||
fragment.add_javascript_url(self._get_studio_resource_url('/js/xblock_asides/structured_tags.js'))
|
||||
fragment.initialize_js('StructuredTagsInit')
|
||||
return fragment
|
||||
else:
|
||||
return Fragment(u'')
|
||||
|
||||
@XBlock.handler
|
||||
def save_tags(self, request=None, suffix=None): # pylint: disable=unused-argument
|
||||
"""
|
||||
Handler to save choosen tags with connected XBlock
|
||||
"""
|
||||
found = False
|
||||
if 'tag' not in request.params:
|
||||
return Response("The required parameter 'tag' is not passed", status=400)
|
||||
|
||||
tag = request.params['tag'].split(':')
|
||||
|
||||
for av_tag in self.available_tags:
|
||||
if av_tag.key == tag[0]:
|
||||
if tag[1] in av_tag.allowed_values:
|
||||
self.saved_tags[tag[0]] = tag[1]
|
||||
found = True
|
||||
elif tag[1] == '':
|
||||
self.saved_tags[tag[0]] = None
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
return Response("Invalid 'tag' parameter", status=400)
|
||||
|
||||
return Response()
|
||||
|
||||
166
cms/lib/xblock/test/test_tagging.py
Normal file
166
cms/lib/xblock/test/test_tagging.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Tests for the Studio Tagging XBlockAside
|
||||
"""
|
||||
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xblock_config.models import StudioConfig
|
||||
from cms.lib.xblock.tagging import StructuredTagsAside
|
||||
from contentstore.views.preview import get_preview_fragment
|
||||
from contentstore.utils import reverse_usage_url
|
||||
from contentstore.tests.utils import AjaxEnabledTestClient
|
||||
from django.test.client import RequestFactory
|
||||
from student.tests.factories import UserFactory
|
||||
from opaque_keys.edx.asides import AsideUsageKeyV1
|
||||
from datetime import datetime
|
||||
from pytz import UTC
|
||||
from lxml import etree
|
||||
from StringIO import StringIO
|
||||
|
||||
|
||||
class StructuredTagsAsideTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Base class for tests of StructuredTagsAside (tagging.py)
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Preparation for the test execution
|
||||
"""
|
||||
self.user_password = super(StructuredTagsAsideTestCase, self).setUp()
|
||||
self.aside_name = 'tagging_aside'
|
||||
self.aside_tag = 'difficulty_tag'
|
||||
self.aside_tag_value = 'hard'
|
||||
|
||||
course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split)
|
||||
self.course = ItemFactory.create(
|
||||
parent_location=course.location,
|
||||
category="course",
|
||||
display_name="Test course",
|
||||
)
|
||||
self.chapter = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category='chapter',
|
||||
display_name="Week 1",
|
||||
publish_item=True,
|
||||
start=datetime(2015, 3, 1, tzinfo=UTC),
|
||||
)
|
||||
self.sequential = ItemFactory.create(
|
||||
parent_location=self.chapter.location,
|
||||
category='sequential',
|
||||
display_name="Lesson 1",
|
||||
publish_item=True,
|
||||
start=datetime(2015, 3, 1, tzinfo=UTC),
|
||||
)
|
||||
self.vertical = ItemFactory.create(
|
||||
parent_location=self.sequential.location,
|
||||
category='vertical',
|
||||
display_name='Subsection 1',
|
||||
publish_item=True,
|
||||
start=datetime(2015, 4, 1, tzinfo=UTC),
|
||||
)
|
||||
self.problem = ItemFactory.create(
|
||||
category="problem",
|
||||
parent_location=self.vertical.location,
|
||||
display_name="A Problem Block",
|
||||
weight=1,
|
||||
user_id=self.user.id,
|
||||
publish_item=False,
|
||||
)
|
||||
self.video = ItemFactory.create(
|
||||
parent_location=self.vertical.location,
|
||||
category='video',
|
||||
display_name='My Video',
|
||||
user_id=self.user.id
|
||||
)
|
||||
|
||||
config = StudioConfig.current()
|
||||
config.enabled = True
|
||||
config.save()
|
||||
|
||||
def test_aside_contains_tags(self):
|
||||
"""
|
||||
Checks that available_tags list is not empty
|
||||
"""
|
||||
self.assertGreater(len(StructuredTagsAside.available_tags), 0,
|
||||
"StructuredTagsAside should contains at least one available tag")
|
||||
|
||||
def test_preview_html(self):
|
||||
"""
|
||||
Checks that html for the StructuredTagsAside is generated correctly
|
||||
"""
|
||||
request = RequestFactory().get('/dummy-url')
|
||||
request.user = UserFactory()
|
||||
request.session = {}
|
||||
|
||||
# Call get_preview_fragment directly.
|
||||
context = {
|
||||
'reorderable_items': set(),
|
||||
'read_only': True
|
||||
}
|
||||
problem_html = get_preview_fragment(request, self.problem, context).content
|
||||
|
||||
parser = etree.HTMLParser()
|
||||
tree = etree.parse(StringIO(problem_html), parser)
|
||||
|
||||
main_div_nodes = tree.xpath('/html/body/div/section/div')
|
||||
self.assertEquals(len(main_div_nodes), 1)
|
||||
|
||||
div_node = main_div_nodes[0]
|
||||
self.assertEquals(div_node.get('data-init'), 'StructuredTagsInit')
|
||||
self.assertEquals(div_node.get('data-runtime-class'), 'PreviewRuntime')
|
||||
self.assertEquals(div_node.get('data-block-type'), 'tagging_aside')
|
||||
self.assertEquals(div_node.get('data-runtime-version'), '1')
|
||||
self.assertIn('xblock_asides-v1', div_node.get('class'))
|
||||
|
||||
select_nodes = div_node.xpath('div/select')
|
||||
self.assertEquals(len(select_nodes), 1)
|
||||
|
||||
select_node = select_nodes[0]
|
||||
self.assertEquals(select_node.get('name'), self.aside_tag)
|
||||
|
||||
# Now ensure the acid_aside is not in the result
|
||||
self.assertNotRegexpMatches(problem_html, r"data-block-type=[\"\']acid_aside[\"\']")
|
||||
|
||||
# Ensure about video don't have asides
|
||||
video_html = get_preview_fragment(request, self.video, context).content
|
||||
self.assertNotRegexpMatches(video_html, "<select")
|
||||
|
||||
def test_handle_requests(self):
|
||||
"""
|
||||
Checks that handler to save tags in StructuredTagsAside works properly
|
||||
"""
|
||||
handler_url = reverse_usage_url(
|
||||
'preview_handler',
|
||||
'%s:%s::%s' % (AsideUsageKeyV1.CANONICAL_NAMESPACE, self.problem.location, self.aside_name),
|
||||
kwargs={'handler': 'save_tags'}
|
||||
)
|
||||
|
||||
client = AjaxEnabledTestClient()
|
||||
client.login(username=self.user.username, password=self.user_password)
|
||||
|
||||
response = client.post(path=handler_url, data={})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
response = client.post(path=handler_url, data={'tag': 'undefined_tag:undefined'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
val = '%s:undefined' % self.aside_tag
|
||||
response = client.post(path=handler_url, data={'tag': val})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
val = '%s:%s' % (self.aside_tag, self.aside_tag_value)
|
||||
response = client.post(path=handler_url, data={'tag': val})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
problem = modulestore().get_item(self.problem.location)
|
||||
asides = problem.runtime.get_asides(problem)
|
||||
tag_aside = None
|
||||
for aside in asides:
|
||||
if isinstance(aside, StructuredTagsAside):
|
||||
tag_aside = aside
|
||||
break
|
||||
|
||||
self.assertIsNotNone(tag_aside, "Necessary StructuredTagsAside object isn't found")
|
||||
self.assertEqual(tag_aside.saved_tags[self.aside_tag], self.aside_tag_value)
|
||||
Reference in New Issue
Block a user