Commit 5f13249c by Braden MacDonald

i18n

parent dd7def1c
......@@ -43,6 +43,11 @@ import uuid
log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
# Make '_' a no-op so we can scrape strings
def _(text):
return text
# Classes ###########################################################
......@@ -94,7 +99,12 @@ 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(AnswerMixin, StepMixin, StudioEditableXBlockMixin, XBlock):
"""
A field where the student enters an answer
......@@ -103,32 +113,32 @@ class AnswerBlock(AnswerMixin, StepMixin, StudioEditableXBlockMixin, XBlock):
to make them searchable and referenceable across xblocks.
"""
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.",
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."),
default="",
scope=Scope.content
)
default_from = String(
display_name="Default From",
help="If a question ID is specified, get the default value from this answer.",
display_name=_("Default From"),
help=_("If a question ID is specified, get the default value from this answer."),
default=None,
scope=Scope.content
)
min_characters = Integer(
display_name="Min. Allowed Characters",
help="Minimum number of characters allowed for the answer",
display_name=_("Min. Allowed Characters"),
help=_("Minimum number of characters allowed for the answer"),
default=0,
scope=Scope.content
)
question = String(
display_name="Question",
help="Question to ask the student",
display_name=_("Question"),
help=_("Question to ask the student"),
scope=Scope.content,
default=""
)
weight = Float(
display_name="Weight",
help="Defines the maximum total grade of the answer block.",
display_name=_("Weight"),
help=_("Defines the maximum total grade of the answer block."),
default=1,
scope=Scope.settings,
enforce_type=True
......@@ -138,7 +148,9 @@ class AnswerBlock(AnswerMixin, StepMixin, StudioEditableXBlockMixin, XBlock):
@property
def studio_display_name(self):
return u"Question {}".format(self.step_number) if not self.lonely_step else u"Question"
if not self.lonely_step:
return self._(u"Question {number}").format(number=self.step_number)
return self._(u"Question")
def __getattribute__(self, name):
""" Provide a read-only display name without adding a display_name field to the class. """
......@@ -227,26 +239,27 @@ class AnswerBlock(AnswerMixin, StepMixin, StudioEditableXBlockMixin, XBlock):
return {'metadata': {}, 'data': {}}
@XBlock.needs("i18n")
class AnswerRecapBlock(AnswerMixin, StudioEditableXBlockMixin, XBlock):
"""
A block that displays an answer previously entered by the student (read-only).
"""
name = String(
display_name="Question ID",
help="The ID of the question for which to display the student's answer.",
display_name=_("Question ID"),
help=_("The ID of the question for which to display the student's answer."),
scope=Scope.content,
)
display_name = String(
display_name="Title",
help="Title of this answer recap section",
display_name=_("Title"),
help=_("Title of this answer recap section"),
scope=Scope.content,
default="",
)
description = String(
help="Description of this answer (optional). Can include HTML.",
display_name=_("Description"),
help=_("Description of this answer (optional). Can include HTML."),
scope=Scope.content,
default="",
display_name="Description",
)
editable_fields = ('name', 'display_name', 'description')
......
......@@ -32,34 +32,44 @@ from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.studio_editable import StudioEditableXBlockMixin
# Make '_' a no-op so we can scrape strings
def _(text):
return text
# Classes ###########################################################
@XBlock.needs("i18n")
class ChoiceBlock(StudioEditableXBlockMixin, XBlock):
"""
Custom choice of an answer for a MCQ/MRQ
"""
value = String(
display_name="Value",
help="Value of the choice when selected. Should be unique.",
display_name=_("Value"),
help=_("Value of the choice when selected. Should be unique."),
scope=Scope.content,
default="",
)
content = String(
display_name="Choice Text",
help="Human-readable version of the choice value",
display_name=_("Choice Text"),
help=_("Human-readable version of the choice value"),
scope=Scope.content,
default="",
)
editable_fields = ('content', )
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@property
def studio_display_name(self):
try:
status = self.get_parent().describe_choice_correctness(self.value)
except Exception:
status = u"Out of Context" # Parent block should implement describe_choice_correctness()
return u"Choice ({})".format(status)
status = self._(u"Out of Context") # Parent block should implement describe_choice_correctness()
return self._(u"Choice ({status})").format(status=status)
def __getattribute__(self, name):
""" Provide a read-only display name without adding a display_name field to the class. """
......@@ -80,9 +90,9 @@ class ChoiceBlock(StudioEditableXBlockMixin, XBlock):
validation.add(ValidationMessage(ValidationMessage.ERROR, msg))
if not data.value.strip():
add_error(u"No value set. This choice will not work correctly.")
add_error(self._(u"No value set. This choice will not work correctly."))
if not data.content.strip():
add_error(u"No choice text set yet.")
add_error(self._(u"No choice text set yet."))
def validate(self):
"""
......@@ -91,7 +101,7 @@ class ChoiceBlock(StudioEditableXBlockMixin, XBlock):
validation = super(ChoiceBlock, self).validate()
if self.get_parent().all_choice_values.count(self.value) > 1:
validation.add(
ValidationMessage(ValidationMessage.ERROR, (
ValidationMessage(ValidationMessage.ERROR, self._(
u"This choice has a non-unique ID and won't work properly. "
"This can happen if you duplicate a choice rather than use the Add Choice button."
))
......
......@@ -39,17 +39,26 @@ log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
# Make '_' a no-op so we can scrape strings
def _(text):
return text
# Classes ###########################################################
class MCQBlock(QuestionnaireAbstractBlock):
"""
An XBlock used to ask multiple-choice questions
"""
student_choice = String(help="Last input submitted by the student", default="", scope=Scope.user_state)
student_choice = String(
# {Last input submitted by the student
default="",
scope=Scope.user_state,
)
correct_choices = List(
display_name="Correct Choice[s]",
help="Specify the value[s] that students may select for this question to be considered correct.",
display_name=_("Correct Choice[s]"),
help=_("Specify the value[s] that students may select for this question to be considered correct."),
scope=Scope.content,
list_values_provider=QuestionnaireAbstractBlock.choice_values_provider,
list_style='set', # Underered, unique items. Affects the UI editor.
......@@ -59,12 +68,12 @@ class MCQBlock(QuestionnaireAbstractBlock):
def describe_choice_correctness(self, choice_value):
if choice_value in self.correct_choices:
if len(self.correct_choices) == 1:
return u"Correct"
return u"Acceptable"
return self._(u"Correct")
return self._(u"Acceptable")
else:
if len(self.correct_choices) == 1:
return u"Wrong"
return u"Not Acceptable"
return self._(u"Wrong")
return self._(u"Not Acceptable")
def submit(self, submission):
log.debug(u'Received MCQ submission: "%s"', submission)
......@@ -121,25 +130,39 @@ class MCQBlock(QuestionnaireAbstractBlock):
correct = set(data.correct_choices)
if not all_values:
add_error(u"No choices set yet.")
add_error(self._(u"No choices set yet."))
elif not correct:
add_error(u"You must indicate the correct answer[s], or the student will always get this question wrong.")
add_error(
self._(u"You must indicate the correct answer[s], or the student will always get this question wrong.")
)
if len(correct) < len(data.correct_choices):
add_error(u"Duplicate correct choices set")
add_error(self._(u"Duplicate correct choices set"))
for val in (correct - all_values):
add_error(u"A choice value listed as correct does not exist: {}".format(choice_name(val)))
add_error(
self._(u"A choice value listed as correct does not exist: {choice}").format(choice=choice_name(val))
)
class RatingBlock(MCQBlock):
"""
An XBlock used to rate something on a five-point scale, e.g. Likert Scale
"""
low = String(help="Label for low ratings", scope=Scope.content, default="Less")
high = String(help="Label for high ratings", scope=Scope.content, default="More")
low = String(
display_name=_("Low"),
help=_("Label for low ratings"),
scope=Scope.content,
default=_("Less"),
)
high = String(
display_name=_("High"),
help=_("Label for high ratings"),
scope=Scope.content,
default=_("More"),
)
FIXED_VALUES = ["1", "2", "3", "4", "5"]
correct_choices = List(
display_name="Accepted Choice[s]",
help="Specify the rating value[s] that students may select for this question to be considered correct.",
display_name=_("Accepted Choice[s]"),
help=_("Specify the rating value[s] that students may select for this question to be considered correct."),
scope=Scope.content,
default=FIXED_VALUES,
list_values_provider=QuestionnaireAbstractBlock.choice_values_provider,
......
......@@ -49,6 +49,11 @@ _default_theme_config = {
'locations': ['public/themes/lms.css']
}
# Make '_' a no-op so we can scrape strings
def _(text):
return text
# Classes ###########################################################
Score = namedtuple("Score", ["raw", "percentage", "correct", "incorrect", "partially_correct"])
......@@ -68,38 +73,42 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
# Content
MENTORING_MODES = ('standard', 'assessment')
mode = String(
display_name="Mode",
help="Mode of the mentoring. 'standard' or 'assessment'",
display_name=_("Mode"),
help=_("Mode of the mentoring. 'standard' or 'assessment'"),
default='standard',
scope=Scope.content,
values=MENTORING_MODES
)
followed_by = String(
help="url_name of the step after the current mentoring block in workflow.",
display_name=_("Followed by"),
help=_("url_name of the step after the current mentoring block in workflow."),
default=None,
scope=Scope.content
)
max_attempts = Integer(
help="Number of max attempts for this questions",
display_name=_("Max. Attempts Allowed"),
help=_("Number of max attempts allowed for this questions"),
default=0,
scope=Scope.content,
enforce_type=True
)
enforce_dependency = Boolean(
help="Should the next step be the current block to complete?",
display_name=_("Enforce Dependency"),
help=_("Should the next step be the current block to complete?"),
default=False,
scope=Scope.content,
enforce_type=True
)
display_submit = Boolean(
help="Allow submission of the current block?",
display_name=_("Show Submit Button"),
help=_("Allow submission of the current block?"),
default=True,
scope=Scope.content,
enforce_type=True
)
xml_content = String(
help="Not used for version 2. This field is here only to preserve the data needed to upgrade from v1 to v2.",
display_name="XML content",
display_name=_("XML content"),
help=_("Not used for version 2. This field is here only to preserve the data needed to upgrade from v1 to v2."),
default='',
scope=Scope.content,
multiline_editor=True
......@@ -107,56 +116,58 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
# Settings
weight = Float(
help="Defines the maximum total grade of the block.",
display_name=_("Weight"),
help=_("Defines the maximum total grade of the block."),
default=1,
scope=Scope.settings,
enforce_type=True
)
display_name = String(
help="Title to display",
default="Mentoring Questions",
display_name=_("Title (Display name)"),
help=_("Title to display"),
default=_("Mentoring Questions"),
scope=Scope.settings
)
# User state
attempted = Boolean(
help="Has the student attempted this mentoring step?",
# Has the student attempted this mentoring step?
default=False,
scope=Scope.user_state
)
completed = Boolean(
help="Has the student completed this mentoring step?",
# Has the student completed this mentoring step?
default=False,
scope=Scope.user_state
)
num_attempts = Integer(
help="Number of attempts a user has answered for this questions",
# Number of attempts a user has answered for this questions
default=0,
scope=Scope.user_state,
enforce_type=True
)
step = Integer(
help="Keep track of the student assessment progress.",
# Keep track of the student assessment progress.
default=0,
scope=Scope.user_state,
enforce_type=True
)
student_results = List(
help="Store results of student choices.",
# Store results of student choices.
default=[],
scope=Scope.user_state
)
# Global user state
next_step = String(
help="url_name of the next step the student must complete (global to all blocks)",
# url_name of the next step the student must complete (global to all blocks)
default='mentoring_first',
scope=Scope.preferences
)
editable_fields = (
'mode', 'followed_by', 'max_attempts', 'enforce_dependency',
'display_submit', 'weight', 'display_name',
'display_name', 'mode', 'followed_by', 'max_attempts', 'enforce_dependency',
'display_submit', 'weight',
)
icon_class = 'problem'
has_score = True
......
......@@ -29,24 +29,30 @@ from xblock.fields import Scope, String
from xblock.fragment import Fragment
from xblockutils.studio_editable import StudioEditableXBlockMixin
# Make '_' a no-op so we can scrape strings
def _(text):
return text
# Classes ###########################################################
@XBlock.needs("i18n")
class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
"""
A message which can be conditionally displayed at the mentoring block level,
for example upon completion of the block
"""
content = String(
display_name="Message",
help="Message to display upon completion",
display_name=_("Message"),
help=_("Message to display upon completion"),
scope=Scope.content,
default="",
multiline_editor="html",
resettable_editor=False,
)
type = String(
help="Type of message",
help=_("Type of message"),
scope=Scope.content,
default="completed",
values=(
......@@ -57,6 +63,10 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
)
editable_fields = ("content", )
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
def fallback_view(self, view_name, context):
html = u'<div class="message {msg_type}">{content}</div>'.format(msg_type=self.type, content=self.content)
return Fragment(html)
......@@ -65,13 +75,13 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
def studio_display_name(self):
if self.type == 'max_attempts_reached':
max_attempts = self.get_parent().max_attempts
return u"Message when student reaches max. # of attempts ({current_limit})".format(
current_limit=u"unlimited" if max_attempts == 0 else max_attempts
return self._(u"Message when student reaches max. # of attempts ({limit})").format(
limit=self._(u"unlimited") if max_attempts == 0 else max_attempts
)
if self.type == 'completed':
return u"Message shown when complete"
return self._(u"Message shown when complete")
if self.type == 'incomplete':
return u"Message shown when incomplete"
return self._(u"Message shown when incomplete")
return u"INVALID MESSAGE"
def __getattribute__(self, name):
......
......@@ -36,24 +36,33 @@ from xblockutils.resources import ResourceLoader
log = logging.getLogger(__name__)
# Make '_' a no-op so we can scrape strings
def _(text):
return text
# Classes ###########################################################
class MRQBlock(QuestionnaireAbstractBlock):
"""
An XBlock used to ask multiple-response questions
"""
student_choices = List(help="Last submissions by the student", default=[], scope=Scope.user_state)
student_choices = List(
# Last submissions by the student
default=[],
scope=Scope.user_state
)
required_choices = List(
display_name="Required Choices",
help=("Specify the value[s] that students must select for this MRQ to be considered correct. "),
display_name=_("Required Choices"),
help=_("Specify the value[s] that students must select for this MRQ to be considered correct."),
scope=Scope.content,
list_values_provider=QuestionnaireAbstractBlock.choice_values_provider,
list_style='set', # Underered, unique items. Affects the UI editor.
default=[],
)
ignored_choices = List(
display_name="Ignored Choices",
help=(
display_name=_("Ignored Choices"),
help=_(
"Specify the value[s] that are neither correct nor incorrect. "
"Any values not listed as required or ignored will be considered wrong."
),
......@@ -67,10 +76,10 @@ class MRQBlock(QuestionnaireAbstractBlock):
def describe_choice_correctness(self, choice_value):
if choice_value in self.required_choices:
return u"Required"
return self._(u"Required")
elif choice_value in self.ignored_choices:
return u"Ignored"
return u"Not Acceptable"
return self._(u"Ignored")
return self._(u"Not Acceptable")
def submit(self, submissions):
log.debug(u'Received MRQ submissions: "%s"', submissions)
......@@ -144,12 +153,12 @@ class MRQBlock(QuestionnaireAbstractBlock):
ignored = set(data.ignored_choices)
if len(required) < len(data.required_choices):
add_error(u"Duplicate required choices set")
add_error(self._(u"Duplicate required choices set"))
if len(ignored) < len(data.ignored_choices):
add_error(u"Duplicate ignored choices set")
add_error(self._(u"Duplicate ignored choices set"))
for val in required.intersection(ignored):
add_error(u"A choice is listed as both required and ignored: {}".format(choice_name(val)))
add_error(self._(u"A choice is listed as both required and ignored: {}").format(choice_name(val)))
for val in (required - all_values):
add_error(u"A choice value listed as required does not exist: {}".format(choice_name(val)))
add_error(self._(u"A choice value listed as required does not exist: {}").format(choice_name(val)))
for val in (ignored - all_values):
add_error(u"A choice value listed as ignored does not exist: {}".format(choice_name(val)))
add_error(self._(u"A choice value listed as ignored does not exist: {}").format(choice_name(val)))
function MentoringBlock(runtime, element) {
// Set up gettext in case it isn't available in the client runtime:
if (typeof gettext == "undefined") {
window.gettext = function gettext_stub(string) { return string; };
window.ngettext = function ngettext_stub(strA, strB, n) { return n == 1 ? strA : strB; };
}
var attemptsTemplate = _.template($('#xblock-attempts-template').html());
var data = $('.mentoring', element).data();
var children = runtime.children(element);
......
......@@ -25,7 +25,7 @@ function MentoringStandardView(runtime, element, mentoring) {
// Messages should only be displayed upon hitting 'submit', not on page reload
mentoring.setContent(messagesDOM, results.message);
if (messagesDOM.html().trim()) {
messagesDOM.prepend('<div class="title1">Feedback</div>');
messagesDOM.prepend('<div class="title1">' + gettext('Feedback') + '</div>');
messagesDOM.show();
}
......
......@@ -41,9 +41,15 @@ from .tip import TipBlock
loader = ResourceLoader(__name__)
# Make '_' a no-op so we can scrape strings
def _(text):
return text
# Classes ###########################################################
@XBlock.needs("i18n")
class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBlockMixin, StepMixin, XBlock):
"""
An abstract class used for MCQ/MRQ blocks
......@@ -54,26 +60,26 @@ class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBloc
"""
name = String(
# This doesn't need to be a field but is kept for backwards compatibility with v1 student data
display_name="Question ID (name)",
help="The ID of this question (required). Should be unique within this mentoring component.",
display_name=_("Question ID (name)"),
help=_("The ID of this question (required). Should be unique within this mentoring component."),
default=UNIQUE_ID,
scope=Scope.settings, # Must be scope.settings, or the unique ID will change every time this block is edited
)
question = String(
display_name="Question",
help="Question to ask the student",
display_name=_("Question"),
help=_("Question to ask the student"),
scope=Scope.content,
default=""
)
message = String(
display_name="Message",
help="General feedback provided when submiting",
display_name=_("Message"),
help=_("General feedback provided when submiting"),
scope=Scope.content,
default=""
)
weight = Float(
display_name="Weight",
help="Defines the maximum total grade of this question.",
display_name=_("Weight"),
help=_("Defines the maximum total grade of this question."),
default=1,
scope=Scope.content,
enforce_type=True
......@@ -81,6 +87,10 @@ class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBloc
editable_fields = ('question', 'message', 'weight')
has_children = True
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
"""
......@@ -108,7 +118,9 @@ class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBloc
@property
def studio_display_name(self):
return u"Question {}".format(self.step_number) if not self.lonely_step else u"Question"
if not self.lonely_step:
return self._(u"Question {number}").format(number=self.step_number)
return self._(u"Question")
def __getattribute__(self, name):
""" Provide a read-only display name without adding a display_name field to the class. """
......@@ -215,9 +227,9 @@ class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBloc
def add_error(msg):
validation.add(ValidationMessage(ValidationMessage.ERROR, msg))
if not data.name:
add_error(u"A unique Question ID is required.")
add_error(self._(u"A unique Question ID is required."))
elif ' ' in data.name:
add_error(u"Question ID should not contain spaces.")
add_error(self._(u"Question ID should not contain spaces."))
def validate(self):
"""
......@@ -232,12 +244,12 @@ class QuestionnaireAbstractBlock(StudioEditableXBlockMixin, StudioContainerXBloc
all_choice_values = self.all_choice_values
all_choice_values_set = set(all_choice_values)
if len(all_choice_values) != len(all_choice_values_set):
add_error(u"Some choice values are not unique.")
add_error(self._(u"Some choice values are not unique."))
# Validate the tips:
values_with_tips = set()
for tip in self.get_tips():
values = set(tip.values)
for val in (values & values_with_tips):
add_error(u"Multiple tips for value '{}'".format(val))
for dummy in (values & values_with_tips):
add_error(self._(u"Multiple tips configured for the same choice."))
values_with_tips.update(values)
return validation
......@@ -27,7 +27,6 @@ import errno
from xblock.core import XBlock
from xblock.exceptions import NoSuchViewError
from xblock.fields import Scope, String
from xblock.fragment import Fragment
......@@ -38,6 +37,11 @@ from xblockutils.studio_editable import StudioEditableXBlockMixin, StudioContain
loader = ResourceLoader(__name__)
# Make '_' a no-op so we can scrape strings
def _(text):
return text
# Classes ###########################################################
......@@ -49,14 +53,14 @@ class MentoringTableBlock(StudioEditableXBlockMixin, StudioContainerXBlockMixin,
Supports different types of formatting through the `type` parameter.
"""
display_name = String(
display_name="Display name",
help="Title of the table",
default="Answers Table",
display_name=_("Display name"),
help=_("Title of the table"),
default=_("Answers Table"),
scope=Scope.settings
)
type = String(
display_name="Special Mode",
help="Variant of the table that will display a specific background image.",
display_name=_("Special Mode"),
help=_("Variant of the table that will display a specific background image."),
scope=Scope.content,
default='',
values=[
......@@ -122,10 +126,10 @@ class MentoringTableColumn(StudioEditableXBlockMixin, StudioContainerXBlockMixin
"""
A column in a mentoring table. Has a header and can contain HTML and AnswerRecapBlocks.
"""
display_name = String(display_name="Display Name", default="Column")
display_name = String(display_name=_("Display Name"), default="Column")
header = String(
display_name="Header",
help="Header of this column",
display_name=_("Header"),
help=_("Header of this column"),
default="",
scope=Scope.content,
multiline_editor="html",
......
{% load i18n %}
<div class="mentoring themed-xblock" data-mode="{{ self.mode }}" data-step="{{ self.step }}">
<div class="missing-dependency warning" data-missing="{{ self.has_missing_dependency }}">
You need to complete <a href="{{ missing_dependency_url }}">the previous step</a> before
{% with url=missing_dependency_url|safe %}
{% blocktrans with link_start="<a href='"|add:url|add:"'>" link_end="</a>" %}
You need to complete {{link_start}}the previous step{{link_end}} before
attempting this step.
{% endblocktrans %}
{% endwith %}
</div>
{% if title %}
......
......@@ -2,18 +2,18 @@
<div class="add-xblock-component new-component-item adding">
<div class="new-component">
<h5>Add New Component</h5>
<h5>{% trans "Add New Component" %}</h5>
<ul class="new-component-type">
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-answer" data-boilerplate="studio_default">Long Answer</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-mcq">Multiple Choice Question</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-rating">Rating Question</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-mrq">Multiple Response Question</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="html">HTML</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-answer-recap">Long Answer Recap</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-table">Answer Recap Table</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-message" data-boilerplate="completed">Message (Complete)</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-message" data-boilerplate="incomplete">Message (Incomplete)</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-message" data-boilerplate="max_attempts_reached">Message (Max # Attempts)</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-answer" data-boilerplate="studio_default">{% trans "Long Answer" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-mcq">{% trans "Multiple Choice Question" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-rating">{% trans "Rating Question" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-mrq">{% trans "Multiple Response Question" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="html">{% trans "HTML" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-answer-recap">{% trans "Long Answer Recap" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-table">{% trans "Answer Recap Table" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-message" data-boilerplate="completed">{% trans "Message (Complete)" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-message" data-boilerplate="incomplete">{% trans "Message (Incomplete)" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-message" data-boilerplate="max_attempts_reached">{% trans "Message (Max # Attempts)" %}</a></li>
</ul>
</div>
</div>
<script type="text/template" id="xblock-attempts-template">
<% if (_.isNumber(max_attempts) && max_attempts > 0) {{ %>
<span> You have used <%= _.min([num_attempts, max_attempts]) %> of <%= max_attempts %> submissions.</span>
<span>
<%= _.template(
ngettext(
"You have used {num_used} of 1 submission.",
"You have used {num_used} of {max_attempts} submissions.",
max_attempts
), {num_used: _.min([num_attempts, max_attempts]), max_attempts: max_attempts}, {interpolate: /\{(.+?)\}/g}
)
%>
</span>
<% }} %>
</script>
<script type="text/template" id="xblock-grade-template">
<% if (_.isNumber(max_attempts) && max_attempts > 0 && num_attempts >= max_attempts) {{ %>
<p>Note: you have used all attempts. Continue to the next unit.</p>
<p><%= gettext("Note: you have used all attempts. Continue to the next unit.") %></p>
<% }} else {{ %>
<p>Note: if you retake this assessment, only your final score counts.</p>
<p><%= gettext("Note: if you retake this assessment, only your final score counts.") %></p>
<% }} %>
<h2>You scored <%= score %>% on this assessment.</h2>
<h2>
<%= _.template(gettext("You scored {percent}% on this assessment."), {percent: score}, {interpolate: /\{(.+?)\}/g}) %>
</h2>
<hr/>
<span class="assessment-checkmark icon-2x checkmark-correct icon-ok fa fa-check"></span>
<p>You answered <%= correct_answer %> questions correctly.</p>
<p>
<%= _.template(
ngettext(
"You answered 1 question correctly.",
"You answered {number_correct} questions correctly.",
correct_answer
), {number_correct: correct_answer}, {interpolate: /\{(.+?)\}/g})
%>
</p>
<span class="assessment-checkmark icon-2x checkmark-partially-correct icon-ok fa fa-check"></span>
<p>You answered <%= partially_correct_answer %> questions partially correct.</p>
<p>
<%= _.template(
ngettext(
"You answered 1 question partially correctly.",
"You answered {number_partially_correct} questions partially correctly.",
partially_correct_answer
), {number_partially_correct: partially_correct_answer}, {interpolate: /\{(.+?)\}/g})
%>
</p>
<span class="assessment-checkmark icon-2x checkmark-incorrect icon-exclamation fa fa-exclamation"></span>
<p>You answered <%= incorrect_answer %> questions incorrectly.</p>
<p>
<%= _.template(
ngettext(
"You answered 1 question incorrectly.",
"You answered {number_incorrect} questions incorrectly.",
incorrect_answer
), {number_incorrect: incorrect_answer}, {interpolate: /\{(.+?)\}/g})
%>
</p>
</script>
......@@ -3,8 +3,8 @@
<div class="add-xblock-component new-component-item adding">
<div class="new-component">
<ul class="new-component-type">
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-choice" data-boilerplate="studio_default">Add Custom Choice</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-tip">Add Tip</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-choice" data-boilerplate="studio_default">{% trans "Add Custom Choice" %}</a></li>
<li><a href="#" class="single-template add-xblock-component-button" data-category="mentoring-tip">{% trans "Add Tip" %}</a></li>
</ul>
</div>
</div>
{% load i18n %}
<p>{{ question }}</p>
<h2>Built-in choices:</h2>
<h2>{% trans "Built-in choices:" %}</h2>
<ul>
<li>Choice (1): <strong>1 - {{ low }}</strong> ({{accepted_statuses.1}})</li>
......@@ -10,4 +11,4 @@
<li>Choice (5): <strong>5 - {{ high }}</strong> ({{accepted_statuses.5}})</li>
</ul>
<h2>Additional custom choices and tips:</h2>
<h2>{% trans "Additional custom choices and tips:" %}</h2>
......@@ -213,10 +213,25 @@ class MentoringAssessmentTest(MentoringAssessmentBaseTest):
self.assert_persistent_elements_present(mentoring)
if expected["num_attempts"] < expected["max_attempts"]:
self.assertIn("Note: if you retake this assessment, only your final score counts.", mentoring.text)
self.assertIn("You answered {correct} questions correctly.".format(**expected), mentoring.text)
self.assertIn("You answered {partial} questions partially correct.".format(**expected), mentoring.text)
self.assertIn("You answered {incorrect} questions incorrectly.".format(**expected), mentoring.text)
self.assertIn("You have used {num_attempts} of {max_attempts} submissions.".format(**expected), mentoring.text)
if expected["correct"] == 1:
self.assertIn("You answered 1 questions correctly.".format(**expected), mentoring.text)
else:
self.assertIn("You answered {correct} questions correctly.".format(**expected), mentoring.text)
if expected["partial"] == 1:
self.assertIn("You answered 1 question partially correctly.", mentoring.text)
else:
self.assertIn("You answered {partial} questions partially correctly.".format(**expected), mentoring.text)
if expected["incorrect"] == 1:
self.assertIn("You answered 1 question incorrectly.", mentoring.text)
else:
self.assertIn("You answered {incorrect} questions incorrectly.".format(**expected), mentoring.text)
if expected["max_attempts"] == 1:
self.assertIn("You have used {num_attempts} of 1 submission.".format(**expected), mentoring.text)
else:
self.assertIn(
"You have used {num_attempts} of {max_attempts} submissions.".format(**expected),
mentoring.text
)
self.assert_hidden(controls.submit)
self.assert_hidden(controls.next_question)
......
......@@ -33,26 +33,51 @@ from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
# Make '_' a no-op so we can scrape strings
def _(text):
return text
# Classes ###########################################################
@XBlock.needs("i18n")
class TipBlock(StudioEditableXBlockMixin, XBlock):
"""
Each choice can define a tip depending on selection
"""
content = String(help="Text of the tip to provide if needed", scope=Scope.content, default="")
content = String(
display_name=_("Content"),
help=_("Text of the tip to show if the student chooses this tip's associated choice[s]"),
scope=Scope.content,
default=""
)
values = List(
display_name="For Choices",
help="List of choices for which to display this tip",
display_name=_("For Choices"),
help=_("List of choices for which to display this tip"),
scope=Scope.content,
default=[],
list_values_provider=lambda self: self.get_parent().human_readable_choices,
list_style='set', # Underered, unique items. Affects the UI editor.
)
width = String(help="Width of the tip popup", scope=Scope.content, default='')
height = String(help="Height of the tip popup", scope=Scope.content, default='')
width = String(
display_name=_("Width"),
help=_("Width of the tip popup (e.g. '400px')"),
scope=Scope.content,
default=''
)
height = String(
display_name=_("Height"),
help=_("Height of the tip popup (e.g. '200px')"),
scope=Scope.content,
default=''
)
editable_fields = ('values', 'content', 'width', 'height')
def _(self, text):
""" translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@property
def studio_display_name(self):
values_list = []
......@@ -62,7 +87,7 @@ class TipBlock(StudioEditableXBlockMixin, XBlock):
if len(display_name) > 20:
display_name = display_name[:20] + u'…'
values_list.append(display_name)
return u"Tip for {}".format(u", ".join(values_list))
return self._(u"Tip for {list_of_choices}").format(list_of_choices=u", ".join(values_list))
def __getattribute__(self, name):
""" Provide a read-only display name without adding a display_name field to the class. """
......@@ -99,8 +124,8 @@ class TipBlock(StudioEditableXBlockMixin, XBlock):
except Exception:
pass
else:
for val in set(data.values) - valid_values:
add_error(u"A choice value listed for this tip does not exist: {}".format(val))
for dummy in set(data.values) - valid_values:
add_error(self._(u"A choice selected for this tip does not exist."))
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
......
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