i18n for self-assessment (ORA-314)

make openendedchild and open_ended_module_v1 act enough like xblocks to get xblock 18n
This commit is contained in:
Adam Palay
2014-02-11 10:46:20 -05:00
parent d6f1ddd84d
commit 97f5b25e2c
21 changed files with 909 additions and 143 deletions

View File

@@ -34,18 +34,31 @@ ACCEPT_FILE_UPLOAD = False
# Contains all reasonable bool and case combinations of True
TRUE_DICT = ["True", True, "TRUE", "true"]
_ = lambda text: text
HUMAN_TASK_TYPE = {
'selfassessment': "Self",
# Translators: "Self" is used to denote an openended response that is self-graded
'selfassessment': _("Self"),
'openended': "edX",
'ml_grading.conf': "AI",
'peer_grading.conf': "Peer",
# Translators: "AI" is used to denote an openended response that is machine-graded
'ml_grading.conf': _("AI"),
# Translators: "Peer" is used to denote an openended response that is peer-graded
'peer_grading.conf': _("Peer"),
}
HUMAN_STATES = {
'intitial': "Not started.",
'assessing': "Being scored.",
'intermediate_done': "Scoring finished.",
'done': "Complete.",
# Translators: "Not started" is used to communicate to a student that their response
# has not yet been graded
'intitial': _("Not started."),
# Translators: "Being scored." is used to communicate to a student that their response
# are in the process of being scored
'assessing': _("Being scored."),
# Translators: "Scoring finished" is used to communicate to a student that their response
# have been scored, but the full scoring process is not yet complete
'intermediate_done': _("Scoring finished."),
# Translators: "Complete" is used to communicate to a student that their
# openended response has been fully scored
'done': _("Complete."),
}
# Default value that controls whether or not to skip basic spelling checks in the controller
@@ -86,6 +99,10 @@ class CombinedOpenEndedV1Module():
# Where the templates live for this problem
TEMPLATE_DIR = "combinedopenended"
# hack: included to make this class act enough like an xblock to get i18n
_services_requested = {"i18n": "need"}
_combined_services = _services_requested
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, metadata=None, static_data=None, **kwargs):
@@ -541,6 +558,7 @@ class CombinedOpenEndedV1Module():
"""
task_html = self.get_html_base()
# set context variables and render template
ugettext = self.system.service(self, "i18n").ugettext
context = {
'items': [{'content': task_html}],
@@ -549,12 +567,12 @@ class CombinedOpenEndedV1Module():
'state': self.state,
'task_count': len(self.task_xml),
'task_number': self.current_task_number + 1,
'status': self.get_status(False),
'status': ugettext(self.get_status(False)),
'display_name': self.display_name,
'accept_file_upload': self.accept_file_upload,
'location': self.location,
'legend_list': LEGEND_LIST,
'human_state': HUMAN_STATES.get(self.state, "Not started."),
'human_state': ugettext(HUMAN_STATES.get(self.state, "Not started.")),
'is_staff': self.system.user_is_staff,
}
@@ -567,7 +585,9 @@ class CombinedOpenEndedV1Module():
Output: rendered html
"""
context = self.get_context()
html = self.system.render_template('{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context)
html = self.system.render_template(
'{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context
)
return html
def get_html_nonsystem(self):
@@ -578,7 +598,9 @@ class CombinedOpenEndedV1Module():
Output: HTML rendered directly via Mako
"""
context = self.get_context()
html = self.system.render_template('{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context)
html = self.system.render_template(
'{0}/combined_open_ended.html'.format(self.TEMPLATE_DIR), context
)
return html
def get_html_base(self):
@@ -815,6 +837,7 @@ class CombinedOpenEndedV1Module():
Input: AJAX data dictionary
Output: Dictionary to be rendered via ajax that contains the result html.
"""
ugettext = self.system.service(self, "i18n").ugettext
all_responses = []
success, can_see_rubric, error = self.check_if_student_has_done_needed_grading()
if not can_see_rubric:
@@ -841,12 +864,20 @@ class CombinedOpenEndedV1Module():
rubric_scores = [[response['rubric_scores'][z]]]
grader_types = [[response['grader_types'][z]]]
feedback_items = [[response['feedback_items'][z]]]
rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']),
rubric_scores,
grader_types, feedback_items)
rubric_html = self.rubric_renderer.render_combined_rubric(
stringify_children(self.static_data['rubric']),
rubric_scores,
grader_types,
feedback_items
)
contexts.append({
'result': rubric_html,
'task_name': 'Scored rubric',
# Translators: "Scored rubric" appears to a user as part of a longer
# string that looks something like: "Scored rubric from grader 1".
# "Scored" is an adjective that modifies the noun "rubric".
# That longer string appears when a user is viewing a graded rubric
# returned from one of the graders of their openended response problem.
'task_name': ugettext('Scored rubric'),
'feedback' : feedback
})
@@ -925,6 +956,7 @@ class CombinedOpenEndedV1Module():
Input: AJAX data dictionary
Output: AJAX dictionary to tbe rendered
"""
ugettext = self.system.service(self, "i18n").ugettext
if self.state != self.DONE:
if not self.ready_to_reset:
return self.out_of_sync_error(data)
@@ -937,10 +969,13 @@ class CombinedOpenEndedV1Module():
return {
'success': False,
# This is a student_facing_error
'error': (
'You have attempted this question {0} times. '
'You are only allowed to attempt it {1} times.'
).format(self.student_attempts, self.max_attempts)
'error': ugettext(
'You have attempted this question {number_of_student_attempts} times. '
'You are only allowed to attempt it {max_number_of_attempts} times.'
).format(
number_of_student_attempts=self.student_attempts,
max_number_of_attempts=self.max_attempts
)
}
self.student_attempts +=1
self.state = self.INITIAL
@@ -980,26 +1015,32 @@ class CombinedOpenEndedV1Module():
Input: None
Output: The status html to be rendered
"""
status = []
ugettext = self.system.service(self, "i18n").ugettext
status_list = []
current_task_human_name = ""
for i in xrange(0, len(self.task_xml)):
human_task_name = self.extract_human_name_from_task(self.task_xml[i])
human_task_name = ugettext(human_task_name)
# Extract the name of the current task for screen readers.
if self.current_task_number == i:
current_task_human_name = human_task_name
task_data = {'task_number': i + 1, 'human_task': human_task_name, 'current': self.current_task_number==i}
status.append(task_data)
task_data = {
'task_number': i + 1,
'human_task': human_task_name,
'current': self.current_task_number == i
}
status_list.append(task_data)
context = {
'status_list': status,
'status_list': status_list,
'grader_type_image_dict': GRADER_TYPE_IMAGE_DICT,
'legend_list': LEGEND_LIST,
'render_via_ajax': render_via_ajax,
'current_task_human_name': current_task_human_name,
}
status_html = self.system.render_template("{0}/combined_open_ended_status.html".format(self.TEMPLATE_DIR),
context)
status_html = self.system.render_template(
"{0}/combined_open_ended_status.html".format(self.TEMPLATE_DIR), context
)
return status_html
@@ -1109,12 +1150,39 @@ class CombinedOpenEndedV1Module():
"""
return dict out-of-sync error message, and also log.
"""
ugettext = self.system.service(self, "i18n").ugettext
#This is a dev_facing_error
log.warning("Combined module state out sync. state: %r, data: %r. %s",
self.state, data, msg)
log.warning(
"Combined module state out sync. state: %r, data: %r. %s",
self.state,
data,
msg
)
#This is a student_facing_error
return {'success': False,
'error': 'The problem state got out-of-sync. Please try reloading the page.'}
return {
'success': False,
'error': ugettext('The problem state got out-of-sync. Please try reloading the page.')
}
@classmethod
def service_declaration(cls, service_name):
"""
This classmethod is copied from XBlock's service_declaration.
It is included to make this class act enough like an XBlock
to get i18n working on it.
This is currently only used for i18n, and will return "need"
in that case.
Arguments:
service_name (string): the name of the service requested.
Returns:
One of "need", "want", or None.
"""
declaration = cls._combined_services.get(service_name)
return declaration
class CombinedOpenEndedV1Descriptor():

