Commit e8be9bf8 by Tim Krones

New version of mentoring block that supports explicit steps.

parent 6af171bb
from .mentoring import MentoringBlock
from .mentoring import MentoringBlock, MentoringWithExplicitStepsBlock
from .step import MentoringStepBlock
from .answer import AnswerBlock, AnswerRecapBlock
from .choice import ChoiceBlock
from .dashboard import DashboardBlock
......
......@@ -30,9 +30,9 @@ from xblock.fields import Scope, Float, Integer, String
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
from xblockutils.studio_editable import StudioEditableXBlockMixin, XBlockWithPreviewMixin
from problem_builder.sub_api import SubmittingXBlockMixin, sub_api
from .step import StepMixin
from .mixins import QuestionMixin, XBlockWithTranslationServiceMixin
import uuid
......@@ -49,7 +49,7 @@ def _(text):
# Classes ###########################################################
class AnswerMixin(object):
class AnswerMixin(XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin):
"""
Mixin to give an XBlock the ability to read/write data to the Answers DB table.
"""
......@@ -114,19 +114,18 @@ class AnswerMixin(object):
if not data.name:
add_error(u"A Question ID is required.")
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@XBlock.needs("i18n")
class AnswerBlock(SubmittingXBlockMixin, AnswerMixin, StepMixin, StudioEditableXBlockMixin, XBlock):
class AnswerBlock(SubmittingXBlockMixin, AnswerMixin, QuestionMixin, StudioEditableXBlockMixin, XBlock):
"""
A field where the student enters an answer
Must be included as a child of a mentoring block. Answers are persisted as django model instances
to make them searchable and referenceable across xblocks.
"""
CATEGORY = 'pb-answer'
STUDIO_LABEL = _(u"Long Answer")
name = String(
display_name=_("Question ID (name)"),
help=_("The ID of this block. Should be unique unless you want the answer to be used in multiple places."),
......@@ -273,6 +272,9 @@ class AnswerRecapBlock(AnswerMixin, StudioEditableXBlockMixin, XBlock):
"""
A block that displays an answer previously entered by the student (read-only).
"""
CATEGORY = 'pb-answer-recap'
STUDIO_LABEL = _(u"Long Answer Recap")
name = String(
display_name=_("Question ID"),
help=_("The ID of the question for which to display the student's answer."),
......
......@@ -27,7 +27,9 @@ from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.studio_editable import StudioEditableXBlockMixin
from xblockutils.studio_editable import StudioEditableXBlockMixin, XBlockWithPreviewMixin
from problem_builder.mixins import XBlockWithTranslationServiceMixin
# Make '_' a no-op so we can scrape strings
......@@ -38,7 +40,7 @@ def _(text):
@XBlock.needs("i18n")
class ChoiceBlock(StudioEditableXBlockMixin, XBlock):
class ChoiceBlock(StudioEditableXBlockMixin, XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin, XBlock):
"""
Custom choice of an answer for a MCQ/MRQ
"""
......@@ -56,10 +58,6 @@ class ChoiceBlock(StudioEditableXBlockMixin, XBlock):
)
editable_fields = ('content', 'value')
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@property
def display_name_with_default(self):
try:
......
......@@ -48,6 +48,9 @@ class MCQBlock(SubmittingXBlockMixin, QuestionnaireAbstractBlock):
"""
An XBlock used to ask multiple-choice questions
"""
CATEGORY = 'pb-mcq'
STUDIO_LABEL = _(u"Multiple Choice Question")
student_choice = String(
# {Last input submitted by the student
default="",
......@@ -158,6 +161,9 @@ class RatingBlock(MCQBlock):
"""
An XBlock used to rate something on a five-point scale, e.g. Likert Scale
"""
CATEGORY = 'pb-rating'
STUDIO_LABEL = _(u"Rating Question")
low = String(
display_name=_("Low"),
help=_("Label for low ratings"),
......
......@@ -24,6 +24,9 @@ import logging
import json
from collections import namedtuple
from itertools import chain
from lazy.lazy import lazy
from xblock.core import XBlock
from xblock.exceptions import NoSuchViewError, JsonHandlerError
......@@ -31,12 +34,18 @@ from xblock.fields import Boolean, Scope, String, Integer, Float, List
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from .message import MentoringMessageBlock
from .step import StepParentMixin, StepMixin
from .message import (
MentoringMessageBlock, CompletedMentoringMessageShim, IncompleteMentoringMessageShim,
MaxAttemptsReachedMentoringMessageShim, OnAssessmentReviewMentoringMessageShim
)
from .mixins import _normalize_id, StepParentMixin, QuestionMixin, XBlockWithTranslationServiceMixin
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContainerXBlockMixin
from xblockutils.studio_editable import (
NestedXBlockSpec, StudioEditableXBlockMixin, StudioContainerXBlockMixin, StudioContainerWithNestedXBlocksMixin
)
try:
# Used to detect if we're in the workbench so we can add Font Awesome
......@@ -70,7 +79,89 @@ PARTIAL = 'partial'
@XBlock.needs("i18n")
@XBlock.wants('settings')
class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioContainerXBlockMixin):
class BaseMentoringBlock(
XBlock, XBlockWithTranslationServiceMixin, StudioEditableXBlockMixin
):
"""
An XBlock that defines functionality shared by mentoring blocks.
"""
# Content
show_title = Boolean(
display_name=_("Show title"),
help=_("Display the title?"),
default=True,
scope=Scope.content
)
has_children = True
icon_class = 'problem'
block_settings_key = 'mentoring'
theme_key = 'theme'
@property
def url_name(self):
"""
Get the url_name for this block. In Studio/LMS it is provided by a mixin, so we just
defer to super(). In the workbench or any other platform, we use the usage_id.
"""
try:
return super(BaseMentoringBlock, self).url_name
except AttributeError:
return unicode(self.scope_ids.usage_id)
def get_theme(self):
"""
Gets theme settings from settings service. Falls back to default (LMS) theme
if settings service is not available, xblock theme settings are not set or does
contain mentoring theme settings.
"""
settings_service = self.runtime.service(self, "settings")
if settings_service:
xblock_settings = settings_service.get_settings_bucket(self)
if xblock_settings and self.theme_key in xblock_settings:
return xblock_settings[self.theme_key]
return _default_theme_config
def include_theme_files(self, fragment):
theme = self.get_theme()
theme_package, theme_files = theme['package'], theme['locations']
for theme_file in theme_files:
fragment.add_css(ResourceLoader(theme_package).load_unicode(theme_file))
@XBlock.json_handler
def view(self, data, suffix=''):
"""
Current HTML view of the XBlock, for refresh by client
"""
frag = self.student_view({})
return {'html': frag.content}
@XBlock.json_handler
def publish_event(self, data, suffix=''):
"""
Publish data for analytics purposes
"""
event_type = data.pop('event_type')
self.runtime.publish(self, event_type, data)
return {'result': 'ok'}
def author_preview_view(self, context):
"""
Child blocks can override this to add a custom preview shown to
authors in Studio when not editing this block's children.
"""
fragment = self.student_view(context)
fragment.add_content(loader.render_template('templates/html/mentoring_url_name.html', {
"url_name": self.url_name
}))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
self.include_theme_files(fragment)
return fragment
class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentMixin):
"""
An XBlock providing mentoring capabilities
......@@ -122,12 +213,6 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
scope=Scope.content,
multiline_editor=True
)
show_title = Boolean(
display_name=_("Show title"),
help=_("Display the title?"),
default=True,
scope=Scope.content
)
# Settings
weight = Float(
......@@ -196,42 +281,21 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
'display_name', 'mode', 'followed_by', 'max_attempts', 'enforce_dependency',
'display_submit', 'feedback_label', 'weight', 'extended_feedback'
)
icon_class = 'problem'
has_score = True
has_children = True
block_settings_key = 'mentoring'
theme_key = 'theme'
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
has_score = True
@property
def is_assessment(self):
""" Checks if mentoring XBlock is in assessment mode """
return self.mode == 'assessment'
def get_theme(self):
"""
Gets theme settings from settings service. Falls back to default (LMS) theme
if settings service is not available, xblock theme settings are not set or does
contain mentoring theme settings.
"""
settings_service = self.runtime.service(self, "settings")
if settings_service:
xblock_settings = settings_service.get_settings_bucket(self)
if xblock_settings and self.theme_key in xblock_settings:
return xblock_settings[self.theme_key]
return _default_theme_config
def get_question_number(self, question_id):
"""
Get the step number of the question id
"""
for child_id in self.children:
question = self.runtime.get_block(child_id)
if isinstance(question, StepMixin) and (question.name == question_id):
if isinstance(question, QuestionMixin) and (question.name == question_id):
return question.step_number
raise ValueError("Question ID in answer set not a step of this Mentoring Block!")
......@@ -272,12 +336,6 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
return Score(score, int(round(score * 100)), correct, incorrect, partially_correct)
def include_theme_files(self, fragment):
theme = self.get_theme()
theme_package, theme_files = theme['package'], theme['locations']
for theme_file in theme_files:
fragment.add_css(ResourceLoader(theme_package).load_unicode(theme_file))
def student_view(self, context):
# Migrate stored data if necessary
self.migrate_fields()
......@@ -296,7 +354,7 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
child_content += u"<p>[{}]</p>".format(self._(u"Error: Unable to load child component."))
elif not isinstance(child, MentoringMessageBlock):
try:
if self.is_assessment and isinstance(child, StepMixin):
if self.is_assessment and isinstance(child, QuestionMixin):
child_fragment = child.render('assessment_step_view', context)
else:
child_fragment = child.render('mentoring_view', context)
......@@ -375,35 +433,6 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
"""
return '/jump_to_id/{}'.format(self.next_step)
@property
def url_name(self):
"""
Get the url_name for this block. In Studio/LMS it is provided by a mixin, so we just
defer to super(). In the workbench or any other platform, we use the usage_id.
"""
try:
return super(MentoringBlock, self).url_name
except AttributeError:
return unicode(self.scope_ids.usage_id)
@XBlock.json_handler
def view(self, data, suffix=''):
"""
Current HTML view of the XBlock, for refresh by client
"""
frag = self.student_view({})
return {'html': frag.content}
@XBlock.json_handler
def publish_event(self, data, suffix=''):
"""
Publish data for analytics purposes
"""
event_type = data.pop('event_type')
self.runtime.publish(self, event_type, data)
return {'result': 'ok'}
def get_message(self, completed):
"""
Get the message to display to a student following a submission in normal mode.
......@@ -622,7 +651,7 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
current_child = None
children = [self.runtime.get_block(child_id) for child_id in self.children]
children = [child for child in children if not isinstance(child, MentoringMessageBlock)]
steps = [child for child in children if isinstance(child, StepMixin)] # Faster than the self.steps property
steps = [child for child in children if isinstance(child, QuestionMixin)] # Faster than the self.steps property
assessment_message = None
review_tips = []
......@@ -751,19 +780,6 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
))
return validation
def author_preview_view(self, context):
"""
Child blocks can override this to add a custom preview shown to authors in Studio when
not editing this block's children.
"""
fragment = self.student_view(context)
fragment.add_content(loader.render_template('templates/html/mentoring_url_name.html', {
"url_name": self.url_name
}))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
self.include_theme_files(fragment)
return fragment
def author_edit_view(self, context):
"""
Add some HTML to the author view that allows authors to add child blocks.
......@@ -804,3 +820,99 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
Scenarios displayed by the workbench. Load them from external (private) repository
"""
return loader.load_scenarios_from_path('templates/xml')
class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNestedXBlocksMixin):
"""
An XBlock providing mentoring capabilities with explicit steps
"""
# Settings
display_name = String(
display_name=_("Title (Display name)"),
help=_("Title to display"),
default=_("Mentoring Questions (with explicit steps)"),
scope=Scope.settings
)
editable_fields = ('display_name',)
@lazy
def questions(self):
""" Get the usage_ids of all of this XBlock's children that are "Questions" """
return list(chain.from_iterable(self.runtime.get_block(step_id).steps for step_id in self.steps))
@property
def steps(self):
"""
Get the usage_ids of all of this XBlock's children that are "Steps"
"""
from .step import MentoringStepBlock # Import here to avoid circular dependency
return [
_normalize_id(child_id) for child_id in self.children if
child_isinstance(self, child_id, MentoringStepBlock)
]
def student_view(self, context):
fragment = Fragment()
child_content = u""
for child_id in self.children:
child = self.runtime.get_block(child_id)
if child is None: # child should not be None but it can happen due to bugs or permission issues
child_content += u"<p>[{}]</p>".format(self._(u"Error: Unable to load child component."))
elif not isinstance(child, MentoringMessageBlock):
child_fragment = self._render_child_fragment(child, context, view='mentoring_view')
fragment.add_frag_resources(child_fragment)
child_content += child_fragment.content
fragment.add_content(loader.render_template('templates/html/mentoring.html', {
'self': self,
'title': self.display_name,
'show_title': self.show_title,
'child_content': child_content,
}))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css'))
self.include_theme_files(fragment)
return fragment
@property
def allowed_nested_blocks(self):
"""
Returns a list of allowed nested XBlocks. Each item can be either
* An XBlock class
* A NestedXBlockSpec
If XBlock class is used it is assumed that this XBlock is enabled and allows multiple instances.
NestedXBlockSpec allows explicitly setting disabled/enabled state, disabled reason (if any) and single/multiple
instances
"""
from .step import MentoringStepBlock # Import here to avoid circular dependency
return [
MentoringStepBlock,
NestedXBlockSpec(CompletedMentoringMessageShim, boilerplate='completed'),
NestedXBlockSpec(IncompleteMentoringMessageShim, boilerplate='incomplete'),
NestedXBlockSpec(MaxAttemptsReachedMentoringMessageShim, boilerplate='max_attempts_reached'),
NestedXBlockSpec(OnAssessmentReviewMentoringMessageShim, boilerplate='on-assessment-review'),
]
def author_edit_view(self, context):
"""
Add some HTML to the author view that allows authors to add child blocks.
"""
context['wrap_children'] = {
'head': u'<div class="mentoring">',
'tail': u'</div>'
}
fragment = super(MentoringWithExplicitStepsBlock, self).author_edit_view(context)
fragment.add_content(loader.render_template('templates/html/mentoring_url_name.html', {
"url_name": self.url_name
}))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css'))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps_edit.js'))
fragment.initialize_js('MentoringWithStepsEdit')
return fragment
......@@ -26,6 +26,8 @@ from xblock.fields import Scope, String
from xblock.fragment import Fragment
from xblockutils.studio_editable import StudioEditableXBlockMixin
from problem_builder.mixins import XBlockWithTranslationServiceMixin
# Make '_' a no-op so we can scrape strings
def _(text):
......@@ -35,7 +37,7 @@ def _(text):
@XBlock.needs("i18n")
class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin, XBlockWithTranslationServiceMixin):
"""
A message which can be conditionally displayed at the mentoring block level,
for example upon completion of the block
......@@ -126,10 +128,6 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
)
editable_fields = ("content", )
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
def mentoring_view(self, context=None):
""" Render this message for use by a mentoring block. """
html = u'<div class="submission-message {msg_type}">{content}</div>'.format(
......@@ -184,3 +182,23 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
block.content += etree.tostring(child, encoding='unicode')
return block
class CompletedMentoringMessageShim(object):
CATEGORY = 'pb-message'
STUDIO_LABEL = _("Message (Complete)")
class IncompleteMentoringMessageShim(object):
CATEGORY = 'pb-message'
STUDIO_LABEL = _("Message (Incomplete)")
class MaxAttemptsReachedMentoringMessageShim(object):
CATEGORY = 'pb-message'
STUDIO_LABEL = _("Message (Max # Attempts)")
class OnAssessmentReviewMentoringMessageShim(object):
CATEGORY = 'pb-message'
STUDIO_LABEL = _("Message (Assessment Review)")
from lazy import lazy
from xblock.fields import String, Boolean, Scope
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader
loader = ResourceLoader(__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
class XBlockWithTranslationServiceMixin(object):
"""
Mixin providing access to i18n service
"""
def _(self, text):
""" Translate text """
return self.runtime.service(self, "i18n").ugettext(text)
class EnumerableChildMixin(XBlockWithTranslationServiceMixin):
CAPTION = _(u"Child")
show_title = Boolean(
display_name=_("Show title"),
help=_("Display the title?"),
default=True,
scope=Scope.content
)
@lazy
def siblings(self):
# TODO: It might make sense to provide a default
# implementation here that just returns normalized ID's of the
# parent's children.
raise NotImplementedError("Should be overridden in child class")
@lazy
def step_number(self):
return list(self.siblings).index(_normalize_id(self.scope_ids.usage_id)) + 1
@lazy
def lonely_child(self):
if _normalize_id(self.scope_ids.usage_id) not in self.siblings:
message = u"{child_caption}'s parent should contain {child_caption}".format(child_caption=self.CAPTION)
raise ValueError(message, self, self.siblings)
return len(self.siblings) == 1
@property
def display_name_with_default(self):
""" Get the title/display_name of this question. """
if self.display_name:
return self.display_name
if not self.lonely_child:
return self._(u"{child_caption} {number}").format(
child_caption=self.CAPTION, number=self.step_number
)
return self._(self.CAPTION)
class StepParentMixin(object):
"""
An XBlock mixin for a parent block containing Step children
"""
@lazy
def steps(self):
"""
Get the usage_ids of all of this XBlock's children that are "Steps"
"""
return [
_normalize_id(child_id) for child_id in self.children if child_isinstance(self, child_id, QuestionMixin)
]
def get_steps(self):
""" Get the step children of this block, cached if possible. """
if getattr(self, "_steps_cache", None) is None:
self._steps_cache = [self.runtime.get_block(child_id) for child_id in self.steps]
return self._steps_cache
class QuestionMixin(EnumerableChildMixin):
"""
An XBlock mixin for a child block that is a "Step".
A step is a question that the user can answer (as opposed to a read-only child).
"""
CAPTION = _(u"Question")
has_author_view = True
# Fields:
display_name = String(
display_name=_("Question title"),
help=_('Leave blank to use the default ("Question 1", "Question 2", etc.)'),
default="", # Blank will use 'Question x' - see display_name_with_default
scope=Scope.content
)
@lazy
def siblings(self):
return self.get_parent().steps
def author_view(self, context):
context = context.copy() if context else {}
context['hide_header'] = True
return self.mentoring_view(context)
def author_preview_view(self, context):
context = context.copy() if context else {}
context['hide_header'] = True
return self.student_view(context)
def assessment_step_view(self, context=None):
"""
assessment_step_view is the same as mentoring_view, except its DIV will have a different
class (.xblock-v1-assessment_step_view) that we use for assessments to hide all the
steps with CSS and to detect which children of mentoring are "Steps" and which are just
decorative elements/instructions.
"""
return self.mentoring_view(context)
......@@ -44,6 +44,9 @@ class MRQBlock(QuestionnaireAbstractBlock):
"""
An XBlock used to ask multiple-response questions
"""
CATEGORY = 'pb-mrq'
STUDIO_LABEL = _(u"Multiple Response Question")
student_choices = List(
# Last submissions by the student
default=[],
......
/* Display of url_name below content */
.xblock[data-block-type=pb-mentoring-step] .url-name-footer,
.xblock[data-block-type=pb-mentoring] .url-name-footer,
.xblock[data-block-type=problem-builder] .url-name-footer,
.xblock[data-block-type=mentoring] .url-name-footer {
font-style: italic;
}
.xblock[data-block-type=pb-mentoring-step] .url-name-footer .url-name,
.xblock[data-block-type=pb-mentoring] .url-name-footer .url-name,
.xblock[data-block-type=problem-builder] .url-name-footer .url-name,
.xblock[data-block-type=mentoring] .url-name-footer .url-name {
margin: 0 10px;
......@@ -11,6 +15,8 @@
}
/* Custom appearance for our "Add" buttons */
.xblock[data-block-type=pb-mentoring-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button,
.xblock[data-block-type=pb-mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button,
.xblock[data-block-type=problem-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button,
.xblock[data-block-type=mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button {
width: 200px;
......@@ -18,6 +24,10 @@
line-height: 30px;
}
.xblock[data-block-type=pb-mentoring-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
.xblock[data-block-type=pb-mentoring-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover,
.xblock[data-block-type=pb-mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
.xblock[data-block-type=pb-mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover,
.xblock[data-block-type=problem-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
.xblock[data-block-type=problem-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover,
.xblock[data-block-type=mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
......@@ -27,6 +37,7 @@
cursor: default;
}
.xblock[data-block-type=pb-mentoring] .submission-message-help p,
.xblock[data-block-type=problem-builder] .submission-message-help p {
border-top: 1px solid #ddd;
font-size: 0.85em;
......
function MentoringWithStepsEdit(runtime, element) {
"use strict";
// Disable "add" buttons when a message of that type already exists:
var $buttons = $('.add-xblock-component-button[data-category=pb-message]', element);
var updateButtons = function() {
$buttons.each(function() {
var msg_type = $(this).data('boilerplate');
$(this).toggleClass('disabled', $('.xblock .submission-message.'+msg_type).length > 0);
});
};
updateButtons();
$buttons.click(function(ev) {
if ($(this).is('.disabled')) {
ev.preventDefault();
ev.stopPropagation();
} else {
$(this).addClass('disabled');
}
});
ProblemBuilderUtil.transformClarifications(element);
StudioEditableXBlockMixin(runtime, element);
}
function StepEdit(runtime, element) {
'use strict';
StudioContainerXBlockWithNestedXBlocksMixin(runtime, element);
ProblemBuilderUtil.transformClarifications(element);
}
......@@ -29,12 +29,12 @@ from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContainerXBlockMixin
from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContainerXBlockMixin, XBlockWithPreviewMixin
from .choice import ChoiceBlock
from .mentoring import MentoringBlock
from .message import MentoringMessageBlock
from .step import StepMixin
from .mixins import QuestionMixin, XBlockWithTranslationServiceMixin
from .tip import TipBlock
# Globals ###########################################################
......@@ -50,7 +50,10 @@ def _(text):
@XBlock.needs("i18n")
class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBlockMixin, StepMixin, XBlock):
class QuestionnaireAbstractBlock(
StudioEditableXBlockMixin, StudioContainerXBlockMixin, QuestionMixin, XBlock, XBlockWithPreviewMixin,
XBlockWithTranslationServiceMixin
):
"""
An abstract class used for MCQ/MRQ blocks
......@@ -88,10 +91,6 @@ class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBloc
editable_fields = ('question', 'message', 'weight', 'display_name', 'show_title')
has_children = True
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@lazy
def html_id(self):
"""
......
......@@ -18,9 +18,25 @@
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
from lazy import lazy
from lazy.lazy import lazy
from xblock.core import XBlock
from xblock.fields import String, Boolean, Scope
from xblock.fragment import Fragment
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import (
NestedXBlockSpec, StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin
)
from problem_builder.answer import AnswerBlock, AnswerRecapBlock
from problem_builder.mcq import MCQBlock, RatingBlock
from problem_builder.mixins import EnumerableChildMixin
from problem_builder.mrq import MRQBlock
from problem_builder.table import MentoringTableBlock
loader = ResourceLoader(__name__)
# Make '_' a no-op so we can scrape strings
......@@ -40,81 +56,108 @@ def _normalize_id(key):
return key
class StepParentMixin(object):
"""
An XBlock mixin for a parent block containing Step children
"""
@lazy
def steps(self):
"""
Get the usage_ids of all of this XBlock's children that are "Steps"
"""
return [_normalize_id(child_id) for child_id in self.children if child_isinstance(self, child_id, StepMixin)]
def get_steps(self):
""" Get the step children of this block, cached if possible. """
if getattr(self, "_steps_cache", None) is None:
self._steps_cache = [self.runtime.get_block(child_id) for child_id in self.steps]
return self._steps_cache
class HtmlBlockShim(object):
CATEGORY = 'html'
STUDIO_LABEL = _(u"HTML")
class StepMixin(object):
@XBlock.needs('i18n')
class MentoringStepBlock(
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin,
EnumerableChildMixin, XBlock
):
"""
An XBlock mixin for a child block that is a "Step".
A step is a question that the user can answer (as opposed to a read-only child).
An XBlock for a step.
"""
has_author_view = True
CAPTION = _(u"Step")
STUDIO_LABEL = _(u"Mentoring Step")
CATEGORY = 'pb-mentoring-step'
# Fields:
display_name = String(
display_name=_("Question title"),
help=_('Leave blank to use the default ("Question 1", "Question 2", etc.)'),
default="", # Blank will use 'Question x' - see display_name_with_default
scope=Scope.content
)
show_title = Boolean(
display_name=_("Show title"),
help=_("Display the title?"),
default=True,
display_name=_("Step Title"),
help=_('Leave blank to use sequential numbering'),
default="",
scope=Scope.content
)
@lazy
def step_number(self):
return list(self.get_parent().steps).index(_normalize_id(self.scope_ids.usage_id)) + 1
editable_fields = ('display_name', 'show_title',)
@lazy
def lonely_step(self):
if _normalize_id(self.scope_ids.usage_id) not in self.get_parent().steps:
raise ValueError("Step's parent should contain Step", self, self.get_parent().steps)
return len(self.get_parent().steps) == 1
def siblings(self):
return self.get_parent().steps
@property
def display_name_with_default(self):
""" Get the title/display_name of this question. """
if self.display_name:
return self.display_name
if not self.lonely_step:
return self._(u"Question {number}").format(number=self.step_number)
return self._(u"Question")
def author_view(self, context):
context = context.copy() if context else {}
context['hide_header'] = True
return self.mentoring_view(context)
def author_preview_view(self, context):
context = context.copy() if context else {}
context['hide_header'] = True
return self.student_view(context)
def assessment_step_view(self, context=None):
def allowed_nested_blocks(self):
"""
Returns a list of allowed nested XBlocks. Each item can be either
* An XBlock class
* A NestedXBlockSpec
If XBlock class is used it is assumed that this XBlock is enabled and allows multiple instances.
NestedXBlockSpec allows explicitly setting disabled/enabled state, disabled reason (if any) and single/multiple
instances
"""
return [
NestedXBlockSpec(AnswerBlock, boilerplate='studio_default'),
MCQBlock, RatingBlock, MRQBlock, HtmlBlockShim,
AnswerRecapBlock, MentoringTableBlock,
]
@property
def steps(self):
""" Get the usage_ids of all of this XBlock's children that are "Questions" """
from mixins import QuestionMixin
return [
_normalize_id(child_id) for child_id in self.children if child_isinstance(self, child_id, QuestionMixin)
]
def author_edit_view(self, context):
"""
assessment_step_view is the same as mentoring_view, except its DIV will have a different
class (.xblock-v1-assessment_step_view) that we use for assessments to hide all the
steps with CSS and to detect which children of mentoring are "Steps" and which are just
decorative elements/instructions.
Add some HTML to the author view that allows authors to add child blocks.
"""
return self.mentoring_view(context)
local_context = dict(context)
local_context['wrap_children'] = {
'head': u'<div class="mentoring">',
'tail': u'</div>'
}
fragment = super(MentoringStepBlock, self).author_edit_view(local_context)
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css'))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/step_edit.js'))
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 """
fragment = Fragment()
child_contents = []
for child_id in self.children:
child = self.runtime.get_block(child_id)
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)
fragment.add_content(loader.render_template('templates/html/step.html', {
'self': self,
'title': self.display_name,
'show_title': self.show_title,
'child_contents': child_contents,
}))
return fragment
......@@ -30,10 +30,11 @@ from xblock.fields import Scope, String, Boolean, Dict
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContainerXBlockMixin
from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContainerXBlockMixin, XBlockWithPreviewMixin
# Globals ###########################################################
from problem_builder import AnswerRecapBlock
from problem_builder.answer import AnswerRecapBlock
from problem_builder.dashboard import ExportMixin
from problem_builder.models import Share
from problem_builder.sub_api import SubmittingXBlockMixin
......@@ -51,7 +52,8 @@ def _(text):
@XBlock.wants("user")
@XBlock.wants("submissions")
class MentoringTableBlock(
StudioEditableXBlockMixin, SubmittingXBlockMixin, StudioContainerXBlockMixin, ExportMixin, XBlock
StudioEditableXBlockMixin, SubmittingXBlockMixin, StudioContainerXBlockMixin, ExportMixin, XBlock,
XBlockWithPreviewMixin
):
"""
Table-type display of information from mentoring blocks
......@@ -59,6 +61,9 @@ class MentoringTableBlock(
Used to present summary of information entered by the students in mentoring blocks.
Supports different types of formatting through the `type` parameter.
"""
CATEGORY = 'pb-table'
STUDIO_LABEL = _(u"Answer Recap Table")
display_name = String(
display_name=_("Display name"),
help=_("Title of the table"),
......
<div class="pb-step">
{% if show_title %}
<div class="title">
<h3>
{% if title %}
{{ title }}
{% else %}
{{ self.display_name_with_default }}
{% endif %}
</h3>
</div>
{% endif %}
{% for child_content in child_contents %}
{{ child_content|safe }}
{% endfor %}
</div>
import unittest
from problem_builder.step import StepMixin, StepParentMixin
from problem_builder.mixins import QuestionMixin, StepParentMixin
from mock import Mock
......@@ -32,7 +32,7 @@ class BaseClass(object):
pass
class Step(BaseClass, StepMixin):
class Step(BaseClass, QuestionMixin):
def __init__(self):
pass
......@@ -41,7 +41,7 @@ class NotAStep(object):
pass
class TestStepMixin(unittest.TestCase):
class TestQuestionMixin(unittest.TestCase):
def test_single_step_is_returned_correctly(self):
block = Parent()
step = Step()
......@@ -77,18 +77,18 @@ class TestStepMixin(unittest.TestCase):
self.assertEquals(step1.step_number, 2)
self.assertEquals(step2.step_number, 1)
def test_lonely_step_is_true_for_stand_alone_steps(self):
def test_lonely_child_is_true_for_stand_alone_steps(self):
block = Parent()
step1 = Step()
block._set_children_for_test(1, "2", step1, "Step", NotAStep(), False)
self.assertTrue(step1.lonely_step)
self.assertTrue(step1.lonely_child)
def test_lonely_step_is_true_if_parent_have_more_steps(self):
def test_lonely_child_is_true_if_parent_have_more_steps(self):
block = Parent()
step1 = Step()
step2 = Step()
block._set_children_for_test(1, step2, "2", step1, "Step", NotAStep(), False)
self.assertFalse(step1.lonely_step)
self.assertFalse(step2.lonely_step)
self.assertFalse(step1.lonely_child)
self.assertFalse(step2.lonely_child)
......@@ -30,6 +30,8 @@ from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
from problem_builder.mixins import XBlockWithTranslationServiceMixin
loader = ResourceLoader(__name__)
......@@ -41,7 +43,7 @@ def _(text):
@XBlock.needs("i18n")
class TipBlock(StudioEditableXBlockMixin, XBlock):
class TipBlock(StudioEditableXBlockMixin, XBlockWithTranslationServiceMixin, XBlock):
"""
Each choice can define a tip depending on selection
"""
......@@ -73,10 +75,6 @@ class TipBlock(StudioEditableXBlockMixin, XBlock):
)
editable_fields = ('values', 'content', 'width', 'height')
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@property
def display_name_with_default(self):
values_list = []
......
ddt
mock
unicodecsv==0.9.4
-e git+https://github.com/edx/xblock-utils.git@213a97a50276d6a2504d8133650b2930ead357a0#egg=xblock-utils
-e git+https://github.com/edx/xblock-utils.git@3b58c757f06943072b170654d676e95b9adb37b0#egg=xblock-utils
-e .
......@@ -41,6 +41,8 @@ def package_data(pkg, root_list):
BLOCKS = [
'problem-builder = problem_builder:MentoringBlock',
'pb-mentoring = problem_builder:MentoringWithExplicitStepsBlock',
'pb-mentoring-step = problem_builder:MentoringStepBlock',
'pb-table = problem_builder:MentoringTableBlock',
'pb-column = problem_builder:MentoringTableColumn',
......
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