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:
Dmitry Viskov
2016-02-15 22:38:09 +03:00
parent d481768571
commit 209ddc700d
23 changed files with 1135 additions and 110 deletions

View File

@@ -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()

View 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)