View File

@@ -11,12 +11,19 @@ GRADER_TYPE_IMAGE_DICT = {
'BC': '/static/images/ml_grading_icon.png',
}
_ = lambda text: text
HUMAN_GRADER_TYPE = {
'SA': 'Self-Assessment',
'PE': 'Peer-Assessment',
'IN': 'Instructor-Assessment',
'ML': 'AI-Assessment',
'BC': 'AI-Assessment',
# Translators: "Self-Assessment" refers to the self-assessed mode of openended evaluation
'SA': _('Self-Assessment'),
# Translators: "Peer-Assessment" refers to the peer-assessed mode of openended evaluation
'PE': _('Peer-Assessment'),
# Translators: "Instructor-Assessment" refers to the instructor-assessed mode of openended evaluation
'IN': _('Instructor-Assessment'),
# Translators: "AI-Assessment" refers to the machine-graded mode of openended evaluation
'ML': _('AI-Assessment'),
# Translators: "AI-Assessment" refers to the machine-graded mode of openended evaluation
'BC': _('AI-Assessment'),
}
DO_NOT_DISPLAY = ['BC', 'IN']
@@ -63,13 +70,16 @@ class CombinedOpenEndedRubric(object):
rubric_template = '{0}/open_ended_rubric.html'.format(self.TEMPLATE_DIR)
if self.view_only:
rubric_template = '{0}/open_ended_view_only_rubric.html'.format(self.TEMPLATE_DIR)
html = self.system.render_template(rubric_template,
{'categories': rubric_categories,
'has_score': self.has_score,
'view_only': self.view_only,
'max_score': max_score,
'combined_rubric': False
})
html = self.system.render_template(
rubric_template,
{
'categories': rubric_categories,
'has_score': self.has_score,
'view_only': self.view_only,
'max_score': max_score,
'combined_rubric': False,
}
)
success = True
except:
#This is a staff_facing_error
@@ -153,7 +163,6 @@ class CombinedOpenEndedRubric(object):
"[extract_category] Category {0} is missing a score. Contact the learning sciences group for assistance.".format(
descriptionxml.text))
# parse description
if descriptionxml.tag != 'description':
#This is a staff_facing_error
@@ -245,17 +254,20 @@ class CombinedOpenEndedRubric(object):
else:
correct.append(.5)
html = self.system.render_template('{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR),
{'categories': rubric_categories,
'max_scores': max_scores,
'correct' : correct,
'has_score': True,
'view_only': True,
'max_score': max_score,
'combined_rubric': True,
'grader_type_image_dict': GRADER_TYPE_IMAGE_DICT,
'human_grader_types': HUMAN_GRADER_TYPE,
})
html = self.system.render_template(
'{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR),
{
'categories': rubric_categories,
'max_scores': max_scores,
'correct': correct,
'has_score': True,
'view_only': True,
'max_score': max_score,
'combined_rubric': True,
'grader_type_image_dict': GRADER_TYPE_IMAGE_DICT,
'human_grader_types': HUMAN_GRADER_TYPE,
}
)
return html
@staticmethod

