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

@@ -9,7 +9,8 @@ from django.http import Http404, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from edxmako.shortcuts import render_to_string
from openedx.core.lib.xblock_utils import replace_static_urls, wrap_xblock, wrap_fragment, request_token
from openedx.core.lib.xblock_utils import replace_static_urls, wrap_xblock, wrap_fragment, wrap_xblock_aside,\
request_token
from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW
from xmodule.contentstore.django import contentstore
from xmodule.error_module import ErrorDescriptor
@@ -19,6 +20,7 @@ from xmodule.services import SettingsService
from xmodule.modulestore.django import modulestore, ModuleI18nService
from xmodule.mixin import wrap_with_license
from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.asides import AsideUsageKeyV1
from xmodule.x_module import ModuleSystem
from xblock.runtime import KvsFieldData
from xblock.django.request import webob_to_django_response, django_to_webob_request
@@ -56,8 +58,18 @@ def preview_handler(request, usage_key_string, handler, suffix=''):
"""
usage_key = UsageKey.from_string(usage_key_string)
descriptor = modulestore().get_item(usage_key)
instance = _load_preview_module(request, descriptor)
if isinstance(usage_key, AsideUsageKeyV1):
descriptor = modulestore().get_item(usage_key.usage_key)
for aside in descriptor.runtime.get_asides(descriptor):
if aside.scope_ids.block_type == usage_key.aside_type:
asides = [aside]
instance = aside
break
else:
descriptor = modulestore().get_item(usage_key)
instance = _load_preview_module(request, descriptor)
asides = []
# Let the module handle the AJAX
req = django_to_webob_request(request)
try:
@@ -80,6 +92,7 @@ def preview_handler(request, usage_key_string, handler, suffix=''):
log.exception("error processing ajax call")
raise
modulestore().update_item(descriptor, request.user.id, asides=asides)
return webob_to_django_response(resp)
@@ -184,6 +197,15 @@ def _preview_module_system(request, descriptor, field_data):
_studio_wrap_xblock,
]
wrappers_asides = [
partial(
wrap_xblock_aside,
'PreviewRuntime',
usage_id_serializer=unicode,
request_token=request_token(request)
)
]
if settings.FEATURES.get("LICENSING", False):
# stick the license wrapper in front
wrappers.insert(0, wrap_with_license)
@@ -208,6 +230,7 @@ def _preview_module_system(request, descriptor, field_data):
# Set up functions to modify the fragment produced by student_view
wrappers=wrappers,
wrappers_asides=wrappers_asides,
error_descriptor_class=ErrorDescriptor,
get_user_role=lambda: get_user_role(request.user, course_id),
# Get the raw DescriptorSystem, not the CombinedSystem

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)

View File

@@ -331,7 +331,8 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "common/j
success: function() {
self.onXBlockRefresh(temporaryView, block_added, is_duplicate);
temporaryView.unbind(); // Remove the temporary view
}
},
initRuntimeData: this
});
},

View File

@@ -30,6 +30,13 @@ define(["jquery", "underscore", "common/js/components/utils/view_utils", "js/vie
});
},
initRuntimeData: function(xblock, options) {
if (options && options.initRuntimeData && xblock && xblock.runtime && !xblock.runtime.page) {
xblock.runtime.page = options.initRuntimeData;
}
return xblock;
},
handleXBlockFragment: function(fragment, options) {
var self = this,
wrapper = this.$el,
@@ -44,8 +51,14 @@ define(["jquery", "underscore", "common/js/components/utils/view_utils", "js/vie
xblockElement = self.$('.xblock').first();
try {
xblock = XBlock.initializeBlock(xblockElement);
self.xblock = xblock;
self.xblockReady(xblock);
self.xblock = self.initRuntimeData(xblock, options);
self.xblockReady(self.xblock);
self.$('.xblock_asides-v1').each(function() {
if (!$(this).hasClass('xblock-initialized')) {
var aside = XBlock.initializeBlock($(this));
self.initRuntimeData(aside, options);
}
});
if (successCallback) {
successCallback(xblock);
}
@@ -76,6 +89,15 @@ define(["jquery", "underscore", "common/js/components/utils/view_utils", "js/vie
var runtime = this.xblock && this.xblock.runtime;
if (runtime) {
runtime.notify(eventName, data);
} else if (this.xblock) {
var xblock_children = this.xblock.element && $(this.xblock.element).prop('xblock_children');
if (xblock_children) {
$(xblock_children).each(function () {
if (this.runtime) {
this.runtime.notify(eventName, data);
}
});
}
}
},

View File

@@ -0,0 +1,40 @@
(function($) {
'use strict';
function StructuredTagsView(runtime, element) {
var $element = $(element);
$element.find("select").each(function() {
var loader = this;
var sts = $(this).attr('structured-tags-select-init');
if (typeof sts === typeof undefined || sts === false) {
$(this).attr('structured-tags-select-init', 1);
$(this).change(function(e) {
e.preventDefault();
var selectedKey = $(loader).find('option:selected').val();
runtime.notify('save', {
state: 'start',
element: element,
message: gettext('Updating Tags')
});
$.post(runtime.handlerUrl(element, 'save_tags'), {
'tag': $(loader).attr('name') + ':' + selectedKey
}).done(function() {
runtime.notify('save', {
state: 'end',
element: element
});
});
});
}
});
}
function initializeStructuredTags(runtime, element) {
return new StructuredTagsView(runtime, element);
}
window.StructuredTagsInit = initializeStructuredTags;
})($);

View File

@@ -1,7 +1,8 @@
<div class="xblock-render">
% for tag in tags:
<label for="problem_tags_${tag['key']}">${tag['title']}</label>:
<select id="problem_tags_${tag['key']}" name="problem_tags_${tag['key']}">
<select id="problem_tags_${tag['key']}" name="${tag['key']}">
<option value="" ${'' if tag['current_value'] else 'selected=""'}>Not selected</option>
% for k,v in tag['values'].iteritems():
<%
selected = ''