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 problem_builder/mcq.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ class RatingBlock(MCQBlock):
list_values_provider=QuestionnaireAbstractBlock.choice_values_provider,
list_style='set', # Underered, unique items. Affects the UI editor.
)
editable_fields = MCQBlock.editable_fields + ('low', 'high')
editable_fields = MCQBlock.editable_fields + ('low', 'high', 'name')

@property
def all_choice_values(self):
Expand Down
5 changes: 5 additions & 0 deletions problem_builder/mentoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,11 @@ def try_again(self, data, suffix=''):
'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.
Expand Down
268 changes: 268 additions & 0 deletions problem_builder/plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2015 Harvard, edX & OpenCraft
#
# This software's license gives you freedom; you can copy, convey,
# propagate, redistribute and/or modify this program under the terms of
# the GNU Affero General Public License (AGPL) as published by the Free
# Software Foundation (FSF), either version 3 of the License, or (at your
# option) any later version of the AGPL published by the FSF.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
# General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program in a file in the toplevel directory called
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#

import json
import logging

from lazy.lazy import lazy

from xblock.core import XBlock
from xblock.fields import String, Scope
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import (
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin
)
from .sub_api import sub_api


loader = ResourceLoader(__name__)

log = logging.getLogger(__name__)


# Make '_' a no-op so we can scrape strings
def _(text):
return text


def _normalize_id(key):
"""
Helper method to normalize a key to avoid issues where some keys have version/branch and others don't.
e.g. self.scope_ids.usage_id != self.runtime.get_block(self.scope_ids.usage_id).scope_ids.usage_id
"""
if hasattr(key, "for_branch"):
key = key.for_branch(None)
if hasattr(key, "for_version"):
key = key.for_version(None)
return key


@XBlock.needs('i18n')
@XBlock.wants('user')
class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, XBlock):

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.

Note you used the NestedXBlocksMixin, though there are no children XBlocks specified for this one. If you're going to take my suggestion about children for the plot items, great. Otherwise the user is given a prompt to add in children blocks, but is presented with no options to actually do so if they follow that link.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@Kelketek I added the mixin because PlotBlock will get children as part of OC-1014, which will add support for defining additional overlay types.

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.

@itsjeyd As long as it's not shipping to production with this oddity, I'm ok with that.

"""
XBlock that displays plot that summarizes answers to scale questions.
"""

CATEGORY = 'sb-plot'
STUDIO_LABEL = _(u"Plot")

# Settings
display_name = String(
display_name=_("Plot title"),
default="Plot",
scope=Scope.content
)

plot_label = String(
display_name=_("Plot label"),
help=_("Label for default overlay that shows student's answers to scale questions"),
default="yours",
scope=Scope.content
)

point_color_default = String(
display_name=_("Point color (default overlay)"),
help=_("Point color to use for default overlay"),
default="green",
scope=Scope.content
)

point_color_average = String(
display_name=_("Point color (average overlay)"),
help=_("Point color to use for average overlay"),
default="blue",
scope=Scope.content
)

q1_label = String(
display_name=_("Quadrant I"),
help=_(
"Label for the first quadrant. "
"Plot uses counter-clockwise numbering starting in the top right quadrant."
),
default="Q1",
scope=Scope.content
)

q2_label = String(
display_name=_("Quadrant II"),
help=_(
"Label for the second quadrant. "
"Plot uses counter-clockwise numbering starting in the top right quadrant."
),
default="Q2",
scope=Scope.content
)

q3_label = String(
display_name=_("Quadrant III"),
help=_(
"Label for the third quadrant. "
"Plot uses counter-clockwise numbering starting in the top right quadrant."
),
default="Q3",
scope=Scope.content
)

q4_label = String(
display_name=_("Quadrant IV"),
help=_(
"Label for the fourth quadrant. "
"Plot uses counter-clockwise numbering starting in the top right quadrant."
),
default="Q4",
scope=Scope.content
)

