Commit 4b866df3 by Stephen Sanchez

Using the XBlock i18n Service for ORA2

parent a0131c36
......@@ -5,7 +5,6 @@ import copy
from collections import defaultdict
from lazy import lazy
from django.utils.translation import ugettext as _
from xblock.core import XBlock
from openassessment.assessment.api import peer as peer_api
......@@ -59,7 +58,7 @@ class GradeMixin(object):
else: # status is 'self' or 'peer', which implies that the workflow is incomplete
path, context = self.render_grade_incomplete(workflow)
except (sub_api.SubmissionError, PeerAssessmentError, SelfAssessmentError):
return self.render_error(_(u"An unexpected error occurred."))
return self.render_error(self._(u"An unexpected error occurred."))
else:
return self.render_assessment(path, context)
......@@ -178,9 +177,9 @@ class GradeMixin(object):
incomplete_steps = []
if _is_incomplete("peer"):
incomplete_steps.append(_("Peer Assessment"))
incomplete_steps.append(self._("Peer Assessment"))
if _is_incomplete("self"):
incomplete_steps.append(_("Self Assessment"))
incomplete_steps.append(self._("Self Assessment"))
return (
'openassessmentblock/grade/oa_grade_incomplete.html',
......@@ -213,7 +212,7 @@ class GradeMixin(object):
'options': feedback_options,
})
except (peer_api.PeerAssessmentInternalError, peer_api.PeerAssessmentRequestError):
return {'success': False, 'msg': _(u"Assessment feedback could not be saved.")}
return {'success': False, 'msg': self._(u"Assessment feedback could not be saved.")}
else:
self.runtime.publish(
self,
......@@ -224,7 +223,7 @@ class GradeMixin(object):
'options': feedback_options,
}
)
return {'success': True, 'msg': _(u"Feedback saved.")}
return {'success': True, 'msg': self._(u"Feedback saved.")}
def _rubric_criteria_grade_context(self, peer_assessments, self_assessment):
"""
......
......@@ -83,7 +83,7 @@ def load(path):
data = pkg_resources.resource_string(__name__, path)
return data.decode("utf8")
@XBlock.needs("i18n")
class OpenAssessmentBlock(
XBlock,
MessageMixin,
......@@ -247,6 +247,7 @@ class OpenAssessmentBlock(
frag.initialize_js('OpenAssessmentBlock')
return frag
@property
def is_admin(self):
"""
......@@ -353,7 +354,7 @@ class OpenAssessmentBlock(
config = parse_from_xml(node)
block = runtime.construct_xblock_from_class(cls, keys)
xblock_validator = validator(block, strict_post_release=False)
xblock_validator = validator(block, block._, strict_post_release=False)
xblock_validator(
create_rubric_dict(config['prompt'], config['rubric_criteria']),
config['rubric_assessments'],
......@@ -373,6 +374,11 @@ class OpenAssessmentBlock(
return block
@property
def _(self):
i18nService = self.runtime.service(self, 'i18n')
return i18nService.ugettext
@property
def valid_assessments(self):
"""
Return a list of assessment dictionaries that we recognize.
......@@ -509,7 +515,7 @@ class OpenAssessmentBlock(
# Resolve unspecified dates and date strings to datetimes
start, due, date_ranges = resolve_dates(
self.start, self.due, [submission_range] + assessment_ranges
self.start, self.due, [submission_range] + assessment_ranges, self._
)
open_range = (start, due)
......
import logging
from django.utils.translation import ugettext as _
from webob import Response
from xblock.core import XBlock
......@@ -9,11 +8,9 @@ from openassessment.assessment.errors import (
PeerAssessmentRequestError, PeerAssessmentInternalError, PeerAssessmentWorkflowError
)
from openassessment.workflow.errors import AssessmentWorkflowError
from openassessment.fileupload import api as file_upload_api
from openassessment.fileupload.api import FileUploadError
from .data_conversion import create_rubric_dict
from .resolve_dates import DISTANT_FUTURE
from .data_conversion import create_rubric_dict, clean_criterion_feedback
from .data_conversion import clean_criterion_feedback
logger = logging.getLogger(__name__)
......@@ -52,16 +49,16 @@ class PeerAssessmentMixin(object):
"""
# Validate the request
if 'options_selected' not in data:
return {'success': False, 'msg': _('Must provide options selected in the assessment')}
return {'success': False, 'msg': self._('Must provide options selected in the assessment')}
if 'overall_feedback' not in data:
return {'success': False, 'msg': _('Must provide overall feedback in the assessment')}
return {'success': False, 'msg': self._('Must provide overall feedback in the assessment')}
if 'criterion_feedback' not in data:
return {'success': False, 'msg': _('Must provide feedback for criteria in the assessment')}
return {'success': False, 'msg': self._('Must provide feedback for criteria in the assessment')}
if self.submission_uuid is None:
return {'success': False, 'msg': _('You must submit a response before you can peer-assess.')}
return {'success': False, 'msg': self._('You must submit a response before you can peer-assess.')}
assessment_ui_model = self.get_assessment_module('peer-assessment')
if assessment_ui_model:
......@@ -85,12 +82,12 @@ class PeerAssessmentMixin(object):
u"Peer API error for submission UUID {}".format(self.submission_uuid),
exc_info=True
)
return {'success': False, 'msg': _(u"Your peer assessment could not be submitted.")}
return {'success': False, 'msg': self._(u"Your peer assessment could not be submitted.")}
except PeerAssessmentInternalError:
logger.exception(
u"Peer API internal error for submission UUID: {}".format(self.submission_uuid)
)
msg = _("Your peer assessment could not be submitted.")
msg = self._("Your peer assessment could not be submitted.")
return {'success': False, 'msg': msg}
# Update both the workflow that the submission we're assessing
......@@ -104,7 +101,7 @@ class PeerAssessmentMixin(object):
u"Workflow error occurred when submitting peer assessment "
u"for submission {}".format(self.submission_uuid)
)
msg = _('Could not update workflow status.')
msg = self._('Could not update workflow status.')
return {'success': False, 'msg': msg}
# Temp kludge until we fix JSON serialization for datetime
......@@ -113,7 +110,7 @@ class PeerAssessmentMixin(object):
return {'success': True, 'msg': u''}
else:
return {'success': False, 'msg': _('Could not load peer assessment.')}
return {'success': False, 'msg': self._('Could not load peer assessment.')}
@XBlock.handler
def render_peer_assessment(self, data, suffix=''):
......@@ -180,15 +177,15 @@ class PeerAssessmentMixin(object):
context_dict["review_num"] = count + 1
if continue_grading:
context_dict["submit_button_text"] = _(
context_dict["submit_button_text"] = self._(
"Submit your assessment & review another response"
)
elif assessment["must_grade"] - count == 1:
context_dict["submit_button_text"] = _(
context_dict["submit_button_text"] = self._(
"Submit your assessment & move onto next step"
)
else:
context_dict["submit_button_text"] = _(
context_dict["submit_button_text"] = self._(
"Submit your assessment & move to response #{response_number}"
).format(response_number=(count + 2))
......
......@@ -4,7 +4,6 @@ Resolve unspecified dates and date strings to datetimes.
import datetime as dt
import pytz
from dateutil.parser import parse as parse_date
from django.utils.translation import ugettext as _
class InvalidDateFormat(Exception):
......@@ -25,12 +24,14 @@ DISTANT_PAST = dt.datetime(dt.MINYEAR, 1, 1, tzinfo=pytz.utc)
DISTANT_FUTURE = dt.datetime(dt.MAXYEAR, 1, 1, tzinfo=pytz.utc)
def _parse_date(value):
def _parse_date(value, _):
"""
Parse an ISO formatted datestring into a datetime object with timezone set to UTC.
Args:
value (str or datetime): The ISO formatted date string or datetime object.
_ (function): The i18n service function used to get the appropriate
text for a message.
Returns:
datetime.datetime
......@@ -51,7 +52,7 @@ def _parse_date(value):
raise InvalidDateFormat(_("'{date}' must be a date string or datetime").format(date=value))
def resolve_dates(start, end, date_ranges):
def resolve_dates(start, end, date_ranges, _):
"""
Resolve date strings (including "default" dates) to datetimes.
The basic rules are:
......@@ -124,6 +125,8 @@ def resolve_dates(start, end, date_ranges):
end (str, ISO date format, or datetime): When the problem closes. A value of None indicates that the problem never closes.
date_ranges (list of tuples): list of (start, end) ISO date string tuples indicating
the start/end timestamps (date string or datetime) of each submission/assessment.
_ (function): An i18n service function to use for retrieving the
proper text.
Returns:
start (datetime): The resolved start date
......@@ -135,8 +138,8 @@ def resolve_dates(start, end, date_ranges):
InvalidDateFormat
"""
# Resolve problem start and end dates to minimum and maximum dates
start = _parse_date(start) if start is not None else DISTANT_PAST
end = _parse_date(end) if end is not None else DISTANT_FUTURE
start = _parse_date(start, _) if start is not None else DISTANT_PAST
end = _parse_date(end, _) if end is not None else DISTANT_FUTURE
resolved_starts = []
resolved_ends = []
......@@ -162,11 +165,11 @@ def resolve_dates(start, end, date_ranges):
# defaults. See the docstring above for a more detailed justification.
for step_start, step_end in date_ranges:
if step_start is not None:
parsed_start = _parse_date(step_start)
parsed_start = _parse_date(step_start, _)
start = min(start, parsed_start)
end = max(end, parsed_start + dt.timedelta(milliseconds=1))
if step_end is not None:
parsed_end = _parse_date(step_end)
parsed_end = _parse_date(step_end, _)
end = max(end, parsed_end)
start = min(start, parsed_end - dt.timedelta(milliseconds=1))
......@@ -182,13 +185,13 @@ def resolve_dates(start, end, date_ranges):
# If I set a start date for peer-assessment, but don't set a start date for the following self-assessment,
# then the self-assessment should default to the same start date as the peer-assessment.
step_start, __ = date_ranges[index]
step_start = _parse_date(step_start) if step_start is not None else prev_start
step_start = _parse_date(step_start, _) if step_start is not None else prev_start
# Resolve "default" end dates to the following end date.
# If I set a due date for self-assessment, but don't set a due date for the previous peer-assessment,
# then the peer-assessment should default to the same due date as the self-assessment.
__, step_end = date_ranges[reverse_index]
step_end = _parse_date(step_end) if step_end is not None else prev_end
step_end = _parse_date(step_end, _) if step_end is not None else prev_end
if step_start < prev_start:
msg = _(u"This step's start date '{start}' cannot be earlier than the previous step's start date '{prev}'.").format(
......
import logging
from django.utils.translation import ugettext as _
from xblock.core import XBlock
from webob import Response
......@@ -9,7 +8,7 @@ from openassessment.workflow import api as workflow_api
from submissions import api as submission_api
from .data_conversion import create_rubric_dict
from .resolve_dates import DISTANT_FUTURE
from .data_conversion import create_rubric_dict, clean_criterion_feedback
from .data_conversion import clean_criterion_feedback
logger = logging.getLogger(__name__)
......@@ -36,7 +35,7 @@ class SelfAssessmentMixin(object):
except:
msg = u"Could not retrieve self assessment for submission {}".format(self.submission_uuid)
logger.exception(msg)
return self.render_error(_(u"An unexpected error occurred."))
return self.render_error(self._(u"An unexpected error occurred."))
else:
return self.render_assessment(path, context)
......@@ -112,16 +111,16 @@ class SelfAssessmentMixin(object):
and "msg" (unicode) containing additional information if an error occurs.
"""
if 'options_selected' not in data:
return {'success': False, 'msg': _(u"Missing options_selected key in request")}
return {'success': False, 'msg': self._(u"Missing options_selected key in request")}
if 'overall_feedback' not in data:
return {'success': False, 'msg': _('Must provide overall feedback in the assessment')}
return {'success': False, 'msg': self._('Must provide overall feedback in the assessment')}
if 'criterion_feedback' not in data:
return {'success': False, 'msg': _('Must provide feedback for criteria in the assessment')}
return {'success': False, 'msg': self._('Must provide feedback for criteria in the assessment')}
if self.submission_uuid is None:
return {'success': False, 'msg': _(u"You must submit a response before you can perform a self-assessment.")}
return {'success': False, 'msg': self._(u"You must submit a response before you can perform a self-assessment.")}
try:
assessment = self_api.create_assessment(
......@@ -142,14 +141,14 @@ class SelfAssessmentMixin(object):
u"for the submission {}".format(self.submission_uuid),
exc_info=True
)
msg = _(u"Your self assessment could not be submitted.")
msg = self._(u"Your self assessment could not be submitted.")
return {'success': False, 'msg': msg}
except (self_api.SelfAssessmentInternalError, workflow_api.AssessmentWorkflowInternalError):
logger.exception(
u"An error occurred while submitting a self assessment "
u"for the submission {}".format(self.submission_uuid),
)
msg = _(u"Your self assessment could not be submitted.")
msg = self._(u"Your self assessment could not be submitted.")
return {'success': False, 'msg': msg}
else:
return {'success': True, 'msg': u""}
......@@ -4,7 +4,6 @@ determine the flow of the problem.
"""
import copy
from functools import wraps
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from xblock.core import XBlock
......@@ -19,13 +18,13 @@ from openassessment.assessment.api import self as self_api
from openassessment.assessment.api import ai as ai_api
def require_global_admin(error_msg):
def require_global_admin(error_key):
"""
Method decorator to restrict access to an XBlock handler
to only global staff.
Args:
error_msg (unicode): The error message to display to the user
error_key (str): The key to the error message to display to the user
if they do not have sufficient permissions.
Returns:
......@@ -35,22 +34,26 @@ def require_global_admin(error_msg):
def _decorator(func): # pylint: disable=C0111
@wraps(func)
def _wrapped(xblock, *args, **kwargs): # pylint: disable=C0111
permission_errors = {
"SCHEDULE_TRAINING": xblock._(u"You do not have permission to schedule training"),
"RESCHEDULE_TASKS": xblock._(u"You do not have permission to reschedule tasks."),
}
if not xblock.is_admin or xblock.in_studio_preview:
return {'success': False, 'msg': unicode(error_msg)}
return {'success': False, 'msg': permission_errors[error_key]}
else:
return func(xblock, *args, **kwargs)
return _wrapped
return _decorator
def require_course_staff(error_msg):
def require_course_staff(error_key):
"""
Method decorator to restrict access to an XBlock render
method to only course staff.
Args:
error_msg (unicode): The error message to display to the user
if they do not have sufficient permissions.
error_key (str): The key for the error message to display to the
user if they do not have sufficient permissions.
Returns:
decorated function
......@@ -59,8 +62,13 @@ def require_course_staff(error_msg):
def _decorator(func): # pylint: disable=C0111
@wraps(func)
def _wrapped(xblock, *args, **kwargs): # pylint: disable=C0111
permission_errors = {
"STAFF_INFO": xblock._(u"You do not have permission to access staff information"),
"STUDENT_INFO": xblock._(u"You do not have permission to access student information."),
}
if not xblock.is_course_staff or xblock.in_studio_preview:
return xblock.render_error(unicode(error_msg))
return xblock.render_error(permission_errors[error_key])
else:
return func(xblock, *args, **kwargs)
return _wrapped
......@@ -73,7 +81,7 @@ class StaffInfoMixin(object):
"""
@XBlock.handler
@require_course_staff(ugettext_lazy(u"You do not have permission to access staff information"))
@require_course_staff("STAFF_INFO")
def render_staff_info(self, data, suffix=''): # pylint: disable=W0613
"""
Template context dictionary for course staff debug panel.
......@@ -142,7 +150,7 @@ class StaffInfoMixin(object):
return path, context
@XBlock.json_handler
@require_global_admin(ugettext_lazy(u"You do not have permission to schedule training"))
@require_global_admin("SCHEDULE_TRAINING")
def schedule_training(self, data, suffix=''): # pylint: disable=W0613
"""
Schedule a new training task for example-based grading.
......@@ -163,22 +171,22 @@ class StaffInfoMixin(object):
return {
'success': True,
'workflow_uuid': workflow_uuid,
'msg': _(u"Training scheduled with new Workflow UUID: {uuid}".format(uuid=workflow_uuid))
'msg': self._(u"Training scheduled with new Workflow UUID: {uuid}".format(uuid=workflow_uuid))
}
except AIError as err:
return {
'success': False,
'msg': _(u"An error occurred scheduling classifier training: {error}".format(error=err))
'msg': self._(u"An error occurred scheduling classifier training: {error}".format(error=err))
}
else:
return {
'success': False,
'msg': _(u"Example Based Assessment is not configured for this location.")
'msg': self._(u"Example Based Assessment is not configured for this location.")
}
@XBlock.handler
@require_course_staff(ugettext_lazy(u"You do not have permission to access student information."))
@require_course_staff("STUDENT_INFO")
def render_student_info(self, data, suffix=''): # pylint: disable=W0613
"""
Renders all relative information for a specific student's workflow.
......@@ -248,7 +256,7 @@ class StaffInfoMixin(object):
return path, context
@XBlock.json_handler
@require_global_admin(ugettext_lazy(u"You do not have permission to reschedule tasks."))
@require_global_admin("RESCHEDULE_TASKS")
def reschedule_unfinished_tasks(self, data, suffix=''): # pylint: disable=W0613
"""
Wrapper which invokes the API call for rescheduling grading tasks.
......@@ -278,10 +286,10 @@ class StaffInfoMixin(object):
ai_api.reschedule_unfinished_tasks(course_id=course_id, item_id=item_id, task_type=u"grade")
return {
'success': True,
'msg': _(u"All AI tasks associated with this item have been rescheduled successfully.")
'msg': self._(u"All AI tasks associated with this item have been rescheduled successfully.")
}
except AIError as ex:
return {
'success': False,
'msg': _(u"An error occurred while rescheduling tasks: {}".format(ex))
'msg': self._(u"An error occurred while rescheduling tasks: {}".format(ex))
}
......@@ -2,7 +2,6 @@
Student training step in the OpenAssessment XBlock.
"""
import logging
from django.utils.translation import ugettext as _
from webob import Response
from xblock.core import XBlock
from openassessment.assessment.api import student_training
......@@ -52,7 +51,7 @@ class StudentTrainingMixin(object):
except: # pylint:disable=W0702
msg = u"Could not render student training step for submission {}".format(self.submission_uuid)
logger.exception(msg)
return self.render_error(_(u"An unexpected error occurred."))
return self.render_error(self._(u"An unexpected error occurred."))
else:
return self.render_assessment(path, context)
......@@ -158,9 +157,9 @@ class StudentTrainingMixin(object):
"""
if 'options_selected' not in data:
return {'success': False, 'msg': _(u"Missing options_selected key in request")}
return {'success': False, 'msg': self._(u"Missing options_selected key in request")}
if not isinstance(data['options_selected'], dict):
return {'success': False, 'msg': _(u"options_selected must be a dictionary")}
return {'success': False, 'msg': self._(u"options_selected must be a dictionary")}
# Check the student's scores against the course author's scores.
# This implicitly updates the student training workflow (which example essay is shown)
......@@ -186,23 +185,23 @@ class StudentTrainingMixin(object):
logger.warning(msg, exc_info=True)
return {
'success': False,
'msg': _(u"Your scores could not be checked.")
'msg': self._(u"Your scores could not be checked.")
}
except student_training.StudentTrainingInternalError:
return {
'success': False,
'msg': _(u"Your scores could not be checked.")
'msg': self._(u"Your scores could not be checked.")
}
except:
return {
'success': False,
'msg': _(u"An unexpected error occurred.")
'msg': self._(u"An unexpected error occurred.")
}
else:
try:
self.update_workflow_status()
except AssessmentWorkflowError:
msg = _('Could not update workflow status.')
msg = self._('Could not update workflow status.')
logger.exception(msg)
return {'success': False, 'msg': msg}
return {
......
......@@ -6,7 +6,6 @@ import copy
import logging
from django.template import Context
from django.template.loader import get_template
from django.utils.translation import ugettext as _
from voluptuous import MultipleInvalid
from xblock.core import XBlock
from xblock.fields import List, Scope
......@@ -91,7 +90,8 @@ class StudioMixin(object):
__, __, date_ranges = resolve_dates(
self.start, self.due,
[(self.submission_start, self.submission_due)] +
[(asmnt.get('start'), asmnt.get('due')) for asmnt in self.valid_assessments]
[(asmnt.get('start'), asmnt.get('due')) for asmnt in self.valid_assessments],
self._
)
submission_start, submission_due = date_ranges[0]
......@@ -143,12 +143,12 @@ class StudioMixin(object):
data = EDITOR_UPDATE_SCHEMA(data)
except MultipleInvalid:
logger.exception('Editor context is invalid')
return {'success': False, 'msg': _('Error updating XBlock configuration')}
return {'success': False, 'msg': self._('Error updating XBlock configuration')}
# Check that the editor assessment order contains all the assessments. We are more flexible on example-based.
if set(DEFAULT_EDITOR_ASSESSMENTS_ORDER) != (set(data['editor_assessments_order']) - {'example-based-assessment'}):
logger.exception('editor_assessments_order does not contain all expected assessment types')
return {'success': False, 'msg': _('Error updating XBlock configuration')}
return {'success': False, 'msg': self._('Error updating XBlock configuration')}
# Backwards compatibility: We used to treat "name" as both a user-facing label
# and a unique identifier for criteria and options.
......@@ -170,18 +170,18 @@ class StudioMixin(object):
try:
assessment['examples'] = parse_examples_from_xml_str(assessment['examples_xml'])
except UpdateFromXmlError:
return {'success': False, 'msg': _(
return {'success': False, 'msg': self._(
u'Validation error: There was an error in the XML definition of the '
u'examples provided by the user. Please correct the XML definition before saving.')
}
except KeyError:
return {'success': False, 'msg': _(
return {'success': False, 'msg': self._(
u'Validation error: No examples were provided for example based assessment.'
)}
# This is where we default to EASE for problems which are edited in the GUI
assessment['algorithm_id'] = 'ease'
xblock_validator = validator(self)
xblock_validator = validator(self, self._)
success, msg = xblock_validator(
create_rubric_dict(data['prompt'], data['criteria']),
data['assessments'],
......@@ -189,7 +189,7 @@ class StudioMixin(object):
submission_due=data['submission_due'],
)
if not success:
return {'success': False, 'msg': _('Validation error: {error}').format(error=msg)}
return {'success': False, 'msg': self._('Validation error: {error}').format(error=msg)}
# At this point, all the input data has been validated,
# so we can safely modify the XBlock fields.
......@@ -204,7 +204,7 @@ class StudioMixin(object):
self.submission_due = data['submission_due']
self.allow_file_upload = bool(data['allow_file_upload'])
return {'success': True, 'msg': _(u'Successfully updated OpenAssessment XBlock')}
return {'success': True, 'msg': self._(u'Successfully updated OpenAssessment XBlock')}
@XBlock.json_handler
def check_released(self, data, suffix=''):
......
import logging
from django.utils.translation import ugettext as _
from xblock.core import XBlock
from submissions import api
......@@ -26,16 +25,6 @@ class SubmissionMixin(object):
"""
submit_errors = {
# Reported to user sometimes, and useful in tests
'ENODATA': _(u'API returned an empty response.'),
'EBADFORM': _(u'API Submission Request Error.'),
'EUNKNOWN': _(u'API returned unclassified exception.'),
'ENOMULTI': _(u'Multiple submissions are not allowed.'),
'ENOPREVIEW': _(u'To submit a response, view this component in Preview or Live mode.'),
'EBADARGS': _(u'"submission" required to submit answer.')
}
@XBlock.json_handler
def submit(self, data, suffix=''):
"""Place the submission text into Openassessment system
......@@ -57,23 +46,30 @@ class SubmissionMixin(object):
"""
if 'submission' not in data:
return False, 'EBADARGS', self.submit_errors['EBADARGS']
return (
False,
'EBADARGS',
self._(u'"submission" required to submit answer.')
)
status = False
status_text = None
student_sub = data['submission']
student_item_dict = self.get_student_item_dict()
# Short-circuit if no user is defined (as in Studio Preview mode)
# Since students can't submit, they will never be able to progress in the workflow
if self.in_studio_preview:
return False, 'ENOPREVIEW', self.submit_errors['ENOPREVIEW']
return (
False,
'ENOPREVIEW',
self._(u'To submit a response, view this component in Preview or Live mode.')
)
workflow = self.get_workflow_info()
status_tag = 'ENOMULTI' # It is an error to submit multiple times for the same item
status_text = self._(u'Multiple submissions are not allowed.')
if not workflow:
status_tag = 'ENODATA'
try:
submission = self.create_submission(
student_item_dict,
......@@ -85,13 +81,12 @@ class SubmissionMixin(object):
except (api.SubmissionError, AssessmentWorkflowError):
logger.exception("This response was not submitted.")
status_tag = 'EUNKNOWN'
status_text = self._(u'API returned unclassified exception.')
else:
status = True
status_tag = submission.get('student_item')
status_text = submission.get('attempt_number')
# relies on success being orthogonal to errors
status_text = status_text if status_text else self.submit_errors[status_tag]
return status, status_tag, status_text
@XBlock.json_handler
......@@ -122,11 +117,11 @@ class SubmissionMixin(object):
{"saved_response": self.saved_response}
)
except:
return {'success': False, 'msg': _(u"This response could not be saved.")}
return {'success': False, 'msg': self._(u"This response could not be saved.")}
else:
return {'success': True, 'msg': u''}
else:
return {'success': False, 'msg': _(u"This response was not submitted.")}
return {'success': False, 'msg': self._(u"This response was not submitted.")}
def create_submission(self, student_item_dict, student_sub):
......@@ -166,11 +161,11 @@ class SubmissionMixin(object):
"""
if "contentType" not in data:
return {'success': False, 'msg': _(u"Must specify contentType.")}
return {'success': False, 'msg': self._(u"Must specify contentType.")}
content_type = data['contentType']
if not content_type.startswith('image/'):
return {'success': False, 'msg': _(u"contentType must be an image.")}
return {'success': False, 'msg': self._(u"contentType must be an image.")}
try:
key = self._get_student_item_key()
......@@ -178,7 +173,7 @@ class SubmissionMixin(object):
return {'success': True, 'url': url}
except FileUploadError:
logger.exception("Error retrieving upload URL.")
return {'success': False, 'msg': _(u"Error retrieving upload URL.")}
return {'success': False, 'msg': self._(u"Error retrieving upload URL.")}
@XBlock.json_handler
def download_url(self, data, suffix=''):
......@@ -270,7 +265,7 @@ class SubmissionMixin(object):
Returns:
unicode
"""
return _(u'This response has been saved but not submitted.') if self.has_saved else _(u'This response has not been saved.')
return self._(u'This response has been saved but not submitted.') if self.has_saved else self._(u'This response has not been saved.')
@XBlock.handler
def render_submission(self, data, suffix=''):
......
......@@ -9,6 +9,8 @@ import ddt
from openassessment.xblock.resolve_dates import resolve_dates, DISTANT_PAST, DISTANT_FUTURE
STUB_I18N = lambda x: x
@ddt.ddt
class ResolveDatesTest(TestCase):
......@@ -35,7 +37,8 @@ class ResolveDatesTest(TestCase):
[
(self.DATE_STRINGS[start], self.DATE_STRINGS[end])
for start, end in tuple(data['date_ranges'])
]
],
STUB_I18N
)
self.assertEqual(resolved_start, self.DATES[data['resolved_start']])
self.assertEqual(resolved_end, self.DATES[data['resolved_end']])
......@@ -57,7 +60,8 @@ class ResolveDatesTest(TestCase):
("1999-01-01", "1999-02-03"),
("2003-01-01", "2003-02-03"),
("3234-01-01", "3234-02-03"),
]
],
STUB_I18N
)
# Should default to the min of all specified start dates
......@@ -76,7 +80,8 @@ class ResolveDatesTest(TestCase):
("1999-01-01", "1999-02-03"),
("2003-01-01", "2003-02-03"),
("3234-01-01", "3234-02-03"),
]
],
STUB_I18N
)
# Should default to the max of all specified end dates
......@@ -95,7 +100,8 @@ class ResolveDatesTest(TestCase):
(None, "2014-08-01"),
(None, None),
(None, None)
]
],
STUB_I18N
)
def test_start_after_step_due(self):
......@@ -106,7 +112,8 @@ class ResolveDatesTest(TestCase):
(None, "2014-08-01"),
(None, None),
(None, None)
]
],
STUB_I18N
)
def test_due_before_step_start(self):
......@@ -117,5 +124,6 @@ class ResolveDatesTest(TestCase):
(None, None),
("2014-02-03", None),
(None, None)
]
],
STUB_I18N
)
......@@ -9,7 +9,6 @@ import pytz
from mock import patch, Mock
from submissions import api as sub_api
from submissions.api import SubmissionRequestError, SubmissionInternalError
from openassessment.xblock.submission_mixin import SubmissionMixin
from .base import XBlockHandlerTestCase, scenario
......@@ -31,7 +30,7 @@ class SubmissionTest(XBlockHandlerTestCase):
resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
self.assertFalse(resp[0])
self.assertEqual(resp[1], "ENOMULTI")
self.assertEqual(resp[2], xblock.submit_errors["ENOMULTI"])
self.assertIsNotNone(resp[2])
@scenario('data/basic_scenario.xml', user_id='Bob')
@patch.object(sub_api, 'create_submission')
......@@ -40,7 +39,7 @@ class SubmissionTest(XBlockHandlerTestCase):
resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
self.assertFalse(resp[0])
self.assertEqual(resp[1], "EUNKNOWN")
self.assertEqual(resp[2], SubmissionMixin().submit_errors["EUNKNOWN"])
self.assertIsNotNone(resp[2])
@scenario('data/basic_scenario.xml', user_id='Bob')
@patch.object(sub_api, 'create_submission')
......@@ -49,6 +48,7 @@ class SubmissionTest(XBlockHandlerTestCase):
resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
self.assertFalse(resp[0])
self.assertEqual(resp[1], "EBADFORM")
self.assertIsNotNone(resp[2])
# In Studio preview mode, the runtime sets the user ID to None
@scenario('data/basic_scenario.xml', user_id=None)
......
......@@ -15,24 +15,25 @@ from openassessment.xblock.validation import (
validate_dates, validate_assessment_examples
)
STUB_I18N = lambda x: x
@ddt.ddt
class AssessmentValidationTest(TestCase):
@ddt.file_data('data/valid_assessments.json')
def test_valid_assessment(self, data):
success, msg = validate_assessments(data["assessments"], data["current_assessments"], data["is_released"])
success, msg = validate_assessments(data["assessments"], data["current_assessments"], data["is_released"], STUB_I18N)
self.assertTrue(success)
self.assertEqual(msg, u'')
@ddt.file_data('data/invalid_assessments.json')
def test_invalid_assessment(self, data):
success, msg = validate_assessments(data["assessments"], data["current_assessments"], data["is_released"])
success, msg = validate_assessments(data["assessments"], data["current_assessments"], data["is_released"], STUB_I18N)
self.assertFalse(success)
self.assertGreater(len(msg), 0)
def test_no_assessments(self):
success, msg = validate_assessments([], [], False)
success, msg = validate_assessments([], [], False, STUB_I18N)
self.assertFalse(success)
self.assertGreater(len(msg), 0)
......@@ -69,7 +70,7 @@ class AssessmentValidationTest(TestCase):
AssertionError
"""
success, msg = validate_assessments(assessments, current_assessments, is_released)
success, msg = validate_assessments(assessments, current_assessments, is_released, STUB_I18N)
self.assertEqual(success, expected_is_valid, msg=msg)
if not success:
......@@ -85,7 +86,7 @@ class RubricValidationTest(TestCase):
is_released = data.get('is_released', False)
is_example_based = data.get('is_example_based', False)
success, msg = validate_rubric(
data['rubric'], current_rubric,is_released, is_example_based
data['rubric'], current_rubric,is_released, is_example_based, STUB_I18N
)
self.assertTrue(success)
self.assertEqual(msg, u'')
......@@ -96,7 +97,7 @@ class RubricValidationTest(TestCase):
is_released = data.get('is_released', False)
is_example_based = data.get('is_example_based', False)
success, msg = validate_rubric(
data['rubric'], current_rubric, is_released, is_example_based
data['rubric'], current_rubric, is_released, is_example_based, STUB_I18N
)
self.assertFalse(success)
self.assertGreater(len(msg), 0)
......@@ -107,13 +108,13 @@ class AssessmentExamplesValidationTest(TestCase):
@ddt.file_data('data/valid_assessment_examples.json')
def test_valid_assessment_examples(self, data):
success, msg = validate_assessment_examples(data['rubric'], data['assessments'])
success, msg = validate_assessment_examples(data['rubric'], data['assessments'], STUB_I18N)
self.assertTrue(success)
self.assertEqual(msg, u'')
@ddt.file_data('data/invalid_assessment_examples.json')
def test_invalid_assessment_examples(self, data):
success, msg = validate_assessment_examples(data['rubric'], data['assessments'])
success, msg = validate_assessment_examples(data['rubric'], data['assessments'], STUB_I18N)
self.assertFalse(success)
self.assertGreater(len(msg), 0)
......@@ -152,7 +153,8 @@ class DateValidationTest(TestCase):
date_range('submission_start', 'submission_due'),
date_range('peer_start', 'peer_due'),
date_range('self_start', 'self_due'),
]
],
STUB_I18N
)
self.assertTrue(success, msg=msg)
......@@ -172,7 +174,8 @@ class DateValidationTest(TestCase):
date_range('submission_start', 'submission_due'),
date_range('peer_start', 'peer_due'),
date_range('self_start', 'self_due'),
]
],
STUB_I18N
)
self.assertFalse(success)
......@@ -181,16 +184,16 @@ class DateValidationTest(TestCase):
def test_invalid_date_format(self):
valid = dt(2014, 1, 1).replace(tzinfo=pytz.UTC).isoformat()
success, _ = validate_dates("invalid", valid, [(valid, valid)])
success, _ = validate_dates("invalid", valid, [(valid, valid)], STUB_I18N)
self.assertFalse(success)
success, _ = validate_dates(valid, "invalid", [(valid, valid)])
success, _ = validate_dates(valid, "invalid", [(valid, valid)], STUB_I18N)
self.assertFalse(success)
success, _ = validate_dates(valid, valid, [("invalid", valid)])
success, _ = validate_dates(valid, valid, [("invalid", valid)], STUB_I18N)
self.assertFalse(success)
success, _ = validate_dates(valid, valid, [(valid, "invalid")])
success, _ = validate_dates(valid, valid, [(valid, "invalid")], STUB_I18N)
self.assertFalse(success)
......@@ -285,7 +288,7 @@ class ValidationIntegrationTest(TestCase):
self.oa_block.rubric_criteria = []
self.oa_block.start = None
self.oa_block.due = None
self.validator = validator(self.oa_block)
self.validator = validator(self.oa_block, STUB_I18N)
def test_validates_successfully(self):
is_valid, msg = self.validator(self.RUBRIC, self.ASSESSMENTS)
......
......@@ -2,7 +2,6 @@
Validate changes to an XBlock before it is updated.
"""
from collections import Counter
from django.utils.translation import ugettext as _
from openassessment.assessment.serializers import rubric_from_dict, InvalidRubric
from openassessment.assessment.api.student_training import validate_training_examples
from openassessment.xblock.resolve_dates import resolve_dates, DateValidationError, InvalidDateFormat
......@@ -82,7 +81,7 @@ def _is_valid_assessment_sequence(assessments):
return sequence in valid_sequences
def validate_assessments(assessments, current_assessments, is_released):
def validate_assessments(assessments, current_assessments, is_released, _):
"""
Check that the assessment dict is semantically valid.
......@@ -99,6 +98,7 @@ def validate_assessments(assessments, current_assessments, is_released):
assessment models. Used to determine if the assessment configuration
has changed since the question had been released.
is_released (boolean) : True if the question has been released.
_ (function): The service function used to get the appropriate i18n text
Returns:
tuple (is_valid, msg) where
......@@ -158,7 +158,7 @@ def validate_assessments(assessments, current_assessments, is_released):
return (True, u'')
def validate_rubric(rubric_dict, current_rubric, is_released, is_example_based):
def validate_rubric(rubric_dict, current_rubric, is_released, is_example_based, _):
"""
Check that the rubric is semantically valid.
......@@ -167,6 +167,7 @@ def validate_rubric(rubric_dict, current_rubric, is_released, is_example_based):
current_rubric (dict): Serialized Rubric model representing the current state of the rubric.
is_released (bool): True if and only if the problem has been released.
is_example_based (bool): True if and only if this is an example-based assessment.
_ (function): The service function used to get the appropriate i18n text
Returns:
tuple (is_valid, msg) where
......@@ -176,7 +177,7 @@ def validate_rubric(rubric_dict, current_rubric, is_released, is_example_based):
try:
rubric_from_dict(rubric_dict)
except InvalidRubric:
return (False, u'This rubric definition is not valid.')
return (False, _(u'This rubric definition is not valid.'))
# No duplicate criteria names
duplicates = _duplicates([criterion['name'] for criterion in rubric_dict['criteria']])
......@@ -229,7 +230,7 @@ def validate_rubric(rubric_dict, current_rubric, is_released, is_example_based):
current_criterion_names = set(criterion.get('name') for criterion in current_rubric['criteria'])
new_criterion_names = set(criterion.get('name') for criterion in rubric_dict['criteria'])
if current_criterion_names != new_criterion_names:
return (False, u'Criteria names cannot be changed after a problem is released')
return (False, _(u'Criteria names cannot be changed after a problem is released'))
# Number of options for each criterion must be the same
for new_criterion, old_criterion in _match_by_order(rubric_dict['criteria'], current_rubric['criteria']):
......@@ -244,7 +245,7 @@ def validate_rubric(rubric_dict, current_rubric, is_released, is_example_based):
return (True, u'')
def validate_dates(start, end, date_ranges):
def validate_dates(start, end, date_ranges, _):
"""
Check that start and due dates are valid.
......@@ -252,6 +253,7 @@ def validate_dates(start, end, date_ranges):
start (str): ISO-formatted date string indicating when the problem opens.
end (str): ISO-formatted date string indicating when the problem closes.
date_ranges (list of tuples): List of (start, end) pair for each submission / assessment.
_ (function): The service function used to get the appropriate i18n text
Returns:
tuple (is_valid, msg) where
......@@ -259,20 +261,21 @@ def validate_dates(start, end, date_ranges):
and msg describes any validation errors found.
"""
try:
resolve_dates(start, end, date_ranges)
resolve_dates(start, end, date_ranges, _)
except (DateValidationError, InvalidDateFormat) as ex:
return (False, unicode(ex))
else:
return (True, u'')
def validate_assessment_examples(rubric_dict, assessments):
def validate_assessment_examples(rubric_dict, assessments, _):
"""
Validate assessment training examples.
Args:
rubric_dict (dict): The serialized rubric model.
assessments (list of dict): List of assessment dictionaries.
_ (function): The service function used to get the appropriate i18n text
Returns:
tuple (is_valid, msg) where
......@@ -298,13 +301,14 @@ def validate_assessment_examples(rubric_dict, assessments):
return True, u''
def validator(oa_block, strict_post_release=True):
def validator(oa_block, _, strict_post_release=True):
"""
Return a validator function configured for the XBlock.
This will validate assessments, rubrics, and dates.
Args:
oa_block (OpenAssessmentBlock): The XBlock being updated.
_ (function): The service function used to get the appropriate i18n text
Keyword Arguments:
strict_post_release (bool): If true, restrict what authors can update once
......@@ -320,7 +324,7 @@ def validator(oa_block, strict_post_release=True):
# Assessments
current_assessments = oa_block.rubric_assessments
success, msg = validate_assessments(assessments, current_assessments, is_released)
success, msg = validate_assessments(assessments, current_assessments, is_released, _)
if not success:
return (False, msg)
......@@ -330,19 +334,19 @@ def validator(oa_block, strict_post_release=True):
'prompt': oa_block.prompt,
'criteria': oa_block.rubric_criteria
}
success, msg = validate_rubric(rubric_dict, current_rubric, is_released, is_example_based)
success, msg = validate_rubric(rubric_dict, current_rubric, is_released, is_example_based, _)
if not success:
return (False, msg)
# Training examples
success, msg = validate_assessment_examples(rubric_dict, assessments)
success, msg = validate_assessment_examples(rubric_dict, assessments, _)
if not success:
return (False, msg)
# Dates
submission_dates = [(submission_start, submission_due)]
assessment_dates = [(asmnt.get('start'), asmnt.get('due')) for asmnt in assessments]
success, msg = validate_dates(oa_block.start, oa_block.due, submission_dates + assessment_dates)
success, msg = validate_dates(oa_block.start, oa_block.due, submission_dates + assessment_dates, _)
if not success:
return (False, msg)
......
......@@ -6,7 +6,6 @@ import lxml.etree as etree
import pytz
import dateutil.parser
import defusedxml.ElementTree as safe_etree
from django.utils.translation import ugettext as _
class UpdateFromXmlError(Exception):
......@@ -201,7 +200,7 @@ def parse_date(date_str, name=""):
formatted_date = parsed_date.strftime("%Y-%m-%dT%H:%M:%S")
return unicode(formatted_date)
except (ValueError, TypeError):
msg = _(
msg = (
'The format of the given date ({date}) for the {name} is invalid. '
'Make sure the date is formatted as YYYY-MM-DDTHH:MM:SS.'
).format(date=date_str, name=name)
......@@ -251,16 +250,16 @@ def _parse_options_xml(options_root):
try:
option_dict['points'] = int(option.get('points'))
except ValueError:
raise UpdateFromXmlError(_('The value for "points" must be an integer.'))
raise UpdateFromXmlError('The value for "points" must be an integer.')
else:
raise UpdateFromXmlError(_('Every "option" element must contain a "points" attribute.'))
raise UpdateFromXmlError('Every "option" element must contain a "points" attribute.')
# Option name
option_name = option.find('name')
if option_name is not None:
option_dict['name'] = _safe_get_text(option_name)
else:
raise UpdateFromXmlError(_('Every "option" element must contain a "name" element.'))
raise UpdateFromXmlError('Every "option" element must contain a "name" element.')
# Option label
# Backwards compatibility: Older problem definitions won't have this.
......@@ -277,7 +276,7 @@ def _parse_options_xml(options_root):
if option_explanation is not None:
option_dict['explanation'] = _safe_get_text(option_explanation)
else:
raise UpdateFromXmlError(_('Every "option" element must contain an "explanation" element.'))
raise UpdateFromXmlError('Every "option" element must contain an "explanation" element.')
# Add the options dictionary to the list
options_list.append(option_dict)
......@@ -313,7 +312,7 @@ def _parse_criteria_xml(criteria_root):
if criterion_name is not None:
criterion_dict['name'] = _safe_get_text(criterion_name)
else:
raise UpdateFromXmlError(_('Every "criterion" element must contain a "name" element.'))
raise UpdateFromXmlError('Every "criterion" element must contain a "name" element.')
# Criterion label
# Backwards compatibility: Older problem definitions won't have this,
......@@ -330,14 +329,14 @@ def _parse_criteria_xml(criteria_root):
if criterion_prompt is not None:
criterion_dict['prompt'] = _safe_get_text(criterion_prompt)
else:
raise UpdateFromXmlError(_('Every "criterion" element must contain a "prompt" element.'))
raise UpdateFromXmlError('Every "criterion" element must contain a "prompt" element.')
# Criterion feedback (disabled, optional, or required)
criterion_feedback = criterion.get('feedback', 'disabled')
if criterion_feedback in ['optional', 'disabled', 'required']:
criterion_dict['feedback'] = criterion_feedback
else:
raise UpdateFromXmlError(_('Invalid value for "feedback" attribute: if specified, it must be set set to "optional" or "required".'))
raise UpdateFromXmlError('Invalid value for "feedback" attribute: if specified, it must be set set to "optional" or "required".')
# Criterion options
criterion_dict['options'] = _parse_options_xml(criterion)
......@@ -404,16 +403,16 @@ def parse_examples_xml(examples):
# Retrieve the answer from the training example
answer_elements = example_el.findall('answer')
if len(answer_elements) != 1:
raise UpdateFromXmlError(_(u'Each "example" element must contain exactly one "answer" element'))
raise UpdateFromXmlError(u'Each "example" element must contain exactly one "answer" element')
example_dict['answer'] = _safe_get_text(answer_elements[0])
# Retrieve the options selected from the training example
example_dict['options_selected'] = []
for select_el in example_el.findall('select'):
if 'criterion' not in select_el.attrib:
raise UpdateFromXmlError(_(u'Each "select" element must have a "criterion" attribute'))
raise UpdateFromXmlError(u'Each "select" element must have a "criterion" attribute')
if 'option' not in select_el.attrib:
raise UpdateFromXmlError(_(u'Each "select" element must have an "option" attribute'))
raise UpdateFromXmlError(u'Each "select" element must have an "option" attribute')
example_dict['options_selected'].append({
'criterion': unicode(select_el.get('criterion')),
......@@ -449,14 +448,14 @@ def parse_assessments_xml(assessments_root):
if 'name' in assessment.attrib:
assessment_dict['name'] = unicode(assessment.get('name'))
else:
raise UpdateFromXmlError(_('All "assessment" elements must contain a "name" element.'))
raise UpdateFromXmlError('All "assessment" elements must contain a "name" element.')
# Assessment start
if 'start' in assessment.attrib:
# Example-based assessment is NOT allowed to have a start date
if assessment_dict['name'] == 'example-based-assessment':
raise UpdateFromXmlError(_('Example-based assessment cannot have a start date'))
raise UpdateFromXmlError('Example-based assessment cannot have a start date')
# Other assessment types CAN have a start date
parsed_start = parse_date(assessment.get('start'), name="{} start date".format(assessment_dict['name']))
......@@ -471,7 +470,7 @@ def parse_assessments_xml(assessments_root):
# Example-based assessment is NOT allowed to have a due date
if assessment_dict['name'] == 'example-based-assessment':
raise UpdateFromXmlError(_('Example-based assessment cannot have a due date'))
raise UpdateFromXmlError('Example-based assessment cannot have a due date')
# Other assessment types CAN have a due date
parsed_due = parse_date(assessment.get('due'), name="{} due date".format(assessment_dict['name']))
......@@ -486,14 +485,14 @@ def parse_assessments_xml(assessments_root):
try:
assessment_dict['must_grade'] = int(assessment.get('must_grade'))
except ValueError:
raise UpdateFromXmlError(_('The "must_grade" value must be a positive integer.'))
raise UpdateFromXmlError('The "must_grade" value must be a positive integer.')
# Assessment must_be_graded_by
if 'must_be_graded_by' in assessment.attrib:
try:
assessment_dict['must_be_graded_by'] = int(assessment.get('must_be_graded_by'))
except ValueError:
raise UpdateFromXmlError(_('The "must_be_graded_by" value must be a positive integer.'))
raise UpdateFromXmlError('The "must_be_graded_by" value must be a positive integer.')
# Training examples
examples = assessment.findall('example')
......@@ -714,7 +713,7 @@ def parse_from_xml(root):
# Check that the root has the correct tag
if root.tag != 'openassessment':
raise UpdateFromXmlError(_('Every open assessment problem must contain an "openassessment" element.'))
raise UpdateFromXmlError('Every open assessment problem must contain an "openassessment" element.')
# Retrieve the start date for the submission
# Set it to None by default; we will update it to the latest start date later on
......@@ -735,21 +734,21 @@ def parse_from_xml(root):
# Retrieve the title
title_el = root.find('title')
if title_el is None:
raise UpdateFromXmlError(_('Every assessment must contain a "title" element.'))
raise UpdateFromXmlError('Every assessment must contain a "title" element.')
else:
title = _safe_get_text(title_el)
# Retrieve the rubric
rubric_el = root.find('rubric')
if rubric_el is None:
raise UpdateFromXmlError(_('Every assessment must contain a "rubric" element.'))
raise UpdateFromXmlError('Every assessment must contain a "rubric" element.')
else:
rubric = parse_rubric_xml(rubric_el)
# Retrieve the assessments
assessments_el = root.find('assessments')
if assessments_el is None:
raise UpdateFromXmlError(_('Every assessment must contain an "assessments" element.'))
raise UpdateFromXmlError('Every assessment must contain an "assessments" element.')
else:
assessments = parse_assessments_xml(assessments_el)
......@@ -802,7 +801,7 @@ def _unicode_to_xml(xml):
try:
return safe_etree.fromstring(xml.encode('utf-8'))
except (ValueError, safe_etree.ParseError):
raise UpdateFromXmlError(_("An error occurred while parsing the XML content."))
raise UpdateFromXmlError("An error occurred while parsing the XML content.")
def parse_examples_from_xml_str(xml):
......
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