Commit 1ee87c88 by Gabe Mulley

Merge pull request #12212 from CredoReference/assigned-tags-included-in-the-emitted-event

Connected aside information is included in the emitted event (on check)
parents c64e0f37 21bbcc9f
...@@ -48,7 +48,10 @@ def add_a_multi_step_component(step, is_advanced, category): ...@@ -48,7 +48,10 @@ def add_a_multi_step_component(step, is_advanced, category):
def see_a_multi_step_component(step, category): def see_a_multi_step_component(step, category):
# Wait for all components to finish rendering # Wait for all components to finish rendering
selector = 'li.studio-xblock-wrapper div.xblock-student_view' if category == 'HTML':
selector = 'li.studio-xblock-wrapper div.xblock-student_view'
else:
selector = 'li.studio-xblock-wrapper div.xblock-author_view'
world.wait_for(lambda _: len(world.css_find(selector)) == len(step.hashes)) world.wait_for(lambda _: len(world.css_find(selector)) == len(step.hashes))
for idx, step_hash in enumerate(step.hashes): for idx, step_hash in enumerate(step.hashes):
......
...@@ -16,6 +16,7 @@ from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW ...@@ -16,6 +16,7 @@ from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.studio_editable import has_author_view
from xmodule.services import SettingsService from xmodule.services import SettingsService
from xmodule.modulestore.django import modulestore, ModuleI18nService from xmodule.modulestore.django import modulestore, ModuleI18nService
from xmodule.mixin import wrap_with_license from xmodule.mixin import wrap_with_license
...@@ -122,6 +123,9 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method ...@@ -122,6 +123,9 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
""" """
if not StudioConfig.asides_enabled(block.scope_ids.block_type): if not StudioConfig.asides_enabled(block.scope_ids.block_type):
return [] return []
# TODO: aside_type != 'acid_aside' check should be removed once AcidBlock is only installed during tests
# (see https://openedx.atlassian.net/browse/TE-811)
return [ return [
aside_type aside_type
for aside_type in super(PreviewModuleSystem, self).applicable_aside_types(block) for aside_type in super(PreviewModuleSystem, self).applicable_aside_types(block)
...@@ -140,10 +144,13 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method ...@@ -140,10 +144,13 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
result.add_frag_resources(frag) result.add_frag_resources(frag)
for aside, aside_fn in aside_frag_fns: for aside, aside_fn in aside_frag_fns:
aside_frag = self.wrap_aside(block, aside, view_name, aside_fn(block, context), context) aside_frag = aside_fn(block, context)
aside.save() if aside_frag.content != u'':
result.add_frag_resources(aside_frag) aside_frag_wrapped = self.wrap_aside(block, aside, view_name, aside_frag, context)
frag.content = frag.content.replace(position_for_asides, position_for_asides + aside_frag.content) aside.save()
result.add_frag_resources(aside_frag_wrapped)
replacement = position_for_asides + aside_frag_wrapped.content
frag.content = frag.content.replace(position_for_asides, replacement)
result.add_content(frag.content) result.add_content(frag.content)
return result return result
...@@ -231,7 +238,7 @@ def _load_preview_module(request, descriptor): ...@@ -231,7 +238,7 @@ def _load_preview_module(request, descriptor):
descriptor: An XModuleDescriptor descriptor: An XModuleDescriptor
""" """
student_data = KvsFieldData(SessionKeyValueStore(request)) student_data = KvsFieldData(SessionKeyValueStore(request))
if _has_author_view(descriptor): if has_author_view(descriptor):
wrapper = partial(CmsFieldData, student_data=student_data) wrapper = partial(CmsFieldData, student_data=student_data)
else: else:
wrapper = partial(LmsFieldData, student_data=student_data) wrapper = partial(LmsFieldData, student_data=student_data)
...@@ -288,7 +295,7 @@ def get_preview_fragment(request, descriptor, context): ...@@ -288,7 +295,7 @@ def get_preview_fragment(request, descriptor, context):
""" """
module = _load_preview_module(request, descriptor) module = _load_preview_module(request, descriptor)
preview_view = AUTHOR_VIEW if _has_author_view(module) else STUDENT_VIEW preview_view = AUTHOR_VIEW if has_author_view(module) else STUDENT_VIEW
try: try:
fragment = module.render(preview_view, context) fragment = module.render(preview_view, context)
...@@ -296,12 +303,3 @@ def get_preview_fragment(request, descriptor, context): ...@@ -296,12 +303,3 @@ def get_preview_fragment(request, descriptor, context):
log.warning("Unable to render %s for %r", preview_view, module, exc_info=True) log.warning("Unable to render %s for %r", preview_view, module, exc_info=True)
fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)})) fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
return fragment return fragment
def _has_author_view(descriptor):
"""
Returns True if the xmodule linked to the descriptor supports "author_view".
If False, "student_view" and LmsFieldData should be used.
"""
return getattr(descriptor, 'has_author_view', False)
...@@ -6,7 +6,7 @@ Structured Tagging based on XBlockAsides ...@@ -6,7 +6,7 @@ Structured Tagging based on XBlockAsides
from xblock.core import XBlockAside, XBlock from xblock.core import XBlockAside, XBlock
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblock.fields import Scope, Dict from xblock.fields import Scope, Dict
from xmodule.x_module import STUDENT_VIEW from xmodule.x_module import AUTHOR_VIEW
from xmodule.capa_module import CapaModule from xmodule.capa_module import CapaModule
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from django.conf import settings from django.conf import settings
...@@ -37,7 +37,7 @@ class StructuredTagsAside(XBlockAside): ...@@ -37,7 +37,7 @@ class StructuredTagsAside(XBlockAside):
""" """
return settings.STATIC_URL + relative_url return settings.STATIC_URL + relative_url
@XBlockAside.aside_for(STUDENT_VIEW) @XBlockAside.aside_for(AUTHOR_VIEW)
def student_view_aside(self, block, context): # pylint: disable=unused-argument def student_view_aside(self, block, context): # pylint: disable=unused-argument
""" """
Display the tag selector with specific categories and allowed values, Display the tag selector with specific categories and allowed values,
...@@ -90,3 +90,12 @@ class StructuredTagsAside(XBlockAside): ...@@ -90,3 +90,12 @@ class StructuredTagsAside(XBlockAside):
return Response("Invalid 'tag' parameter", status=400) return Response("Invalid 'tag' parameter", status=400)
return Response() return Response()
def get_event_context(self, event_type, event): # pylint: disable=unused-argument
"""
This method return data that should be associated with the "check_problem" event
"""
if self.saved_tags and event_type == "problem_check":
return {'saved_tags': self.saved_tags}
else:
return None
...@@ -620,7 +620,7 @@ class CapaMixin(CapaFields): ...@@ -620,7 +620,7 @@ class CapaMixin(CapaFields):
event_info['hint_index'] = hint_index event_info['hint_index'] = hint_index
event_info['hint_len'] = len(demand_hints) event_info['hint_len'] = len(demand_hints)
event_info['hint_text'] = hint_text event_info['hint_text'] = hint_text
self.runtime.track_function('edx.problem.hint.demandhint_displayed', event_info) self.runtime.publish(self, 'edx.problem.hint.demandhint_displayed', event_info)
# We report the index of this hint, the client works out what index to use to get the next hint # We report the index of this hint, the client works out what index to use to get the next hint
return { return {
...@@ -1145,7 +1145,7 @@ class CapaMixin(CapaFields): ...@@ -1145,7 +1145,7 @@ class CapaMixin(CapaFields):
# avoiding problems where an event_info is unmasked twice. # avoiding problems where an event_info is unmasked twice.
event_unmasked = copy.deepcopy(event_info) event_unmasked = copy.deepcopy(event_info)
self.unmask_event(event_unmasked) self.unmask_event(event_unmasked)
self.runtime.track_function(title, event_unmasked) self.runtime.publish(self, title, event_unmasked)
def unmask_event(self, event_info): def unmask_event(self, event_info):
""" """
......
...@@ -49,6 +49,12 @@ class CapaModule(CapaMixin, XModule): ...@@ -49,6 +49,12 @@ class CapaModule(CapaMixin, XModule):
""" """
super(CapaModule, self).__init__(*args, **kwargs) super(CapaModule, self).__init__(*args, **kwargs)
def author_view(self, context):
"""
Renders the Studio preview view.
"""
return self.student_view(context)
def handle_ajax(self, dispatch, data): def handle_ajax(self, dispatch, data):
""" """
This is called by courseware.module_render, to handle an AJAX call. This is called by courseware.module_render, to handle an AJAX call.
...@@ -139,6 +145,7 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -139,6 +145,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
mako_template = "widgets/problem-edit.html" mako_template = "widgets/problem-edit.html"
js = {'coffee': [resource_string(__name__, 'js/src/problem/edit.coffee')]} js = {'coffee': [resource_string(__name__, 'js/src/problem/edit.coffee')]}
js_module_name = "MarkdownEditingDescriptor" js_module_name = "MarkdownEditingDescriptor"
has_author_view = True
css = { css = {
'scss': [ 'scss': [
resource_string(__name__, 'css/editor/edit.scss'), resource_string(__name__, 'css/editor/edit.scss'),
......
...@@ -43,7 +43,7 @@ class StudioEditableBlock(object): ...@@ -43,7 +43,7 @@ class StudioEditableBlock(object):
""" """
Helper method for getting preview view name (student_view or author_view) for a given module. Helper method for getting preview view name (student_view or author_view) for a given module.
""" """
return AUTHOR_VIEW if hasattr(block, AUTHOR_VIEW) else STUDENT_VIEW return AUTHOR_VIEW if has_author_view(block) else STUDENT_VIEW
StudioEditableModule = StudioEditableBlock StudioEditableModule = StudioEditableBlock
...@@ -58,3 +58,10 @@ class StudioEditableDescriptor(object): ...@@ -58,3 +58,10 @@ class StudioEditableDescriptor(object):
""" """
author_view = module_attr(AUTHOR_VIEW) author_view = module_attr(AUTHOR_VIEW)
has_author_view = True has_author_view = True
def has_author_view(descriptor):
"""
Returns True if the xmodule linked to the descriptor supports "author_view".
"""
return getattr(descriptor, 'has_author_view', False)
...@@ -1318,13 +1318,15 @@ class CapaModuleTest(unittest.TestCase): ...@@ -1318,13 +1318,15 @@ class CapaModuleTest(unittest.TestCase):
# Re-mock the module_id to a fixed string, so we can check the logging # Re-mock the module_id to a fixed string, so we can check the logging
module.location = Mock(module.location) module.location = Mock(module.location)
module.location.to_deprecated_string.return_value = 'i4x://edX/capa_test/problem/meh' module.location.to_deprecated_string.return_value = 'i4x://edX/capa_test/problem/meh'
module.get_problem_html()
module.get_demand_hint(0) with patch.object(module.runtime, 'publish') as mock_track_function:
module.runtime.track_function.assert_called_with( module.get_problem_html()
'edx.problem.hint.demandhint_displayed', module.get_demand_hint(0)
{'hint_index': 0, 'module_id': u'i4x://edX/capa_test/problem/meh', mock_track_function.assert_called_with(
'hint_text': 'Demand 1', 'hint_len': 2} module, 'edx.problem.hint.demandhint_displayed',
) {'hint_index': 0, 'module_id': u'i4x://edX/capa_test/problem/meh',
'hint_text': 'Demand 1', 'hint_len': 2}
)
def test_input_state_consistency(self): def test_input_state_consistency(self):
module1 = CapaFactory.create() module1 = CapaFactory.create()
...@@ -1638,11 +1640,11 @@ class CapaModuleTest(unittest.TestCase): ...@@ -1638,11 +1640,11 @@ class CapaModuleTest(unittest.TestCase):
unmasked names should appear in the track_function event_info. unmasked names should appear in the track_function event_info.
""" """
module = CapaFactory.create(xml=self.common_shuffle_xml) module = CapaFactory.create(xml=self.common_shuffle_xml)
with patch.object(module.runtime, 'track_function') as mock_track_function: with patch.object(module.runtime, 'publish') as mock_track_function:
get_request_dict = {CapaFactory.input_key(): 'choice_3'} # the correct choice get_request_dict = {CapaFactory.input_key(): 'choice_3'} # the correct choice
module.check_problem(get_request_dict) module.check_problem(get_request_dict)
mock_call = mock_track_function.mock_calls[0] mock_call = mock_track_function.mock_calls[1]
event_info = mock_call[1][1] event_info = mock_call[1][2]
self.assertEqual(event_info['answers'][CapaFactory.answer_key()], 'choice_3') self.assertEqual(event_info['answers'][CapaFactory.answer_key()], 'choice_3')
# 'permutation' key added to record how problem was shown # 'permutation' key added to record how problem was shown
self.assertEquals(event_info['permutation'][CapaFactory.answer_key()], self.assertEquals(event_info['permutation'][CapaFactory.answer_key()],
...@@ -1706,11 +1708,11 @@ class CapaModuleTest(unittest.TestCase): ...@@ -1706,11 +1708,11 @@ class CapaModuleTest(unittest.TestCase):
</problem> </problem>
""") """)
module = CapaFactory.create(xml=xml) module = CapaFactory.create(xml=xml)
with patch.object(module.runtime, 'track_function') as mock_track_function: with patch.object(module.runtime, 'publish') as mock_track_function:
get_request_dict = {CapaFactory.input_key(): 'choice_2'} # mask_X form when masking enabled get_request_dict = {CapaFactory.input_key(): 'choice_2'} # mask_X form when masking enabled
module.check_problem(get_request_dict) module.check_problem(get_request_dict)
mock_call = mock_track_function.mock_calls[0] mock_call = mock_track_function.mock_calls[1]
event_info = mock_call[1][1] event_info = mock_call[1][2]
self.assertEqual(event_info['answers'][CapaFactory.answer_key()], 'choice_2') self.assertEqual(event_info['answers'][CapaFactory.answer_key()], 'choice_2')
# 'permutation' key added to record how problem was shown # 'permutation' key added to record how problem was shown
self.assertEquals(event_info['permutation'][CapaFactory.answer_key()], self.assertEquals(event_info['permutation'][CapaFactory.answer_key()],
...@@ -2585,13 +2587,13 @@ class TestProblemCheckTracking(unittest.TestCase): ...@@ -2585,13 +2587,13 @@ class TestProblemCheckTracking(unittest.TestCase):
return CustomCapaFactory return CustomCapaFactory
def get_event_for_answers(self, module, answer_input_dict): def get_event_for_answers(self, module, answer_input_dict):
with patch.object(module.runtime, 'track_function') as mock_track_function: with patch.object(module.runtime, 'publish') as mock_track_function:
module.check_problem(answer_input_dict) module.check_problem(answer_input_dict)
self.assertGreaterEqual(len(mock_track_function.mock_calls), 1) self.assertGreaterEqual(len(mock_track_function.mock_calls), 2)
# There are potentially 2 track logs: answers and hint. [-1]=answers. # There are potentially 2 track logs: answers and hint. [-1]=answers.
mock_call = mock_track_function.mock_calls[-1] mock_call = mock_track_function.mock_calls[-1]
event = mock_call[1][1] event = mock_call[1][2]
return event return event
......
...@@ -18,6 +18,7 @@ dummy_render = lambda block, _: Fragment(block.data) # pylint: disable=invalid- ...@@ -18,6 +18,7 @@ dummy_render = lambda block, _: Fragment(block.data) # pylint: disable=invalid-
'xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.render', VanillaRuntime.render 'xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.render', VanillaRuntime.render
) )
@patch('xmodule.html_module.HtmlDescriptor.author_view', dummy_render, create=True) @patch('xmodule.html_module.HtmlDescriptor.author_view', dummy_render, create=True)
@patch('xmodule.html_module.HtmlDescriptor.has_author_view', True, create=True)
@patch('xmodule.x_module.DescriptorSystem.applicable_aside_types', lambda self, block: []) @patch('xmodule.x_module.DescriptorSystem.applicable_aside_types', lambda self, block: [])
class TestLibraryRoot(MixedSplitTestCase): class TestLibraryRoot(MixedSplitTestCase):
""" """
......
...@@ -121,7 +121,7 @@ class LibraryEditPageTest(StudioLibraryTest): ...@@ -121,7 +121,7 @@ class LibraryEditPageTest(StudioLibraryTest):
# Check that the save worked: # Check that the save worked:
self.assertEqual(len(self.lib_page.xblocks), 1) self.assertEqual(len(self.lib_page.xblocks), 1)
problem_block = self.lib_page.xblocks[0] problem_block = self.lib_page.xblocks[0]
self.assertIn("Laura Roslin", problem_block.student_content) self.assertIn("Laura Roslin", problem_block.author_content)
def test_no_discussion_button(self): def test_no_discussion_button(self):
""" """
......
...@@ -554,7 +554,14 @@ def get_module_system_for_user(user, student_data, # TODO # pylint: disable=to ...@@ -554,7 +554,14 @@ def get_module_system_for_user(user, student_data, # TODO # pylint: disable=to
if event_type == 'grade' and not is_masquerading_as_specific_student(user, course_id): if event_type == 'grade' and not is_masquerading_as_specific_student(user, course_id):
handle_grade_event(block, event_type, event) handle_grade_event(block, event_type, event)
else: else:
track_function(event_type, event) aside_context = {}
for aside in block.runtime.get_asides(block):
if hasattr(aside, 'get_event_context'):
aside_event_info = aside.get_event_context(event_type, event)
if aside_event_info is not None:
aside_context[aside.scope_ids.block_type] = aside_event_info
with tracker.get_tracker().context('asides', {'asides': aside_context}):
track_function(event_type, event)
def rebind_noauth_module_to_user(module, real_user): def rebind_noauth_module_to_user(module, real_user):
""" """
......
...@@ -23,7 +23,7 @@ from courseware.module_render import hash_resource ...@@ -23,7 +23,7 @@ from courseware.module_render import hash_resource
from xblock.field_data import FieldData from xblock.field_data import FieldData
from xblock.runtime import Runtime from xblock.runtime import Runtime
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from xblock.core import XBlock from xblock.core import XBlock, XBlockAside
from xblock.fragment import Fragment from xblock.fragment import Fragment
from capa.tests.response_xml_factory import OptionResponseXMLFactory from capa.tests.response_xml_factory import OptionResponseXMLFactory
...@@ -51,6 +51,7 @@ from xmodule.lti_module import LTIDescriptor ...@@ -51,6 +51,7 @@ from xmodule.lti_module import LTIDescriptor
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory, ToyCourseFactory, check_mongo_calls from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory, ToyCourseFactory, check_mongo_calls
from xmodule.modulestore.tests.test_asides import AsideTestType
from xmodule.x_module import XModuleDescriptor, XModule, STUDENT_VIEW, CombinedSystem from xmodule.x_module import XModuleDescriptor, XModule, STUDENT_VIEW, CombinedSystem
from openedx.core.djangoapps.credit.models import CreditCourse from openedx.core.djangoapps.credit.models import CreditCourse
...@@ -1703,9 +1704,36 @@ class TestModuleTrackingContext(SharedModuleStoreTestCase): ...@@ -1703,9 +1704,36 @@ class TestModuleTrackingContext(SharedModuleStoreTestCase):
module_info = self.handle_callback_and_get_module_info(mock_tracker, problem_display_name) module_info = self.handle_callback_and_get_module_info(mock_tracker, problem_display_name)
self.assertEquals(problem_display_name, module_info['display_name']) self.assertEquals(problem_display_name, module_info['display_name'])
def handle_callback_and_get_module_info(self, mock_tracker, problem_display_name=None): @XBlockAside.register_temp_plugin(AsideTestType, 'test_aside')
@patch('xmodule.modulestore.mongo.base.CachingDescriptorSystem.applicable_aside_types',
lambda self, block: ['test_aside'])
@patch('lms.djangoapps.lms_xblock.runtime.LmsModuleSystem.applicable_aside_types',
lambda self, block: ['test_aside'])
def test_context_contains_aside_info(self, mock_tracker):
""" """
Creates a fake module, invokes the callback and extracts the 'module' Check that related xblock asides populate information in the 'problem_check' event in case
the 'get_event_context' method is exist
"""
problem_display_name = u'Test Problem'
def get_event_context(self, event_type, event): # pylint: disable=unused-argument
"""
This method return data that should be associated with the "check_problem" event
"""
return {'content': 'test1', 'data_field': 'test2'}
AsideTestType.get_event_context = get_event_context
context_info = self.handle_callback_and_get_context_info(mock_tracker, problem_display_name)
self.assertIn('asides', context_info)
self.assertIn('test_aside', context_info['asides'])
self.assertIn('content', context_info['asides']['test_aside'])
self.assertEquals(context_info['asides']['test_aside']['content'], 'test1')
self.assertIn('data_field', context_info['asides']['test_aside'])
self.assertEquals(context_info['asides']['test_aside']['data_field'], 'test2')
def handle_callback_and_get_context_info(self, mock_tracker, problem_display_name=None):
"""
Creates a fake module, invokes the callback and extracts the 'context'
metadata from the emitted problem_check event. metadata from the emitted problem_check event.
""" """
descriptor_kwargs = { descriptor_kwargs = {
...@@ -1730,7 +1758,15 @@ class TestModuleTrackingContext(SharedModuleStoreTestCase): ...@@ -1730,7 +1758,15 @@ class TestModuleTrackingContext(SharedModuleStoreTestCase):
event = mock_call[1][0] event = mock_call[1][0]
self.assertEquals(event['event_type'], 'problem_check') self.assertEquals(event['event_type'], 'problem_check')
return event['context']['module'] return event['context']
def handle_callback_and_get_module_info(self, mock_tracker, problem_display_name=None):
"""
Creates a fake module, invokes the callback and extracts the 'module'
metadata from the emitted problem_check event.
"""
event = self.handle_callback_and_get_context_info(mock_tracker, problem_display_name)
return event['module']
def test_missing_display_name(self, mock_tracker): def test_missing_display_name(self, mock_tracker):
actual_display_name = self.handle_callback_and_get_module_info(mock_tracker)['display_name'] actual_display_name = self.handle_callback_and_get_module_info(mock_tracker)['display_name']
......
...@@ -281,4 +281,10 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method ...@@ -281,4 +281,10 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method
if block.scope_ids.block_type in config.disabled_blocks.split(): if block.scope_ids.block_type in config.disabled_blocks.split():
return [] return []
return super(LmsModuleSystem, self).applicable_aside_types() # TODO: aside_type != 'acid_aside' check should be removed once AcidBlock is only installed during tests
# (see https://openedx.atlassian.net/browse/TE-811)
return [
aside_type
for aside_type in super(LmsModuleSystem, self).applicable_aside_types(block)
if aside_type != 'acid_aside'
]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment