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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
40
cms/static/js/xblock_asides/structured_tags.js
Normal file
40
cms/static/js/xblock_asides/structured_tags.js
Normal 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;
|
||||
})($);
|
||||
@@ -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 = ''
|
||||
|
||||
Reference in New Issue
Block a user