View File

@@ -149,14 +149,22 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
event_info['problem_id'] = self.location_string
event_info['student_id'] = system.anonymous_student_id
event_info['survey_responses'] = data
_ = self.system.service(self, "i18n").ugettext
survey_responses = event_info['survey_responses']
for tag in ['feedback', 'submission_id', 'grader_id', 'score']:
if tag not in survey_responses:
# This is a student_facing_error
return {'success': False,
'msg': "Could not find needed tag {0} in the survey responses. Please try submitting again.".format(
tag)}
return {
'success': False,
# Translators: 'tag' is one of 'feedback', 'submission_id',
# 'grader_id', or 'score'. They are categories that a student
# responds to when filling out a post-assessment survey
# of his or her grade from an openended problem.
'msg': _("Could not find needed tag {tag_name} in the "
"survey responses. Please try submitting "
"again.").format(tag_name=tag)
}
try:
submission_id = int(survey_responses['submission_id'])
grader_id = int(survey_responses['grader_id'])
@@ -171,11 +179,17 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
)
log.exception(error_message)
# This is a student_facing_error
return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."}
return {
'success': False,
'msg': _(
"There was an error saving your feedback. Please "
"contact course staff."
)
}
xqueue = system.get('xqueue')
if xqueue is None:
return {'success': False, 'msg': "Couldn't submit feedback."}
return {'success': False, 'msg': _("Couldn't submit feedback.")}
qinterface = xqueue['interface']
qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat)
anonymous_student_id = system.anonymous_student_id
@@ -208,10 +222,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# Convert error to a success value
success = True
message = "Successfully saved your feedback."
message = _("Successfully saved your feedback.")
if error:
success = False
message = "Unable to save your feedback. Please try again later."
message = _("Unable to save your feedback. Please try again later.")
log.error("Unable to send feedback to grader. location: {0}, error_message: {1}".format(
self.location_string, error_message
))
@@ -277,12 +291,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'key': queuekey,
'time': qtime,
}
_ = self.system.service(self, "i18n").ugettext
success = True
message = "Successfully saved your submission."
message = _("Successfully saved your submission.")
if error:
success = False
message = 'Unable to submit your submission to grader. Please try again later.'
# Translators: the `grader` refers to the grading service open response problems
# are sent to, either to be machine-graded, peer-graded, or instructor-graded.
message = _('Unable to submit your submission to the grader. Please try again later.')
log.error("Unable to submit to grader. location: {0}, error_message: {1}".format(
self.location_string, error_message
))
@@ -297,9 +313,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
@param system: Modulesystem
@return: Boolean True (not useful currently)
"""
_ = self.system.service(self, "i18n").ugettext
new_score_msg = self._parse_score_msg(score_msg, system)
if not new_score_msg['valid']:
new_score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.'
# Translators: the `grader` refers to the grading service open response problems
# are sent to, either to be machine-graded, peer-graded, or instructor-graded.
new_score_msg['feedback'] = _('Invalid grader reply. Please contact the course staff.')
# self.child_history is initialized as []. record_latest_score() and record_latest_post_assessment()
# operate on self.child_history[-1]. Thus we have to make sure child_history is not [].
@@ -387,7 +406,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
def format_feedback(feedback_type, value):
feedback_type, value = encode_values(feedback_type, value)
feedback = """
feedback = u"""
<div class="{feedback_type}">
{value}
</div>
@@ -405,10 +424,15 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# that we can do proper escaping here (e.g. are the graders allowed to
# include HTML?)
_ = self.system.service(self, "i18n").ugettext
for tag in ['success', 'feedback', 'submission_id', 'grader_id']:
if tag not in response_items:
# This is a student_facing_error
return format_feedback('errors', 'Error getting feedback from grader.')
return format_feedback(
# Translators: the `grader` refers to the grading service open response problems
# are sent to, either to be machine-graded, peer-graded, or instructor-graded.
'errors', _('Error getting feedback from grader.')
)
feedback_items = response_items['feedback']
try:
@@ -417,12 +441,20 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# This is a dev_facing_error
log.exception("feedback_items from external open ended grader have invalid json {0}".format(feedback_items))
# This is a student_facing_error
return format_feedback('errors', 'Error getting feedback from grader.')
return format_feedback(
# Translators: the `grader` refers to the grading service open response problems
# are sent to, either to be machine-graded, peer-graded, or instructor-graded.
'errors', _('Error getting feedback from grader.')
)
if response_items['success']:
if len(feedback) == 0:
# This is a student_facing_error
return format_feedback('errors', 'No feedback available from grader.')
return format_feedback(
# Translators: the `grader` refers to the grading service open response problems
# are sent to, either to be machine-graded, peer-graded, or instructor-graded.
'errors', _('No feedback available from grader.')
)
for tag in do_not_render:
if tag in feedback:
@@ -648,12 +680,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'check_for_score': self.check_for_score,
'store_answer': self.store_answer,
}
_ = self.system.service(self, "i18n").ugettext
if dispatch not in handlers:
# This is a dev_facing_error
log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
# This is a dev_facing_error
return json.dumps({'error': 'Error handling action. Please try again.', 'success': False})
return json.dumps(
{'error': _('Error handling action. Please try again.'), 'success': False}
)
before = self.get_progress()
d = handlers[dispatch](data, system)
@@ -733,6 +767,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
Input: Modulesystem object
Output: Rendered HTML
"""
_ = self.system.service(self, "i18n").ugettext
# set context variables and render template
eta_string = None
if self.child_state != self.INITIAL:
@@ -740,7 +775,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
score = self.latest_score()
correct = 'correct' if self.is_submission_correct(score) else 'incorrect'
if self.child_state == self.ASSESSING:
eta_string = "Your response has been submitted. Please check back later for your grade."
# Translators: this string appears once an openended response
# is submitted but before it has been graded
eta_string = _("Your response has been submitted. Please check back later for your grade.")
else:
post_assessment = ""
correct = ""

