Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-ora2
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-ora2
Commits
08a5800a
Commit
08a5800a
authored
Aug 04, 2014
by
Stephen Sanchez
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #564 from edx/sanchez/use-i18n-xblock-service
Using the XBlock i18n Service for ORA2
parents
c21b3fd0
4b866df3
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
178 additions
and
158 deletions
+178
-158
openassessment/xblock/grade_mixin.py
+5
-6
openassessment/xblock/openassessmentblock.py
+9
-3
openassessment/xblock/peer_assessment_mixin.py
+12
-15
openassessment/xblock/resolve_dates.py
+12
-9
openassessment/xblock/self_assessment_mixin.py
+8
-9
openassessment/xblock/staff_info_mixin.py
+25
-17
openassessment/xblock/student_training_mixin.py
+7
-8
openassessment/xblock/studio_mixin.py
+9
-9
openassessment/xblock/submission_mixin.py
+18
-23
openassessment/xblock/test/test_resolve_dates.py
+14
-6
openassessment/xblock/test/test_submission.py
+3
-3
openassessment/xblock/test/test_validation.py
+18
-15
openassessment/xblock/validation.py
+17
-13
openassessment/xblock/xml.py
+21
-22
No files found.
openassessment/xblock/grade_mixin.py
View file @
08a5800a
...
@@ -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
):
"""
"""
...
...
openassessment/xblock/openassessmentblock.py
View file @
08a5800a
...
@@ -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
)
...
...
openassessment/xblock/peer_assessment_mixin.py
View file @
08a5800a
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
c
reate_rubric_dict
,
c
lean_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
))
...
...
openassessment/xblock/resolve_dates.py
View file @
08a5800a
...
@@ -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
(
...
...
openassessment/xblock/self_assessment_mixin.py
View file @
08a5800a
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
c
reate_rubric_dict
,
c
lean_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""
}
openassessment/xblock/staff_info_mixin.py
View file @
08a5800a
...
@@ -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): T
he error message to display to the user
error_
key (str): The key to t
he 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
))
}
}
openassessment/xblock/student_training_mixin.py
View file @
08a5800a
...
@@ -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
{
...
...
openassessment/xblock/studio_mixin.py
View file @
08a5800a
...
@@ -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
=
''
):
...
...
openassessment/xblock/submission_mixin.py
View file @
08a5800a
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
=
''
):
...
...
openassessment/xblock/test/test_resolve_dates.py
View file @
08a5800a
...
@@ -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
)
)
openassessment/xblock/test/test_submission.py
View file @
08a5800a
...
@@ -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
.
assert
Equal
(
resp
[
2
],
xblock
.
submit_errors
[
"ENOMULTI"
])
self
.
assert
IsNotNone
(
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
.
assert
Equal
(
resp
[
2
],
SubmissionMixin
()
.
submit_errors
[
"EUNKNOWN"
])
self
.
assert
IsNotNone
(
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
)
...
...
openassessment/xblock/test/test_validation.py
View file @
08a5800a
...
@@ -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
)
...
...
openassessment/xblock/validation.py
View file @
08a5800a
...
@@ -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
)
...
...
openassessment/xblock/xml.py
View file @
08a5800a
...
@@ -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
):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment