Commit 08a5800a by Stephen Sanchez

Merge pull request #564 from edx/sanchez/use-i18n-xblock-service

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