claims = String(
display_name=_("Claims and associated questions"),
help=_(
'Claims and questions that should be included in the plot. '
'Each line defines a triple of the form "claim, q1, q2", '
'where "claim" is arbitrary text that represents a claim, '
'and "q1" and "q2" are IDs of scale questions. '
),
default="",
multiline_editor=True,
resettable_editor=False
)

editable_fields = (
'display_name', 'plot_label', 'point_color_default', 'point_color_average',
'q1_label', 'q2_label', 'q3_label', 'q4_label', 'claims'
)

@lazy
def course_key_str(self):
location = _normalize_id(self.location)
return unicode(location.course_key)

@property
def default_claims(self):
return self._get_claims(self._get_default_response)

@property
def average_claims(self):
return self._get_claims(self._get_average_response)

def _get_claims(self, response_function):

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.

You'd be able to eliminate this function or significantly reduce it by using children as mentioned in the earlier comment.

if not self.claims:
return []

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 question_id, question in zip(question_ids, questions):
if question.name == q1:
r1 = response_function(question, question_id)
if question.name == q2:
r2 = response_function(question, question_id)
if r1 is not None and r2 is not None:
break
claims.append([claim, r1, r2])

return claims

def _get_default_response(self, question, question_id):
# 1. Obtain block_type for question
question_type = question.scope_ids.block_type
# 2. Obtain latest submission for question
student_dict = {
'student_id': self.runtime.anonymous_student_id,
'course_id': self.course_key_str,
'item_id': question_id,
'item_type': question_type,
}
submissions = sub_api.get_submissions(student_dict, limit=1)
# 3. Extract response from latest submission for question
answer_cache = {}
for submission in submissions:
answer = self._get_answer(question, submission, answer_cache)
return int(answer)

def _get_average_response(self, question, question_id):
# 1. Obtain block_type for question
question_type = question.scope_ids.block_type
# 2. Obtain latest submissions for question
submissions = sub_api.get_all_submissions(self.course_key_str, question_id, question_type)
# 3. Extract responses from latest submissions for question and sum them up
answer_cache = {}
response_total = 0
num_submissions = 0 # Can't use len(submissions) because submissions is a generator
for submission in submissions:
answer = self._get_answer(question, submission, answer_cache)
response_total += int(answer)
num_submissions += 1
# 4. Calculate average response for question
if num_submissions:
return response_total / float(num_submissions)

def _get_answer(self, block, submission, answer_cache):
"""
Return answer associated with `submission` to `block`.

`answer_cache` is a dict that is reset for each block.
"""
answer = submission['answer']
# Convert from answer ID to answer label
if answer not in answer_cache:
answer_cache[answer] = block.get_submission_display(answer)
return answer_cache[answer]

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 author_preview_view(self, context):

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.

It would be really nice if some sort of preview could be added, even with phony data somehow. Or just showing the empty grid with buttons. The answer data should be available on the studio side, I'd think-- so someone in the studio should be able to test with it directly if they really wanted to.

return Fragment(
u"<p>{}</p>".format(
_(u"This block displays a plot that summarizes answers to scale questions.")
)
)

def mentoring_view(self, context):
return self.student_view(context)

def student_view(self, context=None):

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.

Does just using this and not the authoring view break anything?

""" Student View """
context = context.copy() if context else {}
context['hide_header'] = True
context['self'] = self
fragment = Fragment()
fragment.add_content(loader.render_template('templates/html/plot.html', context))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/plot.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/vendor/d3.min.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/plot.js'))
fragment.initialize_js('PlotBlock')
return fragment
11 changes: 11 additions & 0 deletions problem_builder/public/css/plot.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.quadrants label {
font-weight: bold;
}

.overlays {
float: right;
}

.quadrants input, .overlays input {
background-color: rgb(204, 204, 204);
}
1 change: 1 addition & 0 deletions problem_builder/public/js/mentoring_with_steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ function MentoringWithStepsBlock(runtime, element) {

function showActiveStep() {
var step = steps[activeStep];
step.updatePlots();
$(step.element).show();
}

Expand Down
Loading