Commit dd04ee8c by Tim Krones

Update plots when navigating to next step to make sure they are always up-to-date.

parent fe2b7258
......@@ -1062,6 +1062,11 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
'active_step': self.active_step
}
def author_preview_view(self, context):
context = context.copy() if context else {}
context['author_preview_view'] = True
return super(MentoringWithExplicitStepsBlock, self).author_preview_view(context)
def author_edit_view(self, context):
"""
Add some HTML to the author view that allows authors to add child blocks.
......
......@@ -44,6 +44,7 @@ def _(text):
@XBlock.needs('i18n')
@XBlock.wants('user')
class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, XBlock):
"""
XBlock that displays plot that summarizes answers to scale questions.
......@@ -141,32 +142,56 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
@property
def default_claims(self):
if not self.claims:
return json.dumps([])
return []
course_id = unicode(getattr(self.runtime, 'course_id', 'course_id'))
course_key = CourseKey.from_string(course_id)
course_key_str = unicode(course_key)
user_service = self.runtime.service(self, 'user')
user = user_service.get_current_user()
username = user.opt_attrs.get('edx-platform.username')
anonymous_user_id = user_service.get_anonymous_user_id(username, course_id)
mentoring_block = self.get_parent().get_parent()
question_ids, questions = mentoring_block.question_ids, mentoring_block.questions
claims = []
for line in self.claims.split('\n'):
claim, q1, q2 = line.split(', ')
r1, r2 = None, None
for step in mentoring_block.steps:
for student_result in step.student_results:
child_name, child_result = student_result
if child_name == q1:
r1 = child_result['submission']
if child_name == q2: # Don't use "elif" here (would break cases in which q1 == q2)
r2 = child_result['submission']
if r1 is not None and r2 is not None:
break
for question_id, question in zip(question_ids, questions):
if question.name == q1:
r1 = self._default_response(course_key_str, question, question_id, anonymous_user_id)
if question.name == q2:
r2 = self._default_response(course_key_str, question, question_id, anonymous_user_id)
if r1 is not None and r2 is not None:
break
claims.append([claim, r1, r2])
return json.dumps(claims)
return claims
def _default_response(self, course_key_str, question, question_id, user_id):
from .tasks import _get_answer # Import here to avoid circular dependency
# 1. Obtain block_type for question
question_type = question.scope_ids.block_type
# 2. Obtain submissions for question using course_key_str, block_id, block_type, user_id
student_dict = {
'student_id': user_id,
'course_id': course_key_str,
'item_id': question_id,
'item_type': question_type,
}
submissions = sub_api.get_submissions(student_dict, limit=1) # Gets latest submission
# 3. Extract response from latest submission for question
answer_cache = {}
for submission in submissions:
answer = _get_answer(question, submission, answer_cache)
return int(answer)
@property
def average_claims(self):
if not self.claims:
return json.dumps([])
return []
course_id = unicode(getattr(self.runtime, 'course_id', 'course_id'))
course_key = CourseKey.from_string(course_id)
......@@ -187,14 +212,14 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
break
claims.append([claim, r1, r2])
return json.dumps(claims)
return claims
def _average_response(self, course_key_str, question, question_id):
from .tasks import _get_answer # Import here to avoid circular dependency
# 1. Obtain block_type for question
question_type = question.scope_ids.block_type
# 2. Obtain submissions for question using course_key_str, block_id, block_type
submissions = sub_api.get_all_submissions(course_key_str, question_id, question_type)
submissions = sub_api.get_all_submissions(course_key_str, question_id, question_type) # Gets latest submissions
# 3. Extract responses from submissions for question and sum them up
answer_cache = {}
response_total = 0
......@@ -207,6 +232,19 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
if num_submissions:
return response_total / float(num_submissions)
def default_claims_json(self):
return json.dumps(self.default_claims)
def average_claims_json(self):
return json.dumps(self.average_claims)
@XBlock.json_handler
def get_data(self, data, suffix):
return {
'default_claims': self.default_claims,
'average_claims': self.average_claims,
}
def clean_studio_edits(self, data):
# FIXME: Use this to clean data.claims (remove leading/trailing whitespace, etc.)
pass
......
......@@ -303,6 +303,7 @@ function MentoringWithStepsBlock(runtime, element) {
function showActiveStep() {
var step = steps[activeStep];
step.updatePlots();
$(step.element).show();
}
......
......@@ -57,7 +57,6 @@ function PlotBlock(runtime, element) {
var defaultClaims = $('.plot-default', element).data('claims');
var averageClaims = $('.plot-average', element).data('claims');
// Colors
var defaultColor = $('.plot-default', element).data('point-color');
......@@ -75,48 +74,60 @@ function PlotBlock(runtime, element) {
var defaultButton = $('.plot-default', element);
var averageButton = $('.plot-average', element);
function toggleOverlay(claims, color, klass) {
function toggleOverlay(claims, color, klass, refresh) {
var selector = "." + klass;
var selection = svgContainer.selectAll(selector);
if (selection.empty()) {
svgContainer.selectAll(selector)
.data(claims)
.enter()
.append("circle")
.attr("class", klass)
.attr("title", function(d) {
return d[0] + ": " + d[1] + ", " + d[2];
})
.attr("cx", function(d) {
return xScale(d[1]);
})
.attr("cy", function(d) {
return yScale(d[2]);
})
.attr("r", 5)
.style("fill", color);
showOverlay(selection, claims, color, klass);
} else {
selection.remove();
hideOverlay(selection);
if (refresh) {
toggleOverlay(claims, color, klass);
}
}
}
function toggleBorderColor(button, color) {
function showOverlay(selection, claims, color, klass) {
selection
.data(claims)
.enter()
.append("circle")
.attr("class", klass)
.attr("title", function(d) {
return d[0] + ": " + d[1] + ", " + d[2];
})
.attr("cx", function(d) {
return xScale(d[1]);
})
.attr("cy", function(d) {
return yScale(d[2]);
})
.attr("r", 5)
.style("fill", color);
}
function hideOverlay(selection) {
selection.remove();
}
function toggleBorderColor(button, color, refresh) {
var $button = $(button);
var overlayOn = $button.data("overlay-on");
if (overlayOn) {
if (overlayOn && !refresh) {
$button.css("border-color", "rgb(237, 237, 237)"); // Default color: grey
$button.data("overlay-on", false);
} else {
$button.css("border-color", color);
$button.data("overlay-on", true);
}
$button.data("overlay-on", !overlayOn);
}
defaultButton.on('click', function() {
toggleOverlay(defaultClaims, defaultColor, 'claim-default');
toggleBorderColor(this, defaultColor);
defaultButton.on('click', function(event, refresh) {
toggleOverlay(defaultClaims, defaultColor, 'claim-default', refresh);
toggleBorderColor(this, defaultColor, refresh);
});
averageButton.on('click', function() {
averageButton.on('click', function(event) {
toggleOverlay(averageClaims, averageColor, 'claim-average');
toggleBorderColor(this, averageColor);
});
......@@ -163,12 +174,43 @@ function PlotBlock(runtime, element) {
toggleQuadrantLabels();
});
// Show default overlay initially
defaultButton.trigger('click');
// Quadrant labels are off initially; color of button for toggling them should reflect this
quadrantsButton.css("border-color", "red");
// Functions that can be called from the outside
var dataXHR;
return {
update: function() {
var handlerUrl = runtime.handlerUrl(element, 'get_data');
if (dataXHR) {
dataXHR.abort();
}
dataXHR = $.post(handlerUrl, JSON.stringify({}))
.success(function(response) {
defaultClaims = response.default_claims;
averageClaims = response.average_claims;
// Default overlay should be visible initially.
// Might still be visible from a previous attempt;
// in that case, we refresh it:
defaultButton.trigger('click', 'refresh');
// Average overlay should be hidden initially.
// This is the default when (re-)loading the page from scratch.
// However, the overlay might still be visible from a previous attempt;
// in that case, we hide it:
var selection = svgContainer.selectAll('.claim-average');
if (!selection.empty()) {
hideOverlay(selection);
toggleBorderColor(averageButton, averageColor);
}
});
}
};
}
function MentoringStepBlock(runtime, element) {
var children = runtime.children(element);
var plots = [];
for (var i = 0; i < children.length; i++) {
var child = children[i];
var blockType = $(child.element).data('block-type');
if (blockType === 'sb-plot') {
plots.push(child);
}
}
var submitXHR, resultsXHR;
function callIfExists(obj, fn) {
......@@ -89,6 +98,15 @@ function MentoringStepBlock(runtime, element) {
hasQuestion: function() {
return $('.sb-step', element).data('has-question')
},
updatePlots: function() {
if (plots) {
for (var i = 0; i < plots.length; i++) {
var plot = plots[i];
plot.update();
}
}
}
};
......
......@@ -225,16 +225,14 @@ class MentoringStepBlock(
fragment.initialize_js('StepEdit')
return fragment
def student_view(self, context=None):
""" Student View """
return self._render_view(context, 'student_view')
def mentoring_view(self, context=None):
""" Mentoring View """
return self._render_view(context, 'mentoring_view')
def _render_view(self, context, view):
""" Actually renders a view """
rendering_for_studio = context.get('author_preview_view')
fragment = Fragment()
child_contents = []
......@@ -243,10 +241,18 @@ class MentoringStepBlock(
if child is None: # child should not be None but it can happen due to bugs or permission issues
child_contents.append(u"<p>[{}]</p>".format(self._(u"Error: Unable to load child component.")))
else:
child_fragment = self._render_child_fragment(child, context, view)
fragment.add_frag_resources(child_fragment)
child_contents.append(child_fragment.content)
if rendering_for_studio and isinstance(child, PlotBlock):
# Don't use view to render plot blocks in Studio.
# This is necessary because:
# - student_view of plot block uses submissions API to retrieve results,
# which causes "SubmissionRequestError" in Studio.
# - author_preview_view does not supply JS code for plot that JS code for step depends on
# (step calls "update" on plot to get latest data during rendering).
child_contents.append(u"<p>{}</p>".format(child.display_name))
else:
child_fragment = self._render_child_fragment(child, context, view)
fragment.add_frag_resources(child_fragment)
child_contents.append(child_fragment.content)
fragment.add_content(loader.render_template('templates/html/step.html', {
'self': self,
......
......@@ -18,14 +18,14 @@
<input type="button"
class="plot-default"
data-claims="{{ self.default_claims }}"
data-claims="{{ self.default_claims_json }}"
data-point-color="{{ self.point_color_default }}"
data-overlay-on="false"
value="{{ self.plot_label }}"
/>
<input type="button"
class="plot-average"
data-claims="{{ self.average_claims }}"
data-claims="{{ self.average_claims_json }}"
data-point-color="{{ self.point_color_average }}"
data-overlay-on="false"
value="Average"
......
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