diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index f8781b841c..fc7cc49433 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -575,11 +575,13 @@ class LoncapaProblem(object): return {} @property - def has_responsive_ui(self): + def has_multi_device_support(self): """ - Returns whether this capa problem has support for responsive UI. + Returns whether this capa problem has multi-device support. """ - return all(responder.has_responsive_ui for responder in self.responders.values()) + return all( + responder.multi_device_support for responder in self.responders.values() + ) # ======= Private Methods Below ======== diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index c1b75c892c..0d7b830540 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -145,9 +145,9 @@ class LoncapaResponse(object): required_attributes = [] # Overridable field that specifies whether this capa response type has support for - # responsive UI, for rendering on devices of different sizes and shapes. + # for rendering on devices of different sizes and shapes. # By default, we set this to False, allowing subclasses to override as appropriate. - has_responsive_ui = False + multi_device_support = False def __init__(self, xml, inputfields, context, system, capa_module): """ @@ -808,7 +808,7 @@ class ChoiceResponse(LoncapaResponse): max_inputfields = 1 allowed_inputfields = ['checkboxgroup', 'radiogroup'] correct_choices = None - has_responsive_ui = True + multi_device_support = True def setup_response(self): self.assign_choice_names() @@ -993,7 +993,7 @@ class MultipleChoiceResponse(LoncapaResponse): max_inputfields = 1 allowed_inputfields = ['choicegroup'] correct_choices = None - has_responsive_ui = True + multi_device_support = True def setup_response(self): # call secondary setup for MultipleChoice questions, to set name @@ -1346,7 +1346,7 @@ class OptionResponse(LoncapaResponse): hint_tag = 'optionhint' allowed_inputfields = ['optioninput'] answer_fields = None - has_responsive_ui = True + multi_device_support = True def setup_response(self): self.answer_fields = self.inputfields @@ -1421,7 +1421,7 @@ class NumericalResponse(LoncapaResponse): allowed_inputfields = ['textline', 'formulaequationinput'] required_attributes = ['answer'] max_inputfields = 1 - has_responsive_ui = True + multi_device_support = True def __init__(self, *args, **kwargs): self.correct_answer = '' @@ -1645,7 +1645,7 @@ class StringResponse(LoncapaResponse): required_attributes = ['answer'] max_inputfields = 1 correct_answer = [] - has_responsive_ui = True + multi_device_support = True def setup_response_backward(self): self.correct_answer = [ diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index e27b704e40..8625573a0d 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -189,13 +189,6 @@ class CapaDescriptor(CapaFields, RawDescriptor): registered_tags = responsetypes.registry.registered_tags() return set([node.tag for node in tree.iter() if node.tag in registered_tags]) - @property - def has_responsive_ui(self): - """ - Returns whether this module has support for responsive UI. - """ - return self.lcp.has_responsive_ui - def index_dictionary(self): """ Return dictionary prepared with module content and type for indexing. @@ -211,6 +204,17 @@ class CapaDescriptor(CapaFields, RawDescriptor): result.update(index) return result + def has_support(self, view, functionality): + """ + Override the XBlock.has_support method to return appropriate + value for the multi-device functionality. + 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 + # Proxy to CapaModule for access to any of its attributes answer_available = module_attr('answer_available') check_button_name = module_attr('check_button_name') diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 5bc086109b..bfcbb49022 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -84,7 +84,9 @@ class HtmlModule(HtmlModuleMixin): """ Module for putting raw html in a course """ - pass + @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 @@ -95,7 +97,6 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): # pylint: d module_class = HtmlModule filename_extension = "xml" template_dir_name = "html" - has_responsive_ui = True show_in_read_only_mode = True js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]} diff --git a/lms/djangoapps/course_structure_api/v0/tests.py b/lms/djangoapps/course_structure_api/v0/tests.py index 42868abd29..3394450f79 100644 --- a/lms/djangoapps/course_structure_api/v0/tests.py +++ b/lms/djangoapps/course_structure_api/v0/tests.py @@ -80,7 +80,7 @@ class CourseViewTestsMixin(object): factory = MultipleChoiceResponseXMLFactory() args = {'choices': [False, True, False]} problem_xml = factory.build_xml(**args) - ItemFactory.create( + self.problem = ItemFactory.create( category="problem", parent_location=self.sequential.location, display_name="Problem 1", @@ -94,6 +94,12 @@ class CourseViewTestsMixin(object): display_name="Video 1", ) + self.html = ItemFactory.create( + category="html", + parent_location=self.sequential.location, + display_name="HTML 1", + ) + self.empty_course = CourseFactory.create( start=datetime(2014, 6, 16, 14, 30), end=datetime(2015, 1, 16), @@ -457,7 +463,7 @@ class CourseBlocksOrNavigationTestMixin(CourseDetailTestMixin, CourseViewTestsMi blocks = response.data[self.block_navigation_view_type] # verify number of blocks - self.assertEquals(len(blocks), 4) + self.assertEquals(len(blocks), 5) # verify fields in blocks for field, block in product(self.block_fields, blocks.values()): @@ -519,6 +525,23 @@ class CourseBlocksTestMixin(object): self.assertIn('problem', root_block['block_count']) self.assertEquals(root_block['block_count']['problem'], 1) + def test_multi_device_support(self): + """ + Verifies the view's response when multi_device support is requested. + """ + response = self.http_get_for_course( + data={'fields': 'multi_device'} + ) + self.assertEquals(response.status_code, 200) + + for block, expected_multi_device_support in ( + (self.problem, True), + (self.html, True), + (self.video, False) + ): + block_response = response.data[self.block_navigation_view_type][unicode(block.location)] + self.assertEquals(block_response['multi_device'], expected_multi_device_support) + class CourseNavigationTestMixin(object): """ @@ -535,7 +558,7 @@ class CourseNavigationTestMixin(object): ) root_block = response.data[self.block_navigation_view_type][unicode(self.course.location)] self.assertIn('descendants', root_block) - self.assertEquals(len(root_block['descendants']), 3) + self.assertEquals(len(root_block['descendants']), 4) def test_depth(self): """ @@ -545,7 +568,7 @@ class CourseNavigationTestMixin(object): container_descendants = ( (self.course.location, 1), - (self.sequential.location, 2), + (self.sequential.location, 3), ) for container_location, expected_num_descendants in container_descendants: block = response.data[self.block_navigation_view_type][unicode(container_location)] diff --git a/lms/djangoapps/course_structure_api/v0/views.py b/lms/djangoapps/course_structure_api/v0/views.py index 7fdf8c3cf4..b541dd746c 100644 --- a/lms/djangoapps/course_structure_api/v0/views.py +++ b/lms/djangoapps/course_structure_api/v0/views.py @@ -321,7 +321,7 @@ class CourseBlocksAndNavigation(ListAPIView): GET api/course_structure/v0/courses/{course_id}/blocks+navigation/ &block_count=video &block_json={"video":{"profiles":["mobile_low"]}} - &fields=graded,format,responsive_ui + &fields=graded,format,multi_device **Parameters**: @@ -335,9 +335,9 @@ class CourseBlocksAndNavigation(ListAPIView): Example: block_count="video,problem" * fields: (list) Indicates which additional fields to return for each block. - Default is "children,graded,format,responsive_ui" + Default is "children,graded,format,multi_device" - Example: fields=graded,format,responsive_ui + Example: fields=graded,format,multi_device * navigation_depth (integer) Indicates how far deep to traverse into the course hierarchy before bundling all the descendants. @@ -387,8 +387,9 @@ class CourseBlocksAndNavigation(ListAPIView): Possible values can be "Homework", "Lab", "Midterm Exam", and "Final Exam". Returned only if "format" is included in the "fields" parameter. - * responsive_ui: (boolean) Whether or not the block's rendering obtained via block_url is responsive. - Returned only if "responsive_ui" is included in the "fields" parameter. + * multi_device: (boolean) Whether or not the block's rendering obtained via block_url has support + for multiple devices. + Returned only if "multi_device" is included in the "fields" parameter. * navigation: A dictionary that maps block IDs to a collection of navigation information about each block. Each block contains the following fields. Returned only if using the "navigation" endpoint. @@ -405,7 +406,7 @@ class CourseBlocksAndNavigation(ListAPIView): """ A class for encapsulating the request information, including what optional fields are requested. """ - DEFAULT_FIELDS = "children,graded,format,responsive_ui" + DEFAULT_FIELDS = "children,graded,format,multi_device" def __init__(self, request, course): self.request = request @@ -417,10 +418,6 @@ class CourseBlocksAndNavigation(ListAPIView): # fields self.fields = set(request.GET.get('fields', self.DEFAULT_FIELDS).split(",")) - # children - self.children = 'children' in self.fields - self.fields.discard('children') - # block_count self.block_count = request.GET.get('block_count', "") self.block_count = ( @@ -489,7 +486,7 @@ class CourseBlocksAndNavigation(ListAPIView): self.descendants_of_parent = parent_block_info.descendants_of_self # add ourselves to the parent's children, if requested. - if request_info.children: + if 'children' in request_info.fields: parent_block_info.value.setdefault("children", []).append(unicode(block.location)) # the block's data to include in the response @@ -592,6 +589,13 @@ class CourseBlocksAndNavigation(ListAPIView): # block JSON data self.add_block_json(request_info, block_info) + # multi-device support + if 'multi_device' in request_info.fields: + block_info.value['multi_device'] = block_info.block.has_support( + getattr(block_info.block, 'student_view', None), + 'multi_device' + ) + # additional fields self.add_additional_fields(request_info, block_info) @@ -667,7 +671,6 @@ class CourseBlocksAndNavigation(ListAPIView): FIELD_MAP = { 'graded': BlockApiField(block_field_name='graded', api_field_default=False), 'format': BlockApiField(block_field_name='format', api_field_default=None), - 'responsive_ui': BlockApiField(block_field_name='has_responsive_ui', api_field_default=False), } def add_additional_fields(self, request_info, block_info): diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 95ff5c15ee..c6ee7ccd9f 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -34,7 +34,7 @@ git+https://github.com/hmarr/django-debug-toolbar-mongo.git@b0686a76f1ce3532088c git+https://github.com/edx/rfc6266.git@v0.0.5-edx#egg=rfc6266==0.0.5-edx # Our libraries: --e git+https://github.com/edx/XBlock.git@017b46b80e712b1318379912257cf1cc1b68eb0e#egg=XBlock +-e git+https://github.com/edx/XBlock.git@d1ff8cf31a9b94916ce06ba06d4176bd72e15768#egg=XBlock -e git+https://github.com/edx/codejail.git@6b17c33a89bef0ac510926b1d7fea2748b73aadd#egg=codejail -e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool -e git+https://github.com/edx/event-tracking.git@0.2.0#egg=event-tracking