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 ...@@ -1062,6 +1062,11 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
'active_step': self.active_step '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): def author_edit_view(self, context):
""" """
Add some HTML to the author view that allows authors to add child blocks. Add some HTML to the author view that allows authors to add child blocks.
......
...@@ -44,6 +44,7 @@ def _(text): ...@@ -44,6 +44,7 @@ def _(text):
@XBlock.needs('i18n') @XBlock.needs('i18n')
@XBlock.wants('user')
class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, XBlock): class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, XBlock):
""" """
XBlock that displays plot that summarizes answers to scale questions. XBlock that displays plot that summarizes answers to scale questions.
...@@ -141,32 +142,56 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin ...@@ -141,32 +142,56 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
@property @property
def default_claims(self): def default_claims(self):
if not self.claims: 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() mentoring_block = self.get_parent().get_parent()
question_ids, questions = mentoring_block.question_ids, mentoring_block.questions
claims = [] claims = []
for line in self.claims.split('\n'): for line in self.claims.split('\n'):
claim, q1, q2 = line.split(', ') claim, q1, q2 = line.split(', ')
r1, r2 = None, None r1, r2 = None, None
for step in mentoring_block.steps: for question_id, question in zip(question_ids, questions):
for student_result in step.student_results: if question.name == q1:
child_name, child_result = student_result r1 = self._default_response(course_key_str, question, question_id, anonymous_user_id)
if child_name == q1: if question.name == q2:
r1 = child_result['submission'] r2 = self._default_response(course_key_str, question, question_id, anonymous_user_id)
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
if r1 is not None and r2 is not None: if r1 is not None and r2 is not None:
break break
claims.append([claim, r1, r2]) 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 @property
def average_claims(self): def average_claims(self):
if not self.claims: if not self.claims:
return json.dumps([]) return []
course_id = unicode(getattr(self.runtime, 'course_id', 'course_id')) course_id = unicode(getattr(self.runtime, 'course_id', 'course_id'))
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
...@@ -187,14 +212,14 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin ...@@ -187,14 +212,14 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
break break
claims.append([claim, r1, r2]) claims.append([claim, r1, r2])
return json.dumps(claims) return claims
def _average_response(self, course_key_str, question, question_id): def _average_response(self, course_key_str, question, question_id):
from .tasks import _get_answer # Import here to avoid circular dependency from .tasks import _get_answer # Import here to avoid circular dependency
# 1. Obtain block_type for question # 1. Obtain block_type for question
question_type = question.scope_ids.block_type question_type = question.scope_ids.block_type
# 2. Obtain submissions for question using course_key_str, block_id, 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 # 3. Extract responses from submissions for question and sum them up
answer_cache = {} answer_cache = {}
response_total = 0 response_total = 0
...@@ -207,6 +232,19 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin ...@@ -207,6 +232,19 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
if num_submissions: if num_submissions:
return response_total / float(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): def clean_studio_edits(self, data):
# FIXME: Use this to clean data.claims (remove leading/trailing whitespace, etc.) # FIXME: Use this to clean data.claims (remove leading/trailing whitespace, etc.)
pass pass
......
...@@ -303,6 +303,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -303,6 +303,7 @@ function MentoringWithStepsBlock(runtime, element) {
function showActiveStep() { function showActiveStep() {
var step = steps[activeStep]; var step = steps[activeStep];
step.updatePlots();
$(step.element).show(); $(step.element).show();
} }
......
...@@ -57,7 +57,6 @@ function PlotBlock(runtime, element) { ...@@ -57,7 +57,6 @@ function PlotBlock(runtime, element) {
var defaultClaims = $('.plot-default', element).data('claims'); var defaultClaims = $('.plot-default', element).data('claims');
var averageClaims = $('.plot-average', element).data('claims'); var averageClaims = $('.plot-average', element).data('claims');
// Colors // Colors
var defaultColor = $('.plot-default', element).data('point-color'); var defaultColor = $('.plot-default', element).data('point-color');
...@@ -75,48 +74,60 @@ function PlotBlock(runtime, element) { ...@@ -75,48 +74,60 @@ function PlotBlock(runtime, element) {
var defaultButton = $('.plot-default', element); var defaultButton = $('.plot-default', element);
var averageButton = $('.plot-average', element); var averageButton = $('.plot-average', element);
function toggleOverlay(claims, color, klass) { function toggleOverlay(claims, color, klass, refresh) {
var selector = "." + klass; var selector = "." + klass;
var selection = svgContainer.selectAll(selector); var selection = svgContainer.selectAll(selector);
if (selection.empty()) { if (selection.empty()) {
svgContainer.selectAll(selector) showOverlay(selection, claims, color, klass);
.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);
} else { } 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 $button = $(button);
var overlayOn = $button.data("overlay-on"); var overlayOn = $button.data("overlay-on");
if (overlayOn) { if (overlayOn && !refresh) {
$button.css("border-color", "rgb(237, 237, 237)"); // Default color: grey $button.css("border-color", "rgb(237, 237, 237)"); // Default color: grey
$button.data("overlay-on", false);
} else { } else {
$button.css("border-color", color); $button.css("border-color", color);
$button.data("overlay-on", true);
} }
$button.data("overlay-on", !overlayOn);
} }
defaultButton.on('click', function() { defaultButton.on('click', function(event, refresh) {
toggleOverlay(defaultClaims, defaultColor, 'claim-default'); toggleOverlay(defaultClaims, defaultColor, 'claim-default', refresh);
toggleBorderColor(this, defaultColor); toggleBorderColor(this, defaultColor, refresh);
}); });
averageButton.on('click', function() { averageButton.on('click', function(event) {
toggleOverlay(averageClaims, averageColor, 'claim-average'); toggleOverlay(averageClaims, averageColor, 'claim-average');
toggleBorderColor(this, averageColor); toggleBorderColor(this, averageColor);
}); });
...@@ -163,12 +174,43 @@ function PlotBlock(runtime, element) { ...@@ -163,12 +174,43 @@ function PlotBlock(runtime, element) {
toggleQuadrantLabels(); toggleQuadrantLabels();
}); });
// Show default overlay initially
defaultButton.trigger('click');
// Quadrant labels are off initially; color of button for toggling them should reflect this // Quadrant labels are off initially; color of button for toggling them should reflect this
quadrantsButton.css("border-color", "red"); 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) { function MentoringStepBlock(runtime, element) {
var children = runtime.children(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; var submitXHR, resultsXHR;
function callIfExists(obj, fn) { function callIfExists(obj, fn) {
...@@ -89,6 +98,15 @@ function MentoringStepBlock(runtime, element) { ...@@ -89,6 +98,15 @@ function MentoringStepBlock(runtime, element) {
hasQuestion: function() { hasQuestion: function() {
return $('.sb-step', element).data('has-question') 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( ...@@ -225,16 +225,14 @@ class MentoringStepBlock(
fragment.initialize_js('StepEdit') fragment.initialize_js('StepEdit')
return fragment return fragment
def student_view(self, context=None):
""" Student View """
return self._render_view(context, 'student_view')
def mentoring_view(self, context=None): def mentoring_view(self, context=None):
""" Mentoring View """ """ Mentoring View """
return self._render_view(context, 'mentoring_view') return self._render_view(context, 'mentoring_view')
def _render_view(self, context, view): def _render_view(self, context, view):
""" Actually renders a view """ """ Actually renders a view """
rendering_for_studio = context.get('author_preview_view')
fragment = Fragment() fragment = Fragment()
child_contents = [] child_contents = []
...@@ -243,10 +241,18 @@ class MentoringStepBlock( ...@@ -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 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."))) child_contents.append(u"<p>[{}]</p>".format(self._(u"Error: Unable to load child component.")))
else: else:
child_fragment = self._render_child_fragment(child, context, view) if rendering_for_studio and isinstance(child, PlotBlock):
# Don't use view to render plot blocks in Studio.
fragment.add_frag_resources(child_fragment) # This is necessary because:
child_contents.append(child_fragment.content) # - 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', { fragment.add_content(loader.render_template('templates/html/step.html', {
'self': self, 'self': self,
......
...@@ -18,14 +18,14 @@ ...@@ -18,14 +18,14 @@
<input type="button" <input type="button"
class="plot-default" class="plot-default"
data-claims="{{ self.default_claims }}" data-claims="{{ self.default_claims_json }}"
data-point-color="{{ self.point_color_default }}" data-point-color="{{ self.point_color_default }}"
data-overlay-on="false" data-overlay-on="false"
value="{{ self.plot_label }}" value="{{ self.plot_label }}"
/> />
<input type="button" <input type="button"
class="plot-average" class="plot-average"
data-claims="{{ self.average_claims }}" data-claims="{{ self.average_claims_json }}"
data-point-color="{{ self.point_color_average }}" data-point-color="{{ self.point_color_average }}"
data-overlay-on="false" data-overlay-on="false"
value="Average" 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