From 9792c93612bde698a8d77d0f26a3c682bb4870eb Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Wed, 9 Nov 2016 13:12:20 +1030 Subject: [PATCH 1/2] Uses Course Blocks API to populate the Instructor Tool's root_block_id drop-down. * Removes the python course tree traversal logic, and replaces it with Javascript. * Adds student_view_data() to downloadable block types to reveal the "question" text to the Course Blocks API --- problem_builder/answer.py | 7 + problem_builder/instructor_tool.py | 100 +-------- problem_builder/public/js/instructor_tool.js | 106 ++++++++++ problem_builder/questionnaire.py | 7 + .../templates/html/instructor_tool.html | 10 +- .../tests/unit/test_instructor_tool.py | 190 +----------------- 6 files changed, 139 insertions(+), 281 deletions(-) diff --git a/problem_builder/answer.py b/problem_builder/answer.py index a9631130..cc3fc0a1 100644 --- a/problem_builder/answer.py +++ b/problem_builder/answer.py @@ -260,6 +260,13 @@ def get_template(cls, template_id): return {'data': {'name': uuid.uuid4().hex[:7]}} return {'metadata': {}, 'data': {}} + def student_view_data(self): + """ + Returns a JSON representation of the student_view of this XBlock, + retrievable from the Course Block API. + """ + return {'question': self.question} + @XBlock.needs("i18n") class AnswerRecapBlock(AnswerMixin, StudioEditableXBlockMixin, XBlock): diff --git a/problem_builder/instructor_tool.py b/problem_builder/instructor_tool.py index c353e6f5..b4e0b7f3 100644 --- a/problem_builder/instructor_tool.py +++ b/problem_builder/instructor_tool.py @@ -34,6 +34,10 @@ PAGE_SIZE = 15 +# URL Path to the Course Blocks REST API. +# Note that we add a trailing slash to avoid the API's redirect hit. +COURSE_BLOCKS_API = '/api/courses/v1/blocks/' + # Make '_' a no-op so we can scrape strings def _(text): @@ -135,12 +139,11 @@ def student_view(self, context=None): _('Long Answer'): 'AnswerBlock', } - flat_block_tree = self._build_course_tree() - - html = loader.render_template( - 'templates/html/instructor_tool.html', - {'block_choices': block_choices, 'block_tree': flat_block_tree} - ) + html = loader.render_template('templates/html/instructor_tool.html', { + 'block_choices': block_choices, + 'course_blocks_api': COURSE_BLOCKS_API, + 'root_block_id': unicode(getattr(self.runtime, 'course_id', 'course_id')), + }) fragment = Fragment(html) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/instructor_tool.css')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/instructor_tool.js')) @@ -150,91 +153,6 @@ def student_view(self, context=None): fragment.initialize_js('InstructorToolBlock') return fragment - def _build_course_tree(self): - """ - Return flat tree of blocks belonging to this block's parent course. - """ - eligible_block_types = ('pb-mcq', 'pb-rating', 'pb-answer') - flat_block_tree = [] - - def get_block_id(block): - """ - Return ID of `block`, taking into account needs of both LMS/CMS and workbench runtimes. - """ - usage_id = block.scope_ids.usage_id - # Try accessing block ID. If usage_id does not have it, return usage_id itself - return unicode(getattr(usage_id, 'block_id', usage_id)) - - def get_block_name(block): - """ - Return name of `block`. - - Try attributes in the following order: - - block.question - - block.name (fallback for old courses) - - block.display_name - - block ID - """ - for attribute in ('question', 'name', 'display_name'): - if getattr(block, attribute, None): - return getattr(block, attribute, None) - return get_block_id(block) - - def get_block_type(block): - """ - Return type of `block`, taking into account different key styles that might be in use. - """ - try: - block_type = block.runtime.id_reader.get_block_type(block.scope_ids.def_id) - except AttributeError: - block_type = block.runtime.id_reader.get_block_type(block.scope_ids.usage_id) - return block_type - - def build_tree(block, ancestors): - """ - Build up a tree of information about the XBlocks descending from `block`. - """ - block_id = get_block_id(block) - block_name = get_block_name(block) - block_type = get_block_type(block) - if block_type != 'pb-choice': - eligible = block_type in eligible_block_types - if eligible: - # If this block is a question whose answers we can export, - # we mark all of its ancestors as exportable too - if ancestors and not ancestors[-1]["eligible"]: - for ancestor in ancestors: - ancestor["eligible"] = True - - new_entry = { - "depth": len(ancestors), - "id": block_id, - "name": block_name, - "eligible": eligible, - } - flat_block_tree.append(new_entry) - if block.has_children and not getattr(block, "has_dynamic_children", lambda: False)(): - for child_id in block.children: - build_tree(block.runtime.get_block(child_id), ancestors=(ancestors + [new_entry])) - - root_block = self - while root_block.parent: - root_block = root_block.get_parent() - root_block_id = get_block_id(root_block) - root_entry = { - "depth": 0, - "id": root_block_id, - "name": "All", - "eligible": False, - } - flat_block_tree.append(root_entry) - - for child_id in root_block.children: - child_block = root_block.runtime.get_block(child_id) - build_tree(child_block, [root_entry]) - - return flat_block_tree - @property def download_url_for_last_report(self): """ Get the URL for the last report, if any """ diff --git a/problem_builder/public/js/instructor_tool.js b/problem_builder/public/js/instructor_tool.js index 1a4dc74c..f9d58e99 100644 --- a/problem_builder/public/js/instructor_tool.js +++ b/problem_builder/public/js/instructor_tool.js @@ -250,6 +250,111 @@ function InstructorToolBlock(runtime, element) { if (statusChanged) updateView(); } + // Block types with answers we can export + var questionBlockTypes = ['pb-mcq', 'pb-rating', 'pb-answer']; + + // Fetch this course's blocks from the REST API, and add them to the + // list of blocks in the Section/Question drop-down list. + function getCourseBlocks() { + $.ajax({ + type: 'GET', + url: $rootBlockId.data('course-blocks-api'), + data: { + course_id: $element.data('course-id'), + requested_fields: 'name,display_name,block_type,children', + student_view_data: questionBlockTypes.join(','), + all_blocks: true, + depth: 'all' + }, + success: updateBlockOptions, + dataType: 'json' + }); + } + + // Appends the blocks returned by the Course Blocks API as options for + // the Section/Question drop-down list, arranged as a tree. + function updateBlockOptions(data) { + + // Constructs an