Commit 5f13249c by Braden MacDonald

i18n

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