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 .answer import AnswerBlock, AnswerRecapBlock
from .choice import ChoiceBlock from .choice import ChoiceBlock
from .dashboard import DashboardBlock from .dashboard import DashboardBlock
......
...@@ -30,9 +30,9 @@ from xblock.fields import Scope, Float, Integer, String ...@@ -30,9 +30,9 @@ from xblock.fields import Scope, Float, Integer, String
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblock.validation import ValidationMessage from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader 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 problem_builder.sub_api import SubmittingXBlockMixin, sub_api
from .step import StepMixin from .mixins import QuestionMixin, XBlockWithTranslationServiceMixin
import uuid import uuid
...@@ -49,7 +49,7 @@ def _(text): ...@@ -49,7 +49,7 @@ def _(text):
# Classes ########################################################### # Classes ###########################################################
class AnswerMixin(object): class AnswerMixin(XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin):
""" """
Mixin to give an XBlock the ability to read/write data to the Answers DB table. Mixin to give an XBlock the ability to read/write data to the Answers DB table.
""" """
...@@ -114,19 +114,18 @@ class AnswerMixin(object): ...@@ -114,19 +114,18 @@ class AnswerMixin(object):
if not data.name: if not data.name:
add_error(u"A Question ID is required.") add_error(u"A Question ID is required.")
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@XBlock.needs("i18n") @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 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 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. to make them searchable and referenceable across xblocks.
""" """
CATEGORY = 'pb-answer'
STUDIO_LABEL = _(u"Long Answer")
name = String( name = String(
display_name=_("Question ID (name)"), 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."), 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): ...@@ -273,6 +272,9 @@ class AnswerRecapBlock(AnswerMixin, StudioEditableXBlockMixin, XBlock):
""" """
A block that displays an answer previously entered by the student (read-only). 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( name = String(
display_name=_("Question ID"), display_name=_("Question ID"),
help=_("The ID of the question for which to display the student's answer."), help=_("The ID of the question for which to display the student's answer."),
......
...@@ -27,7 +27,9 @@ from xblock.core import XBlock ...@@ -27,7 +27,9 @@ from xblock.core import XBlock
from xblock.fields import Scope, String from xblock.fields import Scope, String
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblock.validation import ValidationMessage 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 # Make '_' a no-op so we can scrape strings
...@@ -38,7 +40,7 @@ def _(text): ...@@ -38,7 +40,7 @@ def _(text):
@XBlock.needs("i18n") @XBlock.needs("i18n")
class ChoiceBlock(StudioEditableXBlockMixin, XBlock): class ChoiceBlock(StudioEditableXBlockMixin, XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin, XBlock):
""" """
Custom choice of an answer for a MCQ/MRQ Custom choice of an answer for a MCQ/MRQ
""" """
...@@ -56,10 +58,6 @@ class ChoiceBlock(StudioEditableXBlockMixin, XBlock): ...@@ -56,10 +58,6 @@ class ChoiceBlock(StudioEditableXBlockMixin, XBlock):
) )
editable_fields = ('content', 'value') editable_fields = ('content', 'value')
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@property @property
def display_name_with_default(self): def display_name_with_default(self):
try: try:
......
...@@ -48,6 +48,9 @@ class MCQBlock(SubmittingXBlockMixin, QuestionnaireAbstractBlock): ...@@ -48,6 +48,9 @@ class MCQBlock(SubmittingXBlockMixin, QuestionnaireAbstractBlock):
""" """
An XBlock used to ask multiple-choice questions An XBlock used to ask multiple-choice questions
""" """
CATEGORY = 'pb-mcq'
STUDIO_LABEL = _(u"Multiple Choice Question")
student_choice = String( student_choice = String(
# {Last input submitted by the student # {Last input submitted by the student
default="", default="",
...@@ -158,6 +161,9 @@ class RatingBlock(MCQBlock): ...@@ -158,6 +161,9 @@ class RatingBlock(MCQBlock):
""" """
An XBlock used to rate something on a five-point scale, e.g. Likert Scale 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( low = String(
display_name=_("Low"), display_name=_("Low"),
help=_("Label for low ratings"), help=_("Label for low ratings"),
......
...@@ -26,6 +26,8 @@ from xblock.fields import Scope, String ...@@ -26,6 +26,8 @@ from xblock.fields import Scope, String
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblockutils.studio_editable import StudioEditableXBlockMixin from xblockutils.studio_editable import StudioEditableXBlockMixin
from problem_builder.mixins import XBlockWithTranslationServiceMixin
# Make '_' a no-op so we can scrape strings # Make '_' a no-op so we can scrape strings
def _(text): def _(text):
...@@ -35,7 +37,7 @@ def _(text): ...@@ -35,7 +37,7 @@ def _(text):
@XBlock.needs("i18n") @XBlock.needs("i18n")
class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin): class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin, XBlockWithTranslationServiceMixin):
""" """
A message which can be conditionally displayed at the mentoring block level, A message which can be conditionally displayed at the mentoring block level,
for example upon completion of the block for example upon completion of the block
...@@ -126,10 +128,6 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin): ...@@ -126,10 +128,6 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
) )
editable_fields = ("content", ) editable_fields = ("content", )
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
def mentoring_view(self, context=None): def mentoring_view(self, context=None):
""" Render this message for use by a mentoring block. """ """ Render this message for use by a mentoring block. """
html = u'<div class="submission-message {msg_type}">{content}</div>'.format( html = u'<div class="submission-message {msg_type}">{content}</div>'.format(
...@@ -184,3 +182,23 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin): ...@@ -184,3 +182,23 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
block.content += etree.tostring(child, encoding='unicode') block.content += etree.tostring(child, encoding='unicode')
return block 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): ...@@ -44,6 +44,9 @@ class MRQBlock(QuestionnaireAbstractBlock):
""" """
An XBlock used to ask multiple-response questions An XBlock used to ask multiple-response questions
""" """
CATEGORY = 'pb-mrq'
STUDIO_LABEL = _(u"Multiple Response Question")
student_choices = List( student_choices = List(
# Last submissions by the student # Last submissions by the student
default=[], default=[],
......
/* Display of url_name below content */ /* 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=problem-builder] .url-name-footer,
.xblock[data-block-type=mentoring] .url-name-footer { .xblock[data-block-type=mentoring] .url-name-footer {
font-style: italic; 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=problem-builder] .url-name-footer .url-name,
.xblock[data-block-type=mentoring] .url-name-footer .url-name { .xblock[data-block-type=mentoring] .url-name-footer .url-name {
margin: 0 10px; margin: 0 10px;
...@@ -11,6 +15,8 @@ ...@@ -11,6 +15,8 @@
} }
/* Custom appearance for our "Add" buttons */ /* 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=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 { .xblock[data-block-type=mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button {
width: 200px; width: 200px;
...@@ -18,6 +24,10 @@ ...@@ -18,6 +24,10 @@
line-height: 30px; 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,
.xblock[data-block-type=problem-builder] .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:hover,
.xblock[data-block-type=mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled, .xblock[data-block-type=mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
...@@ -27,6 +37,7 @@ ...@@ -27,6 +37,7 @@
cursor: default; cursor: default;
} }
.xblock[data-block-type=pb-mentoring] .submission-message-help p,
.xblock[data-block-type=problem-builder] .submission-message-help p { .xblock[data-block-type=problem-builder] .submission-message-help p {
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
font-size: 0.85em; 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 ...@@ -29,12 +29,12 @@ from xblock.fragment import Fragment
from xblock.validation import ValidationMessage from xblock.validation import ValidationMessage
from xblockutils.helpers import child_isinstance from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader 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 .choice import ChoiceBlock
from .mentoring import MentoringBlock from .mentoring import MentoringBlock
from .message import MentoringMessageBlock from .message import MentoringMessageBlock
from .step import StepMixin from .mixins import QuestionMixin, XBlockWithTranslationServiceMixin
from .tip import TipBlock from .tip import TipBlock
# Globals ########################################################### # Globals ###########################################################
...@@ -50,7 +50,10 @@ def _(text): ...@@ -50,7 +50,10 @@ def _(text):
@XBlock.needs("i18n") @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 An abstract class used for MCQ/MRQ blocks
...@@ -88,10 +91,6 @@ class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBloc ...@@ -88,10 +91,6 @@ class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBloc
editable_fields = ('question', 'message', 'weight', 'display_name', 'show_title') editable_fields = ('question', 'message', 'weight', 'display_name', 'show_title')
has_children = True has_children = True
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@lazy @lazy
def html_id(self): def html_id(self):
""" """
......
...@@ -18,9 +18,25 @@ ...@@ -18,9 +18,25 @@
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>. # "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.fields import String, Boolean, Scope
from xblock.fragment import Fragment
from xblockutils.helpers import child_isinstance 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 # Make '_' a no-op so we can scrape strings
...@@ -40,81 +56,108 @@ def _normalize_id(key): ...@@ -40,81 +56,108 @@ def _normalize_id(key):
return key return key
class StepParentMixin(object): class HtmlBlockShim(object):
""" CATEGORY = 'html'
An XBlock mixin for a parent block containing Step children STUDIO_LABEL = _(u"HTML")
"""
@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 StepMixin(object): @XBlock.needs('i18n')
class MentoringStepBlock(
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin,
EnumerableChildMixin, XBlock
):
""" """
An XBlock mixin for a child block that is a "Step". An XBlock for a step.
A step is a question that the user can answer (as opposed to a read-only child).
""" """
has_author_view = True CAPTION = _(u"Step")
STUDIO_LABEL = _(u"Mentoring Step")
CATEGORY = 'pb-mentoring-step'
# Fields: # Fields:
display_name = String( display_name = String(
display_name=_("Question title"), display_name=_("Step Title"),
help=_('Leave blank to use the default ("Question 1", "Question 2", etc.)'), help=_('Leave blank to use sequential numbering'),
default="", # Blank will use 'Question x' - see display_name_with_default default="",
scope=Scope.content
)
show_title = Boolean(
display_name=_("Show title"),
help=_("Display the title?"),
default=True,
scope=Scope.content scope=Scope.content
) )
@lazy editable_fields = ('display_name', 'show_title',)
def step_number(self):
return list(self.get_parent().steps).index(_normalize_id(self.scope_ids.usage_id)) + 1
@lazy @lazy
def lonely_step(self): def siblings(self):
if _normalize_id(self.scope_ids.usage_id) not in self.get_parent().steps: return self.get_parent().steps
raise ValueError("Step's parent should contain Step", self, self.get_parent().steps)
return len(self.get_parent().steps) == 1
@property @property
def display_name_with_default(self): def allowed_nested_blocks(self):
""" Get the title/display_name of this question. """ """
if self.display_name: Returns a list of allowed nested XBlocks. Each item can be either
return self.display_name * An XBlock class
if not self.lonely_step: * A NestedXBlockSpec
return self._(u"Question {number}").format(number=self.step_number)
return self._(u"Question") 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
def author_view(self, context): instances
context = context.copy() if context else {} """
context['hide_header'] = True return [
return self.mentoring_view(context) NestedXBlockSpec(AnswerBlock, boilerplate='studio_default'),
MCQBlock, RatingBlock, MRQBlock, HtmlBlockShim,
def author_preview_view(self, context): AnswerRecapBlock, MentoringTableBlock,
context = context.copy() if context else {} ]
context['hide_header'] = True
return self.student_view(context) @property
def steps(self):
def assessment_step_view(self, context=None): """ 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 Add some HTML to the author view that allows authors to add child blocks.
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) 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 ...@@ -30,10 +30,11 @@ from xblock.fields import Scope, String, Boolean, Dict
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContainerXBlockMixin from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContainerXBlockMixin, XBlockWithPreviewMixin
# Globals ########################################################### # Globals ###########################################################
from problem_builder import AnswerRecapBlock
from problem_builder.answer import AnswerRecapBlock
from problem_builder.dashboard import ExportMixin from problem_builder.dashboard import ExportMixin
from problem_builder.models import Share from problem_builder.models import Share
from problem_builder.sub_api import SubmittingXBlockMixin from problem_builder.sub_api import SubmittingXBlockMixin
...@@ -51,7 +52,8 @@ def _(text): ...@@ -51,7 +52,8 @@ def _(text):
@XBlock.wants("user") @XBlock.wants("user")
@XBlock.wants("submissions") @XBlock.wants("submissions")
class MentoringTableBlock( class MentoringTableBlock(
StudioEditableXBlockMixin, SubmittingXBlockMixin, StudioContainerXBlockMixin, ExportMixin, XBlock StudioEditableXBlockMixin, SubmittingXBlockMixin, StudioContainerXBlockMixin, ExportMixin, XBlock,
XBlockWithPreviewMixin
): ):
""" """
Table-type display of information from mentoring blocks Table-type display of information from mentoring blocks
...@@ -59,6 +61,9 @@ class MentoringTableBlock( ...@@ -59,6 +61,9 @@ class MentoringTableBlock(
Used to present summary of information entered by the students in mentoring blocks. Used to present summary of information entered by the students in mentoring blocks.
Supports different types of formatting through the `type` parameter. Supports different types of formatting through the `type` parameter.
""" """
CATEGORY = 'pb-table'
STUDIO_LABEL = _(u"Answer Recap Table")
display_name = String( display_name = String(
display_name=_("Display name"), display_name=_("Display name"),
help=_("Title of the table"), 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 import unittest
from problem_builder.step import StepMixin, StepParentMixin from problem_builder.mixins import QuestionMixin, StepParentMixin
from mock import Mock from mock import Mock
...@@ -32,7 +32,7 @@ class BaseClass(object): ...@@ -32,7 +32,7 @@ class BaseClass(object):
pass pass
class Step(BaseClass, StepMixin): class Step(BaseClass, QuestionMixin):
def __init__(self): def __init__(self):
pass pass
...@@ -41,7 +41,7 @@ class NotAStep(object): ...@@ -41,7 +41,7 @@ class NotAStep(object):
pass pass
class TestStepMixin(unittest.TestCase): class TestQuestionMixin(unittest.TestCase):
def test_single_step_is_returned_correctly(self): def test_single_step_is_returned_correctly(self):
block = Parent() block = Parent()
step = Step() step = Step()
...@@ -77,18 +77,18 @@ class TestStepMixin(unittest.TestCase): ...@@ -77,18 +77,18 @@ class TestStepMixin(unittest.TestCase):
self.assertEquals(step1.step_number, 2) self.assertEquals(step1.step_number, 2)
self.assertEquals(step2.step_number, 1) 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() block = Parent()
step1 = Step() step1 = Step()
block._set_children_for_test(1, "2", step1, "Step", NotAStep(), False) 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() block = Parent()
step1 = Step() step1 = Step()
step2 = Step() step2 = Step()
block._set_children_for_test(1, step2, "2", step1, "Step", NotAStep(), False) block._set_children_for_test(1, step2, "2", step1, "Step", NotAStep(), False)
self.assertFalse(step1.lonely_step) self.assertFalse(step1.lonely_child)
self.assertFalse(step2.lonely_step) self.assertFalse(step2.lonely_child)
...@@ -30,6 +30,8 @@ from xblock.validation import ValidationMessage ...@@ -30,6 +30,8 @@ from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin from xblockutils.studio_editable import StudioEditableXBlockMixin
from problem_builder.mixins import XBlockWithTranslationServiceMixin
loader = ResourceLoader(__name__) loader = ResourceLoader(__name__)
...@@ -41,7 +43,7 @@ def _(text): ...@@ -41,7 +43,7 @@ def _(text):
@XBlock.needs("i18n") @XBlock.needs("i18n")
class TipBlock(StudioEditableXBlockMixin, XBlock): class TipBlock(StudioEditableXBlockMixin, XBlockWithTranslationServiceMixin, XBlock):
""" """
Each choice can define a tip depending on selection Each choice can define a tip depending on selection
""" """
...@@ -73,10 +75,6 @@ class TipBlock(StudioEditableXBlockMixin, XBlock): ...@@ -73,10 +75,6 @@ class TipBlock(StudioEditableXBlockMixin, XBlock):
) )
editable_fields = ('values', 'content', 'width', 'height') editable_fields = ('values', 'content', 'width', 'height')
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@property @property
def display_name_with_default(self): def display_name_with_default(self):
values_list = [] values_list = []
......
ddt ddt
mock mock
unicodecsv==0.9.4 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 . -e .
...@@ -41,6 +41,8 @@ def package_data(pkg, root_list): ...@@ -41,6 +41,8 @@ def package_data(pkg, root_list):
BLOCKS = [ BLOCKS = [
'problem-builder = problem_builder:MentoringBlock', 'problem-builder = problem_builder:MentoringBlock',
'pb-mentoring = problem_builder:MentoringWithExplicitStepsBlock',
'pb-mentoring-step = problem_builder:MentoringStepBlock',
'pb-table = problem_builder:MentoringTableBlock', 'pb-table = problem_builder:MentoringTableBlock',
'pb-column = problem_builder:MentoringTableColumn', '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