Commit 06980764 by Nimisha Asthagiri Committed by J. Cliff Dyer

Transformer: StudentViewTransformer

parent ced7fde3
......@@ -574,15 +574,6 @@ class LoncapaProblem(object):
log.warning("Could not find matching input for id: %s", input_id)
return {}
@property
def has_multi_device_support(self):
"""
Returns whether this capa problem has multi-device support.
"""
return all(
responder.multi_device_support for responder in self.responders.values()
)
# ======= Private Methods Below ========
def _process_includes(self):
......
......@@ -230,9 +230,11 @@ class CapaDescriptor(CapaFields, RawDescriptor):
Returns whether the given view has support for the given functionality.
"""
if functionality == "multi_device":
return self.lcp.has_multi_device_support
else:
return False
return all(
responsetypes.registry.get_class_for_tag(tag).multi_device_support
for tag in self.problem_types
)
return False
# Proxy to CapaModule for access to any of its attributes
answer_available = module_attr('answer_available')
......
......@@ -20,6 +20,7 @@ from xmodule.x_module import XModule, DEPRECATION_VSCOMPAT_EVENT
from xmodule.xml_module import XmlDescriptor, name_to_pathname
from xblock.core import XBlock
from xblock.fields import Scope, String, Boolean, List
from xblock.fragment import Fragment
log = logging.getLogger("edx.courseware")
......@@ -28,7 +29,12 @@ log = logging.getLogger("edx.courseware")
_ = lambda text: text
class HtmlFields(object):
class HtmlBlock(object):
"""
This will eventually subclass XBlock and merge HtmlModule and HtmlDescriptor
into one. For now, it's a place to put the pieces that are already sharable
between the two (field information and XBlock handlers).
"""
display_name = String(
display_name=_("Display Name"),
help=_("This name appears in the horizontal navigation at the top of the page."),
......@@ -38,14 +44,20 @@ class HtmlFields(object):
default=_("Text")
)
data = String(help=_("Html contents to display for this module"), default=u"", scope=Scope.content)
source_code = String(help=_("Source code for LaTeX documents. This feature is not well-supported."), scope=Scope.settings)
source_code = String(
help=_("Source code for LaTeX documents. This feature is not well-supported."),
scope=Scope.settings
)
use_latex_compiler = Boolean(
help=_("Enable LaTeX templates?"),
default=False,
scope=Scope.settings
)
editor = String(
help=_("Select Visual to enter content and have the editor automatically create the HTML. Select Raw to edit HTML directly. If you change this setting, you must save the component and then re-open it for editing."),
help=_(
"Select Visual to enter content and have the editor automatically create the HTML. Select Raw to edit "
"HTML directly. If you change this setting, you must save the component and then re-open it for editing."
),
display_name=_("Editor"),
default="visual",
values=[
......@@ -55,8 +67,25 @@ class HtmlFields(object):
scope=Scope.settings
)
@XBlock.supports("multi_device")
def student_view(self, _context):
"""
Return a fragment that contains the html for the student view
"""
return Fragment(self.get_html())
class HtmlModuleMixin(HtmlFields, XModule):
def get_html(self):
"""
When we switch this to an XBlock, we can merge this with student_view,
but for now the XModule mixin requires that this method be defined.
"""
# pylint: disable=no-member
if self.system.anonymous_student_id:
return self.data.replace("%%USER_ID%%", self.system.anonymous_student_id)
return self.data
class HtmlModuleMixin(HtmlBlock, XModule):
"""
Attributes and methods used by HtmlModules internally.
"""
......@@ -74,23 +103,15 @@ class HtmlModuleMixin(HtmlFields, XModule):
js_module_name = "HTMLModule"
css = {'scss': [resource_string(__name__, 'css/html/display.scss')]}
def get_html(self):
if self.system.anonymous_student_id:
return self.data.replace("%%USER_ID%%", self.system.anonymous_student_id)
return self.data
@edxnotes
class HtmlModule(HtmlModuleMixin):
"""
Module for putting raw html in a course
"""
@XBlock.supports("multi_device")
def student_view(self, context):
return super(HtmlModule, self).student_view(context)
class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # pylint: disable=abstract-method
class HtmlDescriptor(HtmlBlock, XmlDescriptor, EditingDescriptor): # pylint: disable=abstract-method
"""
Module for putting raw html in a course
"""
......@@ -107,28 +128,31 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # pylint: d
# VS[compat] TODO (cpennington): Delete this method once all fall 2012 course
# are being edited in the cms
@classmethod
def backcompat_paths(cls, path):
def backcompat_paths(cls, filepath):
"""
Get paths for html and xml files.
"""
dog_stats_api.increment(
DEPRECATION_VSCOMPAT_EVENT,
tags=["location:html_descriptor_backcompat_paths"]
)
if path.endswith('.html.xml'):
path = path[:-9] + '.html' # backcompat--look for html instead of xml
if path.endswith('.html.html'):
path = path[:-5] # some people like to include .html in filenames..
if filepath.endswith('.html.xml'):
filepath = filepath[:-9] + '.html' # backcompat--look for html instead of xml
if filepath.endswith('.html.html'):
filepath = filepath[:-5] # some people like to include .html in filenames..
candidates = []
while os.sep in path:
candidates.append(path)
_, _, path = path.partition(os.sep)
while os.sep in filepath:
candidates.append(filepath)
_, _, filepath = filepath.partition(os.sep)
# also look for .html versions instead of .xml
nc = []
new_candidates = []
for candidate in candidates:
if candidate.endswith('.xml'):
nc.append(candidate[:-4] + '.html')
return candidates + nc
new_candidates.append(candidate[:-4] + '.html')
return candidates + new_candidates
@classmethod
def filter_templates(cls, template, course):
......@@ -217,8 +241,8 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # pylint: d
break
try:
with system.resources_fs.open(filepath) as file:
html = file.read().decode('utf-8')
with system.resources_fs.open(filepath) as infile:
html = infile.read().decode('utf-8')
# Log a warning if we can't parse the file, but don't error
if not check_html(html) and len(html) > 0:
msg = "Couldn't parse html in {0}, content = {1}".format(filepath, html)
......
......@@ -776,11 +776,13 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
"""
return edxval_api.get_video_info_for_course_and_profiles(unicode(course_id), video_profile_names)
def student_view_json(self, context):
def student_view_data(self, context=None):
"""
Returns a JSON representation of the student_view of this XModule.
The contract of the JSON content is between the caller and the particular XModule.
"""
context = context or {}
# If the "only_on_web" field is set on this video, do not return the rest of the video's data
# in this json view, since this video is to be accessed only through its web view."
if self.only_on_web:
......@@ -791,7 +793,7 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
# Check in VAL data first if edx_video_id exists
if self.edx_video_id:
video_profile_names = context.get("profiles", [])
video_profile_names = context.get("profiles", ["mobile_low"])
# get and cache bulk VAL data for course
val_course_data = self.get_cached_val_data_for_course(video_profile_names, self.location.course_key)
......
"""
Student View Transformer
"""
from openedx.core.lib.block_cache.transformer import BlockStructureTransformer
class StudentViewTransformer(BlockStructureTransformer):
"""
Only show information that is appropriate for a learner
"""
VERSION = 1
STUDENT_VIEW_DATA = 'student_view_data'
STUDENT_VIEW_MULTI_DEVICE = 'student_view_multi_device'
def __init__(self, requested_student_view_data=None):
self.requested_student_view_data = requested_student_view_data or []
@classmethod
def name(cls):
return "blocks_api:student_view"
@classmethod
def collect(cls, block_structure):
"""
Collect student_view_multi_device and student_view_data values for each block
"""
# collect basic xblock fields
block_structure.request_xblock_fields('category')
for block_key in block_structure.topological_traversal():
block = block_structure.get_xblock(block_key)
# We're iterating through descriptors (not bound to a user) that are
# given to us by the modulestore. The reason we look at
# block.__class__ is to avoid the XModuleDescriptor -> XModule
# proxying that would happen if we just examined block directly,
# since it's likely that student_view() is going to be defined on
# the XModule side.
#
# If that proxying happens, this method will throw an
# UndefinedContext exception, because we haven't initialized any of
# the user-specific context.
#
# This isn't a problem for pure XBlocks, because it's all in one
# class, and there's no proxying. So basically, if you encounter a
# problem where your particular XModule explodes here (and don't
# have the time to convert it to an XBlock), please try refactoring
# so that you declare your student_view() method in a common
# ancestor class of both your Descriptor and Module classes. As an
# example, I changed the name of HtmlFields to HtmlBlock and moved
# student_view() from HtmlModuleMixin to HtmlBlock.
student_view = getattr(block.__class__, 'student_view', None)
supports_multi_device = block.has_support(student_view, 'multi_device')
block_structure.set_transformer_block_field(
block_key,
cls,
cls.STUDENT_VIEW_MULTI_DEVICE,
supports_multi_device,
)
if getattr(block, 'student_view_data', None):
student_view_data = block.student_view_data()
block_structure.set_transformer_block_field(
block_key,
cls,
cls.STUDENT_VIEW_DATA,
student_view_data,
)
def transform(self, usage_info, block_structure): # pylint: disable=unused-argument
"""
Mutates block_structure based on the given usage_info.
"""
for block_key in block_structure.post_order_traversal():
if block_structure.get_xblock_field(block_key, 'category') not in self.requested_student_view_data:
block_structure.remove_transformer_block_field(block_key, self, self.STUDENT_VIEW_DATA)
"""
Tests for StudentViewTransformer.
"""
# pylint: disable=protected-access
from openedx.core.lib.block_cache.block_structure_factory import BlockStructureFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import ToyCourseFactory
from ..student_view import StudentViewTransformer
class TestStudentViewTransformer(ModuleStoreTestCase):
"""
Test proper behavior for StudentViewTransformer
"""
def setUp(self):
super(TestStudentViewTransformer, self).setUp()
self.course_key = ToyCourseFactory.create().id
self.course_usage_key = self.store.make_course_usage_key(self.course_key)
self.block_structure = BlockStructureFactory.create_from_modulestore(self.course_usage_key, self.store)
def test_transform(self):
# collect phase
StudentViewTransformer.collect(self.block_structure)
self.block_structure._collect_requested_xblock_fields()
# transform phase
StudentViewTransformer('video').transform(usage_info=None, block_structure=self.block_structure)
# verify video data
video_block_key = self.course_key.make_usage_key('video', 'sample_video')
self.assertIsNotNone(
self.block_structure.get_transformer_block_field(
video_block_key, StudentViewTransformer, StudentViewTransformer.STUDENT_VIEW_DATA,
)
)
self.assertFalse(
self.block_structure.get_transformer_block_field(
video_block_key, StudentViewTransformer, StudentViewTransformer.STUDENT_VIEW_MULTI_DEVICE,
)
)
# verify html data
html_block_key = self.course_key.make_usage_key('html', 'toyhtml')
self.assertIsNone(
self.block_structure.get_transformer_block_field(
html_block_key, StudentViewTransformer, StudentViewTransformer.STUDENT_VIEW_DATA,
)
)
self.assertTrue(
self.block_structure.get_transformer_block_field(
html_block_key, StudentViewTransformer, StudentViewTransformer.STUDENT_VIEW_MULTI_DEVICE,
)
)
......@@ -658,8 +658,8 @@ class CourseBlocksAndNavigation(ListAPIView):
method, add the response from the 'student_view_json" method as the data for the block.
"""
if block_info.type in request_info.block_json:
if getattr(block_info.block, 'student_view_json', None):
block_info.value["block_json"] = block_info.block.student_view_json(
if getattr(block_info.block, 'student_view_data', None):
block_info.value["block_json"] = block_info.block.student_view_data(
context=request_info.block_json[block_info.type]
)
......
......@@ -869,7 +869,7 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
@ddt.ddt
class TestVideoDescriptorStudentViewJson(TestCase):
"""
Tests for the student_view_json method on VideoDescriptor.
Tests for the student_view_data method on VideoDescriptor.
"""
TEST_DURATION = 111.0
TEST_PROFILE = "mobile"
......@@ -914,15 +914,15 @@ class TestVideoDescriptorStudentViewJson(TestCase):
def get_result(self, allow_cache_miss=True):
"""
Returns the result from calling the video's student_view_json method.
Returns the result from calling the video's student_view_data method.
Arguments:
allow_cache_miss is passed in the context to the student_view_json method.
allow_cache_miss is passed in the context to the student_view_data method.
"""
context = {
"profiles": [self.TEST_PROFILE],
"allow_cache_miss": "True" if allow_cache_miss else "False"
}
return self.video.student_view_json(context)
return self.video.student_view_data(context)
def verify_result_with_fallback_url(self, result):
"""
......
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