-
Notifications
You must be signed in to change notification settings - Fork 59
Step Builder: New block type: "Plot" #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fe2b725
dd04ee8
fbf23f9
df202ce
cc0fda9
c2fb804
3296ca7
e487221
bc2a12e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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): | ||
| """ | ||
| 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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
PlotBlockwill get children as part of OC-1014, which will add support for defining additional overlay types.There was a problem hiding this comment.
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.