Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ dependencies:
- "pip install -r requirements.txt"
- "pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements/base.txt"
- "pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements/test.txt"
- "pip uninstall -y xblock-problem-builder && python setup.py sdist && pip install dist/xblock-problem-builder-2.6.1.tar.gz"
- "pip uninstall -y xblock-problem-builder && python setup.py sdist && pip install dist/xblock-problem-builder-2.6.2.tar.gz"
- "pip install -r test_requirements.txt"
- "mkdir var"
test:
Expand Down
7 changes: 7 additions & 0 deletions problem_builder/answer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
100 changes: 9 additions & 91 deletions problem_builder/instructor_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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'))
Expand All @@ -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 """
Expand Down
106 changes: 106 additions & 0 deletions problem_builder/public/js/instructor_tool.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pomegranited Typos: "Fetch this course's blocks ..., and add the ??? to the list of blocks ..." (perhaps you meant to say "and add them"?)

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pomegranited Nit: Move empty line up so it separates getCourseBlocks and updateBlockOptions functions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed with a3dd42f.

// Constructs an <option> element from the given block to add to the
// list of root blocks.
//
// Uses the block's:
// * question, name, or display name as the label.
// * depth in the course to indent the label, to make the tree
// structure more visible.
// * 'enabled' attribute to decide whether the <option>
// element is selectable, i.e. available as a download filter.
//
// Returns the <option> element so that it can be enabled later,
// if it's found to have a descendant that is enabled.
var appendBlock = function(block) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pomegranited You already added some nice comments explaining individual steps to the body of this function. It would be great to have a general comment that summarizes what this function does (and why it needs to return $option).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed with a3dd42f.

var blockId = block.id.split('+block@').pop(),
padding = Array(2*block.depth).join('&nbsp;'),
disabled = (block.enabled ? undefined : 'disabled'),
labelAttr,
label,
$option;

// Merge any fields exposed by student_view_data, so they can be
// candidates for the label attribute.
block = _.extend(block, block['student_view_data']);

// Find the best label attribute available for the block.
labelAttr = _.find(
['question', 'name', 'display_name'],
function(attr) {
return block[attr];
}
);
label = padding + (block[labelAttr] || blockId);
$option = $('<option>', {value: blockId, html: label, disabled: disabled});

$rootBlockId.append($option);
return $option;
},

// Builds the tree of course blocks.
buildTree = function(block, ancestors) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pomegranited Same nit here: Empty line above start of function definition would be nice :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed with a3dd42f.

// Omit pb-choice blocks
if (block.type == 'pb-choice') return;

// Enable the exportable blocks, and their ancestors.
if (_.contains(questionBlockTypes, block.type)) {
block.enabled = true;

for (var i = ancestors.length; i > 0; --i) {
var ancestor = ancestors[i-1];

// No need to continue; these ancestors are already enabled.
if (ancestor.enabled) break;

ancestor.enabled = true;
ancestor.element.removeAttr('disabled');
}
}

block.depth = ancestors.length;
block.element = appendBlock(block);

// Recurse over all the child blocks, including the current block as an ancestor.
var childAncestors = ancestors.concat([block]);
_.each(block.children, function(child_id) {
buildTree(data.blocks[child_id], childAncestors);
});
},
root = data.blocks[data.root];

// Label the root block as "All"
root.name = gettext('All');

// Remove any existing options
$rootBlockId.empty();

// Build the course blocks tree from the root.
buildTree(root, []);
}

function disableActions() {
$startButton.prop('disabled', true);
$cancelButton.prop('disabled', true);
Expand Down Expand Up @@ -369,6 +474,7 @@ function InstructorToolBlock(runtime, element) {

showSpinner();
disableActions();
getCourseBlocks();
getStatus();

}
7 changes: 7 additions & 0 deletions problem_builder/questionnaire.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,10 @@ def message_formatted(self):
format_html = getattr(self.runtime, 'replace_urls', lambda html: html)
return format_html(self.message)
return ""

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}
10 changes: 2 additions & 8 deletions problem_builder/templates/html/instructor_tool.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,8 @@ <h3>{% trans "Filters" %}</h3>
<div class="data-export-field">
<label>
<span>{% trans "Section/Question:" %}</span>
<select name="root_block_id">
{% for block in block_tree %}
<option value="{{ block.id }}"
{% if not block.eligible %} disabled="disabled" {% endif %}>
{% for _ in ""|ljust:block.depth %}&nbsp;&nbsp;{% endfor %}
{{ block.name }}
</option>
{% endfor %}
<select name="root_block_id" data-course-blocks-api="{{course_blocks_api}}">
<option value="{{root_block_id}}">{% trans "All" %}</option>
</select>
</label>
</div>
Expand Down
Loading