View File

@@ -25,9 +25,6 @@ MAX_ATTEMPTS = 1
# Overriden by max_score specified in xml.
MAX_SCORE = 1
FILE_NOT_FOUND_IN_RESPONSE_MESSAGE = "We could not find a file in your submission. Please try choosing a file or pasting a link to your file into the answer box."
ERROR_SAVING_FILE_MESSAGE = "We are having trouble saving your file. Please try another file or paste a link to your file into the answer box."
def upload_to_s3(file_to_upload, keyname, s3_interface):
'''
@@ -91,12 +88,23 @@ class OpenEndedChild(object):
DONE = 'done'
# This is used to tell students where they are at in the module
_ = lambda text: text
HUMAN_NAMES = {
'initial': 'Not started',
'assessing': 'In progress',
'post_assessment': 'Done',
'done': 'Done',
}
# Translators: "Not started" communicates to a student that their response
# has not yet been graded
'initial': _('Not started'),
# Translators: "In progress" communicates to a student that their response
# is currently in the grading process
'assessing': _('In progress'),
# Translators: "Done" communicates to a student that their response
# has been fully graded
'post_assessment': _('Done'),
'done': _('Done'),
}
# included to make this act enough like an xblock to get i18n
_services_requested = {"i18n": "need"}
_combined_services = _services_requested
def __init__(self, system, location, definition, descriptor, static_data,
instance_state=None, shared_state=None, **kwargs):
@@ -463,6 +471,8 @@ class OpenEndedChild(object):
@return: Boolean success, and updated AJAX data dictionary.
"""
_ = self.system.service(self, "i18n").ugettext
error_message = ""
if not self.accept_file_upload:
@@ -481,7 +491,11 @@ class OpenEndedChild(object):
# If success is False, we have not found a link, and no file was attached.
# Show error to student.
if success is False:
error_message = FILE_NOT_FOUND_IN_RESPONSE_MESSAGE
error_message = _(
"We could not find a file in your submission. "
"Please try choosing a file or pasting a URL to your "
"file into the answer box."
)
except Exception:
# In this case, an image was submitted by the student, but the image could not be uploaded to S3. Likely
@@ -490,7 +504,10 @@ class OpenEndedChild(object):
"but the image was not able to be uploaded to S3. This could indicate a configuration "
"issue with this deployment and the S3_INTERFACE setting.")
success = False
error_message = ERROR_SAVING_FILE_MESSAGE
error_message = _(
"We are having trouble saving your file. Please try another "
"file or paste a URL to your file into the answer box."
)
return success, error_message, data
@@ -534,3 +551,23 @@ class OpenEndedChild(object):
eta_string = ""
return eta_string
@classmethod
def service_declaration(cls, service_name):
"""
This classmethod is copied from XBlock's service_declaration.
It is included to make this class act enough like an XBlock
to get i18n working on it.
This is currently only used for i18n, and will return "need"
in that case.
Arguments:
service_name (string): the name of the service requested.
Returns:
One of "need", "want", or None.
"""
declaration = cls._combined_services.get(service_name)
return declaration

View File

@@ -125,8 +125,9 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
rubric_html = rubric_dict['html']
# we'll render it
context = {'rubric': rubric_html,
'max_score': self._max_score,
context = {
'rubric': rubric_html,
'max_score': self._max_score,
}
if self.child_state == self.ASSESSING:
@@ -233,7 +234,11 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
# This is a dev_facing_error
log.error("Non-integer score value passed to save_assessment, or no score list present.")
# This is a student_facing_error
return {'success': False, 'error': "Error saving your score. Please notify course staff."}
_ = self.system.service(self, "i18n").ugettext
return {
'success': False,
'error': _("Error saving your score. Please notify course staff.")
}
# Record score as assessment and rubric scores as post assessment
self.record_latest_score(score)
@@ -266,9 +271,11 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
self.record_latest_post_assessment(data['hint'])
self.change_state(self.DONE)
return {'success': True,
'message_html': '',
'allow_reset': self._allow_reset()}
return {
'success': True,
'message_html': '',
'allow_reset': self._allow_reset(),
}
def latest_post_assessment(self, system):
latest_post_assessment = super(SelfAssessmentModule, self).latest_post_assessment(system)