Commit 45c53fba by gradyward

Merge pull request #532 from edx/will/grady/self-assessment-feedback

Will/grady/self assessment feedback
parents f2f55958 d6012733
...@@ -89,7 +89,15 @@ def get_score(submission_uuid, requirements): ...@@ -89,7 +89,15 @@ def get_score(submission_uuid, requirements):
} }
def create_assessment(submission_uuid, user_id, options_selected, rubric_dict, scored_at=None): def create_assessment(
submission_uuid,
user_id,
options_selected,
criterion_feedback,
overall_feedback,
rubric_dict,
scored_at=None
):
""" """
Create a self-assessment for a submission. Create a self-assessment for a submission.
...@@ -97,6 +105,11 @@ def create_assessment(submission_uuid, user_id, options_selected, rubric_dict, s ...@@ -97,6 +105,11 @@ def create_assessment(submission_uuid, user_id, options_selected, rubric_dict, s
submission_uuid (str): The unique identifier for the submission being assessed. submission_uuid (str): The unique identifier for the submission being assessed.
user_id (str): The ID of the user creating the assessment. This must match the ID of the user who made the submission. user_id (str): The ID of the user creating the assessment. This must match the ID of the user who made the submission.
options_selected (dict): Mapping of rubric criterion names to option values selected. options_selected (dict): Mapping of rubric criterion names to option values selected.
criterion_feedback (dict): Dictionary mapping criterion names to the
free-form text feedback the user gave for the criterion.
Since criterion feedback is optional, some criteria may not appear
in the dictionary.
overall_feedback (unicode): Free-form text feedback on the submission overall.
rubric_dict (dict): Serialized Rubric model. rubric_dict (dict): Serialized Rubric model.
Kwargs: Kwargs:
...@@ -143,15 +156,24 @@ def create_assessment(submission_uuid, user_id, options_selected, rubric_dict, s ...@@ -143,15 +156,24 @@ def create_assessment(submission_uuid, user_id, options_selected, rubric_dict, s
rubric = rubric_from_dict(rubric_dict) rubric = rubric_from_dict(rubric_dict)
# Create the self assessment # Create the self assessment
assessment = Assessment.create(rubric, user_id, submission_uuid, SELF_TYPE, scored_at=scored_at) assessment = Assessment.create(
AssessmentPart.create_from_option_names(assessment, options_selected) rubric,
user_id,
submission_uuid,
SELF_TYPE,
scored_at=scored_at,
feedback=overall_feedback
)
# This will raise an `InvalidRubricSelection` if the selected options do not match the rubric.
AssessmentPart.create_from_option_names(assessment, options_selected, feedback=criterion_feedback)
_log_assessment(assessment, submission) _log_assessment(assessment, submission)
except InvalidRubric: except InvalidRubric as ex:
msg = "Invalid rubric definition" msg = "Invalid rubric definition: " + str(ex)
logger.warning(msg, exc_info=True) logger.warning(msg, exc_info=True)
raise SelfAssessmentRequestError(msg) raise SelfAssessmentRequestError(msg)
except InvalidRubricSelection: except InvalidRubricSelection as ex:
msg = "Selected options do not match the rubric" msg = "Selected options do not match the rubric: " + str(ex)
logger.warning(msg, exc_info=True) logger.warning(msg, exc_info=True)
raise SelfAssessmentRequestError(msg) raise SelfAssessmentRequestError(msg)
......
...@@ -243,10 +243,19 @@ class RubricIndex(object): ...@@ -243,10 +243,19 @@ class RubricIndex(object):
criterion.name: criterion criterion.name: criterion
for criterion in criteria for criterion in criteria
} }
self._option_index = {
(option.criterion.name, option.name): option # Finds the set of all criteria which have options by traversing through the options, and adding all of
for option in options # the options' associated criteria to an expanding set.
} criteria_with_options = set()
option_index = {}
for option in options:
option_index[(option.criterion.name, option.name)] = option
criteria_with_options.add(option.criterion)
# Anything not in the above mentioned set is a zero option criteria, and we save it here for future reference.
self._criteria_without_options = set(self._criteria_index.values()) - criteria_with_options
self._option_index = option_index
# By convention, if multiple options in the same criterion have the # By convention, if multiple options in the same criterion have the
# same point value, we return the *first* option. # same point value, we return the *first* option.
...@@ -379,10 +388,7 @@ class RubricIndex(object): ...@@ -379,10 +388,7 @@ class RubricIndex(object):
set of `Criterion` set of `Criterion`
""" """
return set( return self._criteria_without_options
criterion for criterion in self._criteria_index.values()
if criterion.options.count() == 0
)
class Assessment(models.Model): class Assessment(models.Model):
...@@ -655,8 +661,8 @@ class AssessmentPart(models.Model): ...@@ -655,8 +661,8 @@ class AssessmentPart(models.Model):
} }
# Validate that we have selections for all criteria # Validate that we have selections for all criteria
# This will raise an exception if we're missing any criteria # This will raise an exception if we're missing any selections/feedback required for criteria
cls._check_has_all_criteria(rubric_index, set(selected.keys() + feedback.keys())) cls._check_all_criteria_assessed(rubric_index, selected.keys(), feedback.keys())
# Retrieve the criteria/option/feedback for criteria that have options. # Retrieve the criteria/option/feedback for criteria that have options.
# Since we're using the rubric's index, we'll get an `InvalidRubricSelection` error # Since we're using the rubric's index, we'll get an `InvalidRubricSelection` error
...@@ -773,3 +779,35 @@ class AssessmentPart(models.Model): ...@@ -773,3 +779,35 @@ class AssessmentPart(models.Model):
if len(missing_criteria) > 0: if len(missing_criteria) > 0:
msg = u"Missing selections for criteria: {missing}".format(missing=missing_criteria) msg = u"Missing selections for criteria: {missing}".format(missing=missing_criteria)
raise InvalidRubricSelection(msg) raise InvalidRubricSelection(msg)
@classmethod
def _check_all_criteria_assessed(cls, rubric_index, selected_criteria, criteria_feedback):
"""
Verify that we've selected options OR have feedback for all criteria in the rubric.
Verifies the predicate for all criteria (X) in the rubric:
has-an-option-selected(X) OR (has-zero-options(X) AND has-criterion-feedback(X))
Args:
rubric_index (RubricIndex): The index of the rubric's data.
selected_criteria (list): list of criterion names that have an option selected
criteria_feedback (list): list of criterion names that have feedback on them
Returns:
None
Raises:
InvalidRubricSelection
"""
missing_option_selections = rubric_index.find_missing_criteria(selected_criteria)
zero_option_criteria = set([c.name for c in rubric_index.find_criteria_without_options()])
zero_option_criteria_missing_feedback = zero_option_criteria - set(criteria_feedback)
optioned_criteria_missing_selection = missing_option_selections - zero_option_criteria
missing_criteria = zero_option_criteria_missing_feedback | optioned_criteria_missing_selection
if len(missing_criteria) > 0:
msg = u"Missing selections for criteria: {missing}".format(missing=', '.join(missing_criteria))
raise InvalidRubricSelection(msg)
\ No newline at end of file
{
"No Option Selected, Has Options, No Feedback": {
"has_option_selected": false,
"has_zero_options": false,
"has_feedback": false,
"expected_error": true
},
"No Option Selected, Has Options, Has Feedback": {
"has_option_selected": false,
"has_zero_options": false,
"has_feedback": true,
"expected_error": true
},
"No Option Selected, No Options, No Feedback": {
"has_option_selected": false,
"has_zero_options": true,
"has_feedback": false,
"expected_error": true
},
"No Option Selected, No Options, Has Feedback": {
"has_option_selected": false,
"has_zero_options": true,
"has_feedback": true,
"expected_error": false
},
"Has Option Selected, Has Options, No Feedback": {
"has_option_selected": true,
"has_zero_options": false,
"has_feedback": false,
"expected_error": false
},
"Has Option Selected, No Options, Has Feedback": {
"has_option_selected": true,
"has_zero_options": true,
"has_feedback": true,
"expected_error": true
},
"Has Option Selected, No Options, No Feedback": {
"has_option_selected": true,
"has_zero_options": true,
"has_feedback": false,
"expected_error": true
},
"Has Option Selected, Has Options, Has Feedback": {
"has_option_selected": true,
"has_zero_options": false,
"has_feedback": true,
"expected_error": false
}
}
\ No newline at end of file
...@@ -2,13 +2,16 @@ ...@@ -2,13 +2,16 @@
""" """
Tests for the assessment Django models. Tests for the assessment Django models.
""" """
import copy import copy, ddt
from openassessment.test_utils import CacheResetTest from openassessment.test_utils import CacheResetTest
from openassessment.assessment.serializers import rubric_from_dict from openassessment.assessment.serializers import rubric_from_dict
from openassessment.assessment.models import Assessment, AssessmentPart, InvalidRubricSelection from openassessment.assessment.models import Assessment, AssessmentPart, InvalidRubricSelection
from .constants import RUBRIC from .constants import RUBRIC
from openassessment.assessment.api.self import create_assessment
from submissions.api import create_submission
from openassessment.assessment.errors import SelfAssessmentRequestError
@ddt.ddt
class AssessmentTest(CacheResetTest): class AssessmentTest(CacheResetTest):
""" """
Tests for the `Assessment` and `AssessmentPart` models. Tests for the `Assessment` and `AssessmentPart` models.
...@@ -148,3 +151,65 @@ class AssessmentTest(CacheResetTest): ...@@ -148,3 +151,65 @@ class AssessmentTest(CacheResetTest):
criterion['options'] = [] criterion['options'] = []
return rubric_from_dict(rubric_dict) return rubric_from_dict(rubric_dict)
@ddt.file_data('data/models_check_criteria_assessed.json')
def test_check_all_criteria_assessed(self, data):
student_item = {
'student_id': u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗',
'item_id': 'test_item',
'course_id': 'test_course',
'item_type': 'test_type'
}
submission = create_submission(student_item, "Test answer")
rubric, options_selected, criterion_feedback = self._create_data_structures_with_criterion_properties(
has_option_selected=data['has_option_selected'],
has_zero_options=data['has_zero_options'],
has_feedback=data['has_feedback']
)
error = False
try:
create_assessment(
submission['uuid'], student_item['student_id'], options_selected,
criterion_feedback, "overall feedback", rubric
)
except SelfAssessmentRequestError:
error = True
self.assertTrue(data['expected_error'] == error)
def _create_data_structures_with_criterion_properties(
self,
has_option_selected=True,
has_zero_options=True,
has_feedback=True
):
"""
Generates a dummy set of criterion definition structures that will allow us to specificy a specific combination
of criterion attributes for a test case.
"""
options = []
if not has_zero_options:
options = [{
"name": "Okay",
"points": 1,
"description": "It was okay I guess."
}]
rubric = {
'criteria': [
{
"name": "Quality",
"prompt": "How 'good' was it?",
"options": options
}
]
}
options_selected = {}
if has_option_selected:
options_selected['Quality'] = 'Okay'
criterion_feedback = {}
if has_feedback:
criterion_feedback['Quality'] = "This was an assignment of average quality."
return rubric, options_selected, criterion_feedback
\ No newline at end of file
...@@ -51,6 +51,16 @@ class TestSelfApi(CacheResetTest): ...@@ -51,6 +51,16 @@ class TestSelfApi(CacheResetTest):
"accuracy": "very accurate", "accuracy": "very accurate",
} }
CRITERION_FEEDBACK = {
"clarity": "Like a morning in the restful city of San Fransisco, the piece was indescribable, beautiful, and too foggy to properly comprehend.",
"accuracy": "Like my sister's cutting comments about my weight, I may not have enjoyed the piece, but I cannot fault it for its factual nature."
}
OVERALL_FEEDBACK = (
u"Unfortunately, the nature of being is too complex to comment, judge, or discern any one"
u"arbitrary set of things over another."
)
def test_create_assessment(self): def test_create_assessment(self):
# Initially, there should be no submission or self assessment # Initially, there should be no submission or self assessment
self.assertEqual(get_assessment("5"), None) self.assertEqual(get_assessment("5"), None)
...@@ -66,7 +76,7 @@ class TestSelfApi(CacheResetTest): ...@@ -66,7 +76,7 @@ class TestSelfApi(CacheResetTest):
# Create a self-assessment for the submission # Create a self-assessment for the submission
assessment = create_assessment( assessment = create_assessment(
submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗', submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗',
self.OPTIONS_SELECTED, self.RUBRIC, self.OPTIONS_SELECTED, self.CRITERION_FEEDBACK, self.OVERALL_FEEDBACK, self.RUBRIC,
scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc) scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc)
) )
...@@ -82,7 +92,7 @@ class TestSelfApi(CacheResetTest): ...@@ -82,7 +92,7 @@ class TestSelfApi(CacheResetTest):
self.assertEqual(assessment['submission_uuid'], submission['uuid']) self.assertEqual(assessment['submission_uuid'], submission['uuid'])
self.assertEqual(assessment['points_earned'], 8) self.assertEqual(assessment['points_earned'], 8)
self.assertEqual(assessment['points_possible'], 10) self.assertEqual(assessment['points_possible'], 10)
self.assertEqual(assessment['feedback'], u'') self.assertEqual(assessment['feedback'], u'' + self.OVERALL_FEEDBACK)
self.assertEqual(assessment['score_type'], u'SE') self.assertEqual(assessment['score_type'], u'SE')
def test_create_assessment_no_submission(self): def test_create_assessment_no_submission(self):
...@@ -90,7 +100,7 @@ class TestSelfApi(CacheResetTest): ...@@ -90,7 +100,7 @@ class TestSelfApi(CacheResetTest):
with self.assertRaises(SelfAssessmentRequestError): with self.assertRaises(SelfAssessmentRequestError):
create_assessment( create_assessment(
'invalid_submission_uuid', u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗', 'invalid_submission_uuid', u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗',
self.OPTIONS_SELECTED, self.RUBRIC, self.OPTIONS_SELECTED, self.CRITERION_FEEDBACK, self.OVERALL_FEEDBACK, self.RUBRIC,
scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc) scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc)
) )
...@@ -102,7 +112,22 @@ class TestSelfApi(CacheResetTest): ...@@ -102,7 +112,22 @@ class TestSelfApi(CacheResetTest):
with self.assertRaises(SelfAssessmentRequestError): with self.assertRaises(SelfAssessmentRequestError):
create_assessment( create_assessment(
'invalid_submission_uuid', u'another user', 'invalid_submission_uuid', u'another user',
self.OPTIONS_SELECTED, self.RUBRIC, self.OPTIONS_SELECTED, self.CRITERION_FEEDBACK, self.OVERALL_FEEDBACK, self.RUBRIC,
scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc)
)
def test_create_assessment_invalid_criterion_feedback(self):
# Create a submission
submission = create_submission(self.STUDENT_ITEM, "Test answer")
# Mutate the criterion feedback to not include all the appropriate criteria.
criterion_feedback = {"clarify": "not", "accurate": "sure"}
# Attempt to create a self-assessment with criterion_feedback that do not match the rubric
with self.assertRaises(SelfAssessmentRequestError):
create_assessment(
submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗',
self.OPTIONS_SELECTED, criterion_feedback, self.OVERALL_FEEDBACK, self.RUBRIC,
scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc) scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc)
) )
...@@ -118,7 +143,7 @@ class TestSelfApi(CacheResetTest): ...@@ -118,7 +143,7 @@ class TestSelfApi(CacheResetTest):
with self.assertRaises(SelfAssessmentRequestError): with self.assertRaises(SelfAssessmentRequestError):
create_assessment( create_assessment(
submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗', submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗',
options, self.RUBRIC, options, self.CRITERION_FEEDBACK, self.OVERALL_FEEDBACK, self.RUBRIC,
scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc) scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc)
) )
...@@ -134,7 +159,7 @@ class TestSelfApi(CacheResetTest): ...@@ -134,7 +159,7 @@ class TestSelfApi(CacheResetTest):
with self.assertRaises(SelfAssessmentRequestError): with self.assertRaises(SelfAssessmentRequestError):
create_assessment( create_assessment(
submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗', submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗',
options, self.RUBRIC, options, self.CRITERION_FEEDBACK, self.OVERALL_FEEDBACK, self.RUBRIC,
scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc) scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc)
) )
...@@ -150,7 +175,7 @@ class TestSelfApi(CacheResetTest): ...@@ -150,7 +175,7 @@ class TestSelfApi(CacheResetTest):
with self.assertRaises(SelfAssessmentRequestError): with self.assertRaises(SelfAssessmentRequestError):
create_assessment( create_assessment(
submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗', submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗',
options, self.RUBRIC, options, self.CRITERION_FEEDBACK, self.OVERALL_FEEDBACK, self.RUBRIC,
scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc) scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc)
) )
...@@ -165,7 +190,7 @@ class TestSelfApi(CacheResetTest): ...@@ -165,7 +190,7 @@ class TestSelfApi(CacheResetTest):
# Do not override the scored_at timestamp, so it should be set to the current time # Do not override the scored_at timestamp, so it should be set to the current time
assessment = create_assessment( assessment = create_assessment(
submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗', submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗',
self.OPTIONS_SELECTED, self.RUBRIC, self.OPTIONS_SELECTED, self.CRITERION_FEEDBACK, self.OVERALL_FEEDBACK, self.RUBRIC,
) )
# Retrieve the self-assessment # Retrieve the self-assessment
...@@ -183,14 +208,14 @@ class TestSelfApi(CacheResetTest): ...@@ -183,14 +208,14 @@ class TestSelfApi(CacheResetTest):
# Self assess once # Self assess once
assessment = create_assessment( assessment = create_assessment(
submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗', submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗',
self.OPTIONS_SELECTED, self.RUBRIC, self.OPTIONS_SELECTED, self.CRITERION_FEEDBACK, self.OVERALL_FEEDBACK, self.RUBRIC,
) )
# Attempt to self-assess again, which should raise an exception # Attempt to self-assess again, which should raise an exception
with self.assertRaises(SelfAssessmentRequestError): with self.assertRaises(SelfAssessmentRequestError):
create_assessment( create_assessment(
submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗', submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗',
self.OPTIONS_SELECTED, self.RUBRIC, self.OPTIONS_SELECTED, self.CRITERION_FEEDBACK, self.OVERALL_FEEDBACK, self.RUBRIC,
) )
# Expect that we still have the original assessment # Expect that we still have the original assessment
...@@ -213,17 +238,20 @@ class TestSelfApi(CacheResetTest): ...@@ -213,17 +238,20 @@ class TestSelfApi(CacheResetTest):
"options": [] "options": []
}) })
criterion_feedback = copy.deepcopy(self.CRITERION_FEEDBACK)
criterion_feedback['feedback only'] = "This is the feedback for the Zero Option Criterion."
# Create a self-assessment for the submission # Create a self-assessment for the submission
assessment = create_assessment( assessment = create_assessment(
submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗', submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗',
self.OPTIONS_SELECTED, rubric, self.OPTIONS_SELECTED, criterion_feedback, self.OVERALL_FEEDBACK, rubric,
scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc) scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc)
) )
# The self-assessment should have set the feedback for # The self-assessment should have set the feedback for
# the criterion with no options to an empty string # the criterion with no options to an empty string
self.assertEqual(assessment["parts"][2]["option"], None) self.assertEqual(assessment["parts"][2]["option"], None)
self.assertEqual(assessment["parts"][2]["feedback"], u"") self.assertEqual(assessment["parts"][2]["feedback"], u"This is the feedback for the Zero Option Criterion.")
def test_create_assessment_all_criteria_have_zero_options(self): def test_create_assessment_all_criteria_have_zero_options(self):
# Create a submission to self-assess # Create a submission to self-assess
...@@ -237,14 +265,25 @@ class TestSelfApi(CacheResetTest): ...@@ -237,14 +265,25 @@ class TestSelfApi(CacheResetTest):
# Create a self-assessment for the submission # Create a self-assessment for the submission
# We don't select any options, since none of the criteria have options # We don't select any options, since none of the criteria have options
options_selected = {} options_selected = {}
# However, because they don't have options, they need to have criterion feedback.
criterion_feedback = {
'clarity': 'I thought it was about as accurate as Scrubs is to the medical profession.',
'accuracy': 'I thought it was about as accurate as Scrubs is to the medical profession.'
}
overall_feedback = ""
assessment = create_assessment( assessment = create_assessment(
submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗', submission['uuid'], u'𝖙𝖊𝖘𝖙 𝖚𝖘𝖊𝖗',
options_selected, rubric, options_selected, criterion_feedback, overall_feedback,
scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc) rubric, scored_at=datetime.datetime(2014, 4, 1).replace(tzinfo=pytz.utc)
) )
# The self-assessment should have set the feedback for # The self-assessment should have set the feedback for
# all criteria to an empty string. # all criteria to an empty string.
for part in assessment["parts"]: for part in assessment["parts"]:
self.assertEqual(part["option"], None) self.assertEqual(part["option"], None)
self.assertEqual(part["feedback"], u"") self.assertEqual(
part["feedback"], u'I thought it was about as accurate as Scrubs is to the medical profession.'
)
...@@ -107,7 +107,7 @@ class Command(BaseCommand): ...@@ -107,7 +107,7 @@ class Command(BaseCommand):
print "-- Creating self assessment" print "-- Creating self assessment"
self_api.create_assessment( self_api.create_assessment(
submission_uuid, student_item['student_id'], submission_uuid, student_item['student_id'],
options_selected, rubric options_selected, {}, " ".join(loremipsum.get_paragraphs(2)), rubric
) )
@property @property
......
...@@ -146,27 +146,42 @@ ...@@ -146,27 +146,42 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if criterion.feedback %} {% if criterion.peer_feedback or criterion.self_feedback %}
<li class="answer--feedback ui-toggle-visibility {% if criterion.options %}is--collapsed{% endif %}"> <li class="answer--feedback ui-toggle-visibility {% if criterion.options %}is--collapsed{% endif %}">
{% if criterion.options %} {% if criterion.options %}
<h5 class="answer--feedback__title ui-toggle-visibility__control"> <h5 class="answer--feedback__title ui-toggle-visibility__control">
<i class="ico icon-caret-right"></i> <i class="ico icon-caret-right"></i>
<span class="answer--feedback__title__copy">{% trans "Additional Comments" %} ({{ criterion.feedback|length }})</span> {% if criterion.self_feedback %}
<span class="answer--feedback__title__copy">{% trans "Additional Comments" %} ({{ criterion.peer_feedback|length|add:'1' }})</span>
{% else %}
<span class="answer--feedback__title__copy">{% trans "Additional Comments" %} ({{ criterion.peer_feedback|length }})</span>
{% endif %}
</h5> </h5>
{% endif %} {% endif %}
<ul class="answer--feedback__content {% if criterion.options %}ui-toggle-visibility__content{% endif %}"> <ul class="answer--feedback__content {% if criterion.options %}ui-toggle-visibility__content{% endif %}">
{% for feedback in criterion.feedback %} {% for feedback in criterion.peer_feedback %}
<li class="feedback feedback--{{ forloop.counter }}"> <li class="feedback feedback--{{ forloop.counter }}">
<h6 class="feedback__source"> <h6 class="feedback__source">
{% trans "Peer" %} {{ forloop.counter }} {% trans "Peer" %} {{ forloop.counter }}
</h6> </h6>
<div class="feedback__value"> <div class="feedback__value">
{{ feedback }} {{ feedback }}
</div> </div>
</li> </li>
{% endfor %} {% endfor %}
{% if criterion.self_feedback %}
<li class="feedback feedback--{{ forloop.counter }}">
<h6 class="feedback__source">
{% trans "Your Assessment" %}
</h6>
<div class="feedback__value">
{{ criterion.self_feedback }}
</div>
</li>
{% endif %}
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
...@@ -175,7 +190,7 @@ ...@@ -175,7 +190,7 @@
</li> </li>
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}
{% if peer_assessments %} {% if peer_assessments or self_assessment.feedback %}
<li class="question question--feedback ui-toggle-visibility"> <li class="question question--feedback ui-toggle-visibility">
<h4 class="question__title ui-toggle-visibility__control"> <h4 class="question__title ui-toggle-visibility__control">
<i class="ico icon-caret-right"></i> <i class="ico icon-caret-right"></i>
...@@ -204,6 +219,23 @@ ...@@ -204,6 +219,23 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}
{% if self_assessment.feedback %}
<li class="answer self-evaluation--0" id="question--feedback__answer-0">
<h5 class="answer__title">
<span class="answer__source">
<span class="label sr">{% trans "Self assessment" %}: </span>
<span class="value">{% trans "Self assessment" %}</span>
</span>
</h5>
<div class="answer__value">
<h6 class="label sr">{% trans "Your assessment" %}: </h6>
<div class="value">
<p>{{ self_assessment.feedback }}</p>
</div>
</div>
</li>
{% endif %}
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
......
{% spaceless %}
{% load i18n %}
<fieldset class="assessment__fields">
<ol class="list list--fields assessment__rubric">
{% for criterion in rubric_criteria %}
<li
class="field field--radio is--required assessment__rubric__question ui-toggle-visibility {% if criterion.options %}has--options{% endif %}"
id="assessment__rubric__question--{{ criterion.order_num }}"
>
<h4 class="question__title ui-toggle-visibility__control">
<i class="ico icon-caret-right"></i>
<span class="ui-toggle-visibility__control__copy question__title__copy">{{ criterion.prompt }}</span>
<span class="label--required sr">* ({% trans "Required" %})</span>
</h4>
<div class="ui-toggle-visibility__content">
<ol class="question__answers">
{% for option in criterion.options %}
<li class="answer">
<div class="wrapper--input">
<input type="radio"
name="{{ criterion.name }}"
id="assessment__rubric__question--{{ criterion.order_num }}__{{ option.order_num }}"
class="answer__value"
value="{{ option.name }}" />
<label for="assessment__rubric__question--{{ criterion.order_num }}__{{ option.order_num }}"
class="answer__label"
>{{ option.name }}</label>
</div>
<div class="wrapper--metadata">
<span class="answer__tip">{{ option.explanation }}</span>
<span class="answer__points">{{ option.points }} <span class="answer__points__label">{% trans "points" %}</span></span>
</div>
</li>
{% endfor %}
{% if criterion.feedback == 'optional' or criterion.feedback == 'required' %}
<li class="answer--feedback">
<div class="wrapper--input">
<label for="assessment__rubric__question--{{ criterion.order_num }}__feedback" class="answer__label">{% trans "Comments" %}</label>
<textarea
id="assessment__rubric__question--{{ criterion.order_num }}__feedback"
class="answer__value"
value="{{ criterion.name }}"
name="{{ criterion.name }}"
maxlength="300"
{% if criterion.feedback == 'required' %}required{% endif %}
>
</textarea>
</div>
</li>
{% endif %}
</ol>
</div>
</li>
{% endfor %}
<li class="wrapper--input field field--textarea assessment__rubric__question assessment__rubric__question--feedback" id="assessment__rubric__question--feedback">
<label class="question__title" for="assessment__rubric__question--feedback__value">
<span class="question__title__copy">{{ rubric_feedback_prompt }}</span>
</label>
<div class="wrapper--input">
<textarea
id="assessment__rubric__question--feedback__value"
placeholder="{% trans "I noticed that this response..." %}"
maxlength="500"
>
</textarea>
</div>
</li>
</ol>
</fieldset>
{% endspaceless %}
\ No newline at end of file
...@@ -72,77 +72,7 @@ ...@@ -72,77 +72,7 @@
</div> </div>
<form id="peer-assessment--001__assessment" class="peer-assessment__assessment" method="post"> <form id="peer-assessment--001__assessment" class="peer-assessment__assessment" method="post">
<fieldset class="assessment__fields"> {% include "openassessmentblock/oa_rubric.html" %}
<ol class="list list--fields assessment__rubric">
{% for criterion in rubric_criteria %}
<li
class="field field--radio is--required assessment__rubric__question ui-toggle-visibility {% if criterion.options %}has--options{% endif %}"
id="assessment__rubric__question--{{ criterion.order_num }}"
>
<h4 class="question__title ui-toggle-visibility__control">
<i class="ico icon-caret-right"></i>
<span class="ui-toggle-visibility__control__copy question__title__copy">{{ criterion.prompt }}</span>
<span class="label--required sr">* ({% trans "Required" %})</span>
</h4>
<div class="ui-toggle-visibility__content">
<ol class="question__answers">
{% for option in criterion.options %}
<li class="answer">
<div class="wrapper--input">
<input type="radio"
name="{{ criterion.name }}"
id="assessment__rubric__question--{{ criterion.order_num }}__{{ option.order_num }}"
class="answer__value"
value="{{ option.name }}" />
<label for="assessment__rubric__question--{{ criterion.order_num }}__{{ option.order_num }}"
class="answer__label"
>{{ option.name }}</label>
</div>
<div class="wrapper--metadata">
<span class="answer__tip">{{ option.explanation }}</span>
<span class="answer__points">{{ option.points }} <span class="answer__points__label">{% trans "points" %}</span></span>
</div>
</li>
{% endfor %}
{% if criterion.feedback == 'optional' or criterion.feedback == 'required' %}
<li class="answer--feedback">
<div class="wrapper--input">
<label for="assessment__rubric__question--{{ criterion.order_num }}__feedback" class="answer__label">{% trans "Comments" %}</label>
<textarea
id="assessment__rubric__question--{{ criterion.order_num }}__feedback"
class="answer__value"
value="{{ criterion.name }}"
name="{{ criterion.name }}"
maxlength="300"
{% if criterion.feedback == 'required' %}required{% endif %}
>
</textarea>
</div>
</li>
{% endif %}
</ol>
</div>
</li>
{% endfor %}
<li class="wrapper--input field field--textarea assessment__rubric__question assessment__rubric__question--feedback" id="assessment__rubric__question--feedback">
<label class="question__title" for="assessment__rubric__question--feedback__value">
<span class="question__title__copy">{{ rubric_feedback_prompt }}</span>
</label>
<div class="wrapper--input">
<textarea
id="assessment__rubric__question--feedback__value"
placeholder="{% trans "I noticed that this response..." %}"
maxlength="500"
>
</textarea>
</div>
</li>
</ol>
</fieldset>
</form> </form>
</article> </article>
</li> </li>
......
...@@ -59,46 +59,7 @@ ...@@ -59,46 +59,7 @@
</article> </article>
<form id="self-assessment--001__assessment" class="self-assessment__assessment" method="post"> <form id="self-assessment--001__assessment" class="self-assessment__assessment" method="post">
<fieldset class="assessment__fields"> {% include "openassessmentblock/oa_rubric.html" %}
<ol class="list list--fields assessment__rubric">
{% for criterion in rubric_criteria %}
{% if criterion.options %}
<li
class="field field--radio is--required assessment__rubric__question ui-toggle-visibility has--options"
id="assessment__rubric__question--{{ criterion.order_num }}"
>
<h4 class="question__title ui-toggle-visibility__control">
<i class="ico icon-caret-right"></i>
<span class="question__title__copy">{{ criterion.prompt }}</span>
<span class="label--required sr">* ({% trans "Required" %})</span>
</h4>
<div class="ui-toggle-visibility__content">
<ol class="question__answers">
{% for option in criterion.options %}
<li class="answer">
<div class="wrapper--input">
<input type="radio"
name="{{ criterion.name }}"
id="assessment__rubric__question--{{ criterion.order_num }}__{{ option.order_num }}"
class="answer__value"
value="{{ option.name }}" />
<label for="assessment__rubric__question--{{ criterion.order_num }}__{{ option.order_num }}"
class="answer__label">{{ option.name }}</label>
</div>
<div class="wrapper--metadata">
<span class="answer__tip">{{ option.explanation }}</span>
<span class="answer__points">{{option.points}} <span class="answer__points__label">{% trans "points" %}</span></span>
</div>
</li>
{% endfor %}
</ol>
</div>
</li>
{% endif %}
{% endfor %}
</ol>
</fieldset>
</form> </form>
</div> </div>
......
...@@ -73,3 +73,23 @@ def create_rubric_dict(prompt, criteria): ...@@ -73,3 +73,23 @@ def create_rubric_dict(prompt, criteria):
"prompt": prompt, "prompt": prompt,
"criteria": criteria "criteria": criteria
} }
def clean_criterion_feedback(rubric_criteria, criterion_feedback):
"""
Remove per-criterion feedback for criteria with feedback disabled
in the rubric.
Args:
rubric_criteria (list): The rubric criteria from the problem definition.
criterion_feedback (dict): Mapping of criterion names to feedback text.
Returns:
dict
"""
return {
criterion['name']: criterion_feedback[criterion['name']]
for criterion in rubric_criteria
if criterion['name'] in criterion_feedback
and criterion.get('feedback', 'disabled') in ['optional', 'required']
}
...@@ -127,7 +127,7 @@ class GradeMixin(object): ...@@ -127,7 +127,7 @@ class GradeMixin(object):
'peer_assessments': peer_assessments, 'peer_assessments': peer_assessments,
'self_assessment': self_assessment, 'self_assessment': self_assessment,
'example_based_assessment': example_based_assessment, 'example_based_assessment': example_based_assessment,
'rubric_criteria': self._rubric_criteria_with_feedback(peer_assessments), 'rubric_criteria': self._rubric_criteria_with_feedback(peer_assessments, self_assessment),
'has_submitted_feedback': has_submitted_feedback, 'has_submitted_feedback': has_submitted_feedback,
'allow_file_upload': self.allow_file_upload, 'allow_file_upload': self.allow_file_upload,
'file_url': self.get_download_url_from_submission(student_submission) 'file_url': self.get_download_url_from_submission(student_submission)
...@@ -218,13 +218,14 @@ class GradeMixin(object): ...@@ -218,13 +218,14 @@ class GradeMixin(object):
) )
return {'success': True, 'msg': _(u"Feedback saved.")} return {'success': True, 'msg': _(u"Feedback saved.")}
def _rubric_criteria_with_feedback(self, peer_assessments): def _rubric_criteria_with_feedback(self, peer_assessments, self_assessment):
""" """
Add per-criterion feedback from peer assessments to the rubric criteria. Add per-criterion feedback from peer assessments to the rubric criteria.
Filters out empty feedback. Filters out empty feedback.
Args: Args:
peer_assessments (list of dict): Serialized assessment models from the peer API. peer_assessments (list of dict): Serialized assessment models from the peer API.
self_assessment (dict): Serialized assessment model from the self API
Returns: Returns:
list of criterion dictionaries list of criterion dictionaries
...@@ -245,16 +246,24 @@ class GradeMixin(object): ...@@ -245,16 +246,24 @@ class GradeMixin(object):
] ]
""" """
criteria = copy.deepcopy(self.rubric_criteria) criteria = copy.deepcopy(self.rubric_criteria)
criteria_feedback = defaultdict(list) peer_criteria_feedback = defaultdict(list)
self_criteria_feedback = {}
for assessment in peer_assessments: for assessment in peer_assessments:
for part in assessment['parts']: for part in assessment['parts']:
if part['feedback']: if part['feedback']:
part_criterion_name = part['criterion']['name'] part_criterion_name = part['criterion']['name']
criteria_feedback[part_criterion_name].append(part['feedback']) peer_criteria_feedback[part_criterion_name].append(part['feedback'])
if self_assessment:
for part in self_assessment['parts']:
if part['feedback']:
part_criterion_name = part['criterion']['name']
self_criteria_feedback[part_criterion_name] = part['feedback']
for criterion in criteria: for criterion in criteria:
criterion_name = criterion['name'] criterion_name = criterion['name']
criterion['feedback'] = criteria_feedback[criterion_name] criterion['peer_feedback'] = peer_criteria_feedback[criterion_name]
criterion['self_feedback'] = self_criteria_feedback.get(criterion_name)
return criteria return criteria
...@@ -9,10 +9,8 @@ from openassessment.assessment.errors import ( ...@@ -9,10 +9,8 @@ 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 .resolve_dates import DISTANT_FUTURE from .resolve_dates import DISTANT_FUTURE
from .data_conversion import create_rubric_dict, clean_criterion_feedback
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -64,19 +62,15 @@ class PeerAssessmentMixin(object): ...@@ -64,19 +62,15 @@ class PeerAssessmentMixin(object):
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:
rubric_dict = {
'criteria': self.rubric_criteria
}
try: try:
# Create the assessment # Create the assessment
assessment = peer_api.create_assessment( assessment = peer_api.create_assessment(
self.submission_uuid, self.submission_uuid,
self.get_student_item_dict()["student_id"], self.get_student_item_dict()["student_id"],
data['options_selected'], data['options_selected'],
self._clean_criterion_feedback(data['criterion_feedback']), clean_criterion_feedback(self.rubric_criteria, data['criterion_feedback']),
data['overall_feedback'], data['overall_feedback'],
rubric_dict, create_rubric_dict(self.prompt, self.rubric_criteria),
assessment_ui_model['must_be_graded_by'] assessment_ui_model['must_be_graded_by']
) )
...@@ -268,22 +262,3 @@ class PeerAssessmentMixin(object): ...@@ -268,22 +262,3 @@ class PeerAssessmentMixin(object):
logger.exception(err) logger.exception(err)
return peer_submission return peer_submission
def _clean_criterion_feedback(self, criterion_feedback):
"""
Remove per-criterion feedback for criteria with feedback disabled
in the rubric.
Args:
criterion_feedback (dict): Mapping of criterion names to feedback text.
Returns:
dict
"""
return {
criterion['name']: criterion_feedback[criterion['name']]
for criterion in self.rubric_criteria
if criterion['name'] in criterion_feedback
and criterion.get('feedback', 'disabled') in ['optional', 'required']
}
...@@ -8,6 +8,7 @@ from openassessment.assessment.api import self as self_api ...@@ -8,6 +8,7 @@ from openassessment.assessment.api import self as self_api
from openassessment.workflow import api as workflow_api from openassessment.workflow import api as workflow_api
from submissions import api as submission_api from submissions import api as submission_api
from .resolve_dates import DISTANT_FUTURE from .resolve_dates import DISTANT_FUTURE
from .data_conversion import create_rubric_dict, clean_criterion_feedback
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -112,6 +113,12 @@ class SelfAssessmentMixin(object): ...@@ -112,6 +113,12 @@ class SelfAssessmentMixin(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': _(u"Missing options_selected key in request")}
if 'overall_feedback' not in data:
return {'success': False, 'msg': _('Must provide overall feedback in the assessment')}
if 'criterion_feedback' not in data:
return {'success': False, 'msg': _('Must provide feedback for criteria in the assessment')}
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': _(u"You must submit a response before you can perform a self-assessment.")}
...@@ -120,7 +127,9 @@ class SelfAssessmentMixin(object): ...@@ -120,7 +127,9 @@ class SelfAssessmentMixin(object):
self.submission_uuid, self.submission_uuid,
self.get_student_item_dict()['student_id'], self.get_student_item_dict()['student_id'],
data['options_selected'], data['options_selected'],
{"criteria": self.rubric_criteria} clean_criterion_feedback(self.rubric_criteria, data['criterion_feedback']),
data['overall_feedback'],
create_rubric_dict(self.prompt, self.rubric_criteria)
) )
self.publish_assessment_event("openassessmentblock.self_assess", assessment) self.publish_assessment_event("openassessmentblock.self_assess", assessment)
......
if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}OpenAssessment.BaseView=function(runtime,element,server){this.runtime=runtime;this.element=element;this.server=server;this.fileUploader=new OpenAssessment.FileUploader;this.responseView=new OpenAssessment.ResponseView(this.element,this.server,this.fileUploader,this);this.trainingView=new OpenAssessment.StudentTrainingView(this.element,this.server,this);this.selfView=new OpenAssessment.SelfView(this.element,this.server,this);this.peerView=new OpenAssessment.PeerView(this.element,this.server,this);this.gradeView=new OpenAssessment.GradeView(this.element,this.server,this);this.messageView=new OpenAssessment.MessageView(this.element,this.server,this);this.staffInfoView=new OpenAssessment.StaffInfoView(this.element,this.server,this)};OpenAssessment.BaseView.prototype={scrollToTop:function(){if($.scrollTo instanceof Function){$(window).scrollTo($("#openassessment__steps"),800,{offset:-50})}},setUpCollapseExpand:function(parentSel){parentSel.find(".ui-toggle-visibility__control").click(function(eventData){var sel=$(eventData.target).closest(".ui-toggle-visibility");sel.toggleClass("is--collapsed")})},load:function(){this.responseView.load();this.loadAssessmentModules();this.staffInfoView.load()},loadAssessmentModules:function(){this.trainingView.load();this.peerView.load();this.selfView.load();this.gradeView.load()},loadMessageView:function(){this.messageView.load()},toggleActionError:function(type,msg){var element=this.element;var container=null;if(type=="save"){container=".response__submission__actions"}else if(type=="submit"||type=="peer"||type=="self"||type=="student-training"){container=".step__actions"}else if(type=="feedback_assess"){container=".submission__feedback__actions"}else if(type=="upload"){container="#upload__error"}if(container===null){if(msg!==null){console.log(msg)}}else{var msgHtml=msg===null?"":msg;$(container+" .message__content",element).html("<p>"+msgHtml+"</p>");$(container,element).toggleClass("has--error",msg!==null)}},showLoadError:function(step){var container="#openassessment__"+step;$(container).toggleClass("has--error",true);$(container+" .step__status__value i").removeClass().addClass("ico icon-warning-sign");$(container+" .step__status__value .copy").html(gettext("Unable to Load"))}};function OpenAssessmentBlock(runtime,element){var server=new OpenAssessment.Server(runtime,element);var view=new OpenAssessment.BaseView(runtime,element,server);view.load()}OpenAssessment.StudioView=function(runtime,element,server){this.runtime=runtime;this.server=server;this.codeBox=CodeMirror.fromTextArea($(element).find(".openassessment-editor").first().get(0),{mode:"xml",lineNumbers:true,lineWrapping:true});var view=this;$(element).find(".openassessment-save-button").click(function(eventData){view.save()});$(element).find(".openassessment-cancel-button").click(function(eventData){view.cancel()})};OpenAssessment.StudioView.prototype={load:function(){var view=this;this.server.loadXml().done(function(xml){view.codeBox.setValue(xml)}).fail(function(msg){view.showError(msg)})},save:function(){var view=this;this.server.checkReleased().done(function(isReleased){if(isReleased){view.confirmPostReleaseUpdate($.proxy(view.updateXml,view))}else{view.updateXml()}}).fail(function(errMsg){view.showError(msg)})},confirmPostReleaseUpdate:function(onConfirm){var msg=gettext("This problem has already been released. Any changes will apply only to future assessments.");if(confirm(msg)){onConfirm()}},updateXml:function(){this.runtime.notify("save",{state:"start"});var xml=this.codeBox.getValue();var view=this;this.server.updateXml(xml).done(function(){view.runtime.notify("save",{state:"end"});view.load()}).fail(function(msg){view.showError(msg)})},cancel:function(){this.runtime.notify("cancel",{})},showError:function(errorMsg){this.runtime.notify("error",{msg:errorMsg})}};function OpenAssessmentEditor(runtime,element){var server=new OpenAssessment.Server(runtime,element);var view=new OpenAssessment.StudioView(runtime,element,server);view.load()}OpenAssessment.FileUploader=function(){this.upload=function(url,data,contentType){return $.Deferred(function(defer){$.ajax({url:url,type:"PUT",data:data,async:false,processData:false,contentType:contentType}).done(function(data,textStatus,jqXHR){defer.resolve()}).fail(function(data,textStatus,jqXHR){defer.rejectWith(this,[textStatus])})}).promise()}};OpenAssessment.GradeView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.GradeView.prototype={load:function(){var view=this;var baseView=this.baseView;this.server.render("grade").done(function(html){$("#openassessment__grade",view.element).replaceWith(html);view.installHandlers()}).fail(function(errMsg){baseView.showLoadError("grade",errMsg)})},installHandlers:function(){var sel=$("#openassessment__grade",this.element);this.baseView.setUpCollapseExpand(sel);var view=this;sel.find("#feedback__submit").click(function(eventObject){eventObject.preventDefault();view.submitFeedbackOnAssessment()})},feedbackText:function(text){if(typeof text==="undefined"){return $("#feedback__remarks__value",this.element).val()}else{$("#feedback__remarks__value",this.element).val(text)}},feedbackOptions:function(options){var view=this;if(typeof options==="undefined"){return $.map($(".feedback__overall__value:checked",view.element),function(element,index){return $(element).val()})}else{$(".feedback__overall__value",this.element).prop("checked",false);$.each(options,function(index,opt){$("#feedback__overall__value--"+opt,view.element).prop("checked",true)})}},setHidden:function(sel,hidden){sel.toggleClass("is--hidden",hidden);sel.attr("aria-hidden",hidden?"true":"false")},isHidden:function(sel){return sel.hasClass("is--hidden")&&sel.attr("aria-hidden")=="true"},feedbackState:function(newState){var containerSel=$(".submission__feedback__content",this.element);var instructionsSel=containerSel.find(".submission__feedback__instructions");var fieldsSel=containerSel.find(".submission__feedback__fields");var actionsSel=containerSel.find(".submission__feedback__actions");var transitionSel=containerSel.find(".transition__status");var messageSel=containerSel.find(".message--complete");if(typeof newState==="undefined"){var isSubmitting=containerSel.hasClass("is--transitioning")&&containerSel.hasClass("is--submitting")&&!this.isHidden(transitionSel)&&this.isHidden(messageSel)&&this.isHidden(instructionsSel)&&this.isHidden(fieldsSel)&&this.isHidden(actionsSel);var hasSubmitted=containerSel.hasClass("is--submitted")&&this.isHidden(transitionSel)&&!this.isHidden(messageSel)&&this.isHidden(instructionsSel)&&this.isHidden(fieldsSel)&&this.isHidden(actionsSel);var isOpen=!containerSel.hasClass("is--submitted")&&!containerSel.hasClass("is--transitioning")&&!containerSel.hasClass("is--submitting")&&this.isHidden(transitionSel)&&this.isHidden(messageSel)&&!this.isHidden(instructionsSel)&&!this.isHidden(fieldsSel)&&!this.isHidden(actionsSel);if(isOpen){return"open"}else if(isSubmitting){return"submitting"}else if(hasSubmitted){return"submitted"}else{throw"Invalid feedback state"}}else{if(newState=="open"){containerSel.toggleClass("is--transitioning",false);containerSel.toggleClass("is--submitting",false);containerSel.toggleClass("is--submitted",false);this.setHidden(instructionsSel,false);this.setHidden(fieldsSel,false);this.setHidden(actionsSel,false);this.setHidden(transitionSel,true);this.setHidden(messageSel,true)}else if(newState=="submitting"){containerSel.toggleClass("is--transitioning",true);containerSel.toggleClass("is--submitting",true);containerSel.toggleClass("is--submitted",false);this.setHidden(instructionsSel,true);this.setHidden(fieldsSel,true);this.setHidden(actionsSel,true);this.setHidden(transitionSel,false);this.setHidden(messageSel,true)}else if(newState=="submitted"){containerSel.toggleClass("is--transitioning",false);containerSel.toggleClass("is--submitting",false);containerSel.toggleClass("is--submitted",true);this.setHidden(instructionsSel,true);this.setHidden(fieldsSel,true);this.setHidden(actionsSel,true);this.setHidden(transitionSel,true);this.setHidden(messageSel,false)}}},submitFeedbackOnAssessment:function(){var view=this;var baseView=this.baseView;$("#feedback__submit",this.element).toggleClass("is--disabled",true);view.feedbackState("submitting");this.server.submitFeedbackOnAssessment(this.feedbackText(),this.feedbackOptions()).done(function(){view.feedbackState("submitted")}).fail(function(errMsg){baseView.toggleActionError("feedback_assess",errMsg)})}};OpenAssessment.MessageView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.MessageView.prototype={load:function(){var view=this;var baseView=this.baseView;this.server.render("message").done(function(html){$("#openassessment__message",view.element).replaceWith(html)}).fail(function(errMsg){baseView.showLoadError("message",errMsg)})}};OpenAssessment.PeerView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.PeerView.prototype={load:function(){var view=this;this.server.render("peer_assessment").done(function(html){$("#openassessment__peer-assessment",view.element).replaceWith(html);view.installHandlers(false)}).fail(function(errMsg){view.baseView.showLoadError("peer-assessment")});view.baseView.loadMessageView()},loadContinuedAssessment:function(){var view=this;view.continueAssessmentEnabled(false);this.server.renderContinuedPeer().done(function(html){$("#openassessment__peer-assessment",view.element).replaceWith(html);view.installHandlers(true)}).fail(function(errMsg){view.baseView.showLoadError("peer-assessment");view.continueAssessmentEnabled(true)})},continueAssessmentEnabled:function(enabled){var button=$("#peer-assessment__continue__grading",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},installHandlers:function(isContinuedAssessment){var sel=$("#openassessment__peer-assessment",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#peer-assessment--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(view.peerSubmitEnabled,view))}sel.find("#peer-assessment--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();if(!isContinuedAssessment){view.peerAssess()}else{view.continuedPeerAssess()}});sel.find("#peer-assessment__continue__grading").click(function(eventObject){eventObject.preventDefault();view.loadContinuedAssessment()})},peerSubmitEnabled:function(enabled){var button=$("#peer-assessment--001__assessment__submit",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},peerAssess:function(){var view=this;var baseView=view.baseView;this.peerAssessRequest(function(){view.load();baseView.loadAssessmentModules();baseView.scrollToTop()})},continuedPeerAssess:function(){var view=this;var gradeView=this.baseView.gradeView;var baseView=view.baseView;view.peerAssessRequest(function(){view.loadContinuedAssessment();gradeView.load();baseView.scrollToTop()})},peerAssessRequest:function(successFunction){var view=this;view.baseView.toggleActionError("peer",null);view.peerSubmitEnabled(false);this.server.peerAssess(this.rubric.optionsSelected(),this.rubric.criterionFeedback(),this.overallFeedback()).done(successFunction).fail(function(errMsg){view.baseView.toggleActionError("peer",errMsg);view.peerSubmitEnabled(true)})},overallFeedback:function(overallFeedback){var selector="#assessment__rubric__question--feedback__value";if(typeof overallFeedback==="undefined"){return $(selector,this.element).val()}else{$(selector,this.element).val(overallFeedback)}}};OpenAssessment.ResponseView=function(element,server,fileUploader,baseView){this.element=element;this.server=server;this.fileUploader=fileUploader;this.baseView=baseView;this.savedResponse="";this.files=null;this.imageType=null;this.lastChangeTime=Date.now();this.errorOnLastSave=false;this.autoSaveTimerId=null};OpenAssessment.ResponseView.prototype={AUTO_SAVE_POLL_INTERVAL:2e3,AUTO_SAVE_WAIT:3e4,MAX_FILE_SIZE:5242880,load:function(){var view=this;this.server.render("submission").done(function(html){$("#openassessment__response",view.element).replaceWith(html);view.installHandlers();view.setAutoSaveEnabled(true)}).fail(function(errMsg){view.baseView.showLoadError("response")})},installHandlers:function(){var sel=$("#openassessment__response",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);this.savedResponse=this.response();var handleChange=function(eventData){view.handleResponseChanged()};sel.find("#submission__answer__value").on("change keyup drop paste",handleChange);var handlePrepareUpload=function(eventData){view.prepareUpload(eventData.target.files)};sel.find("input[type=file]").on("change",handlePrepareUpload);sel.find("#step--response__submit").click(function(eventObject){eventObject.preventDefault();view.submit()});sel.find("#submission__save").click(function(eventObject){eventObject.preventDefault();view.save()});sel.find("#file__upload").click(function(eventObject){eventObject.preventDefault();$(".submission__answer__display__image",view.element).removeClass("is--hidden");view.fileUpload()})},setAutoSaveEnabled:function(enabled){if(enabled){if(this.autoSaveTimerId===null){this.autoSaveTimerId=setInterval($.proxy(this.autoSave,this),this.AUTO_SAVE_POLL_INTERVAL)}}else{if(this.autoSaveTimerId!==null){clearInterval(this.autoSaveTimerId)}}},submitEnabled:function(enabled){var sel=$("#step--response__submit",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},saveEnabled:function(enabled){var sel=$("#submission__save",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},saveStatus:function(msg){var sel=$("#response__save_status h3",this.element);if(typeof msg==="undefined"){return sel.text()}else{var label=gettext("Status of Your Response");sel.html('<span class="sr">'+label+":"+"</span>\n"+msg)}},unsavedWarningEnabled:function(enabled){if(typeof enabled==="undefined"){return window.onbeforeunload!==null}else{if(enabled){window.onbeforeunload=function(){return gettext("If you leave this page without saving or submitting your response, you'll lose any work you've done on the response.")}}else{window.onbeforeunload=null}}},response:function(text){var sel=$("#submission__answer__value",this.element);if(typeof text==="undefined"){return sel.val()}else{sel.val(text)}},responseChanged:function(){var currentResponse=$.trim(this.response());var savedResponse=$.trim(this.savedResponse);return savedResponse!==currentResponse},autoSave:function(){var timeSinceLastChange=Date.now()-this.lastChangeTime;if(this.responseChanged()&&timeSinceLastChange>this.AUTO_SAVE_WAIT&&!this.errorOnLastSave){this.save()}},handleResponseChanged:function(){var isBlank=$.trim(this.response())!=="";this.submitEnabled(isBlank);if(this.responseChanged()){this.saveEnabled(isBlank);this.saveStatus(gettext("This response has not been saved."));this.unsavedWarningEnabled(true)}this.lastChangeTime=Date.now()},save:function(){this.errorOnLastSave=false;this.saveStatus(gettext("Saving..."));this.baseView.toggleActionError("save",null);this.unsavedWarningEnabled(false);var view=this;var savedResponse=this.response();this.server.save(savedResponse).done(function(){view.savedResponse=savedResponse;var currentResponse=view.response();view.submitEnabled(currentResponse!=="");if(currentResponse==savedResponse){view.saveEnabled(false);view.saveStatus(gettext("This response has been saved but not submitted."))}}).fail(function(errMsg){view.saveStatus(gettext("Error"));view.baseView.toggleActionError("save",errMsg);view.errorOnLastSave=true})},submit:function(){this.submitEnabled(false);var view=this;var baseView=this.baseView;this.confirmSubmission().pipe(function(){var submission=$("#submission__answer__value",view.element).val();baseView.toggleActionError("response",null);return view.server.submit(submission)}).done($.proxy(view.moveToNextStep,view)).fail(function(errCode,errMsg){if(errCode=="ENOMULTI"){view.moveToNextStep()}else{if(errMsg){baseView.toggleActionError("submit",errMsg)}view.submitEnabled(true)}})},moveToNextStep:function(){this.load();this.baseView.loadAssessmentModules();this.unsavedWarningEnabled(false)},confirmSubmission:function(){var msg="You're about to submit your response for this assignment. "+"After you submit this response, you can't change it or submit a new response.";return $.Deferred(function(defer){if(confirm(msg)){defer.resolve()}else{defer.reject()}})},prepareUpload:function(files){this.files=null;this.imageType=files[0].type;if(files[0].size>this.MAX_FILE_SIZE){this.baseView.toggleActionError("upload",gettext("File size must be 5MB or less."))}else if(this.imageType.substring(0,6)!="image/"){this.baseView.toggleActionError("upload",gettext("File must be an image."))}else{this.baseView.toggleActionError("upload",null);this.files=files}$("#file__upload").toggleClass("is--disabled",this.files===null)},fileUpload:function(){var view=this;var fileUpload=$("#file__upload");fileUpload.addClass("is--disabled");var handleError=function(errMsg){view.baseView.toggleActionError("upload",errMsg);fileUpload.removeClass("is--disabled")};this.server.getUploadUrl(view.imageType).done(function(url){var image=view.files[0];view.fileUploader.upload(url,image,view.imageType).done(function(){view.imageUrl();view.baseView.toggleActionError("upload",null)}).fail(handleError)}).fail(handleError)},imageUrl:function(){var view=this;var image=$("#submission__answer__image",view.element);view.server.getDownloadUrl().done(function(url){image.attr("src",url);return url})}};OpenAssessment.Rubric=function(element){this.element=element};OpenAssessment.Rubric.prototype={criterionFeedback:function(criterionFeedback){var selector="textarea.answer__value";var feedback={};$(selector,this.element).each(function(index,sel){if(typeof criterionFeedback!=="undefined"){$(sel).val(criterionFeedback[sel.name]);feedback[sel.name]=criterionFeedback[sel.name]}else{feedback[sel.name]=$(sel).val()}});return feedback},optionsSelected:function(optionsSelected){var selector="input[type=radio]";if(typeof optionsSelected==="undefined"){var options={};$(selector+":checked",this.element).each(function(index,sel){options[sel.name]=sel.value});return options}else{$(selector,this.element).prop("checked",false);$(selector,this.element).each(function(index,sel){if(optionsSelected.hasOwnProperty(sel.name)){if(sel.value==optionsSelected[sel.name]){$(sel).prop("checked",true)}}})}},canSubmitCallback:function(callback){var rubric=this;callback(rubric.canSubmit());$(this.element).on("change keyup drop paste",function(){callback(rubric.canSubmit())})},canSubmit:function(){var numChecked=$("input[type=radio]:checked",this.element).length;var numAvailable=$(".field--radio.assessment__rubric__question.has--options",this.element).length;var completedRequiredComments=true;$("textarea[required]",this.element).each(function(){var trimmedText=$.trim($(this).val());if(trimmedText===""){completedRequiredComments=false}});return numChecked==numAvailable&&completedRequiredComments},showCorrections:function(corrections){var selector="input[type=radio]";var hasErrors=false;$(selector,this.element).each(function(index,sel){var listItem=$(sel).parents(".assessment__rubric__question");if(corrections.hasOwnProperty(sel.name)){hasErrors=true;listItem.find(".message--incorrect").removeClass("is--hidden");listItem.find(".message--correct").addClass("is--hidden")}else{listItem.find(".message--correct").removeClass("is--hidden");listItem.find(".message--incorrect").addClass("is--hidden")}});return hasErrors}};OpenAssessment.SelfView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.SelfView.prototype={load:function(){var view=this;this.server.render("self_assessment").done(function(html){$("#openassessment__self-assessment",view.element).replaceWith(html);view.installHandlers()}).fail(function(errMsg){view.showLoadError("self-assessment")})},installHandlers:function(){var view=this;var sel=$("#openassessment__self-assessment",view.element);this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#self-assessment--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(this.selfSubmitEnabled,this))}sel.find("#self-assessment--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();view.selfAssess()})},selfSubmitEnabled:function(enabled){var button=$("#self-assessment--001__assessment__submit",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},selfAssess:function(){var view=this;var baseView=this.baseView;baseView.toggleActionError("self",null);view.selfSubmitEnabled(false);var options=this.rubric.optionsSelected();this.server.selfAssess(options).done(function(){baseView.loadAssessmentModules();baseView.scrollToTop()}).fail(function(errMsg){baseView.toggleActionError("self",errMsg);view.selfSubmitEnabled(true)})}};OpenAssessment.Server=function(runtime,element){this.runtime=runtime;this.element=element};OpenAssessment.Server.prototype={url:function(handler){return this.runtime.handlerUrl(this.element,handler)},render:function(component){var url=this.url("render_"+component);return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html"}).done(function(data){defer.resolveWith(this,[data])}).fail(function(data){defer.rejectWith(this,[gettext("This section could not be loaded.")])})}).promise()},renderContinuedPeer:function(){var url=this.url("render_peer_assessment");return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html",data:{continue_grading:true}}).done(function(data){defer.resolveWith(this,[data])}).fail(function(data){defer.rejectWith(this,[gettext("This section could not be loaded.")])})}).promise()},studentInfo:function(student_id){var url=this.url("render_student_info");return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html",data:{student_id:student_id}}).done(function(data){defer.resolveWith(this,[data])}).fail(function(data){defer.rejectWith(this,[gettext("This section could not be loaded.")])})}).promise()},submit:function(submission){var url=this.url("submit");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({submission:submission})}).done(function(data){var success=data[0];if(success){var studentId=data[1];var attemptNum=data[2];defer.resolveWith(this,[studentId,attemptNum])}else{var errorNum=data[1];var errorMsg=data[2];defer.rejectWith(this,[errorNum,errorMsg])}}).fail(function(data){defer.rejectWith(this,["AJAX",gettext("This response could not be submitted.")])})}).promise()},save:function(submission){var url=this.url("save_submission");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({submission:submission})}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This response could not be saved.")])})}).promise()},submitFeedbackOnAssessment:function(text,options){var url=this.url("submit_feedback");var payload=JSON.stringify({feedback_text:text,feedback_options:options});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This feedback could not be submitted.")])})}).promise()},peerAssess:function(optionsSelected,criterionFeedback,overallFeedback){var url=this.url("peer_assess");var payload=JSON.stringify({options_selected:optionsSelected,criterion_feedback:criterionFeedback,overall_feedback:overallFeedback});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})}).promise()},selfAssess:function(optionsSelected){var url=this.url("self_assess");var payload=JSON.stringify({options_selected:optionsSelected});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},trainingAssess:function(optionsSelected){var url=this.url("training_assess");var payload=JSON.stringify({options_selected:optionsSelected});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolveWith(this,[data.corrections])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},scheduleTraining:function(){var url=this.url("schedule_training");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""'}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},rescheduleUnfinishedTasks:function(){var url=this.url("reschedule_unfinished_tasks");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""'}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("One or more rescheduling tasks failed.")])})})},loadXml:function(){var url=this.url("xml");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""'}).done(function(data){if(data.success){defer.resolveWith(this,[data.xml])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This problem could not be loaded.")])})}).promise()},updateXml:function(xml){var url=this.url("update_xml");var payload=JSON.stringify({xml:xml});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This problem could not be saved.")])})}).promise()},checkReleased:function(){var url=this.url("check_released");var payload='""';return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolveWith(this,[data.is_released])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("The server could not be contacted.")])})}).promise()},getUploadUrl:function(contentType){var url=this.url("upload_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({contentType:contentType})}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("Could not retrieve upload url.")])})}).promise()},getDownloadUrl:function(){var url=this.url("download_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({})}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("Could not retrieve download url.")])})}).promise()}};if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}OpenAssessment.StaffInfoView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.StaffInfoView.prototype={load:function(){var view=this;if($("#openassessment__staff-info",view.element).length>0){this.server.render("staff_info").done(function(html){$("#openassessment__staff-info",view.element).replaceWith(html);view.installHandlers()}).fail(function(errMsg){view.baseView.showLoadError("staff_info")})}},loadStudentInfo:function(){var view=this;var sel=$("#openassessment__staff-info",this.element);var student_id=sel.find("#openassessment__student_id").val();this.server.studentInfo(student_id).done(function(html){$("#openassessment__student-info",view.element).replaceWith(html)}).fail(function(errMsg){view.showLoadError("student_info")})},installHandlers:function(){var sel=$("#openassessment__staff-info",this.element);var view=this;if(sel.length<=0){return}this.baseView.setUpCollapseExpand(sel,function(){});sel.find("#openassessment_student_info_form").submit(function(eventObject){eventObject.preventDefault();view.loadStudentInfo()});sel.find("#submit_student_id").click(function(eventObject){eventObject.preventDefault();view.loadStudentInfo()});sel.find("#schedule_training").click(function(eventObject){eventObject.preventDefault();view.scheduleTraining()});sel.find("#reschedule_unfinished_tasks").click(function(eventObject){eventObject.preventDefault();view.rescheduleUnfinishedTasks()})},scheduleTraining:function(){var view=this;this.server.scheduleTraining().done(function(msg){$("#schedule_training_message",this.element).text(msg)}).fail(function(errMsg){$("#schedule_training_message",this.element).text(errMsg)})},rescheduleUnfinishedTasks:function(){var view=this;this.server.rescheduleUnfinishedTasks().done(function(msg){$("#reschedule_unfinished_tasks_message",this.element).text(msg)}).fail(function(errMsg){$("#reschedule_unfinished_tasks_message",this.element).text(errMsg)})}};OpenAssessment.StudentTrainingView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.StudentTrainingView.prototype={load:function(){var view=this;this.server.render("student_training").done(function(html){$("#openassessment__student-training",view.element).replaceWith(html);view.installHandlers()}).fail(function(errMsg){view.baseView.showLoadError("student-training")})},installHandlers:function(){var sel=$("#openassessment__student-training",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#student-training--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(this.assessButtonEnabled,this))}sel.find("#student-training--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();view.assess()})},assess:function(){this.assessButtonEnabled(false);var options={};if(this.rubric!==null){options=this.rubric.optionsSelected()}var view=this;var baseView=this.baseView;this.server.trainingAssess(options).done(function(corrections){var incorrect=$("#openassessment__student-training--incorrect",this.element);var instructions=$("#openassessment__student-training--instructions",this.element);if(!view.rubric.showCorrections(corrections)){view.load();baseView.loadAssessmentModules();incorrect.addClass("is--hidden");instructions.removeClass("is--hidden")}else{instructions.addClass("is--hidden"); if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}OpenAssessment.BaseView=function(runtime,element,server){this.runtime=runtime;this.element=element;this.server=server;this.fileUploader=new OpenAssessment.FileUploader;this.responseView=new OpenAssessment.ResponseView(this.element,this.server,this.fileUploader,this);this.trainingView=new OpenAssessment.StudentTrainingView(this.element,this.server,this);this.selfView=new OpenAssessment.SelfView(this.element,this.server,this);this.peerView=new OpenAssessment.PeerView(this.element,this.server,this);this.gradeView=new OpenAssessment.GradeView(this.element,this.server,this);this.messageView=new OpenAssessment.MessageView(this.element,this.server,this);this.staffInfoView=new OpenAssessment.StaffInfoView(this.element,this.server,this)};OpenAssessment.BaseView.prototype={scrollToTop:function(){if($.scrollTo instanceof Function){$(window).scrollTo($("#openassessment__steps"),800,{offset:-50})}},setUpCollapseExpand:function(parentSel){parentSel.find(".ui-toggle-visibility__control").click(function(eventData){var sel=$(eventData.target).closest(".ui-toggle-visibility");sel.toggleClass("is--collapsed")})},load:function(){this.responseView.load();this.loadAssessmentModules();this.staffInfoView.load()},loadAssessmentModules:function(){this.trainingView.load();this.peerView.load();this.selfView.load();this.gradeView.load()},loadMessageView:function(){this.messageView.load()},toggleActionError:function(type,msg){var element=this.element;var container=null;if(type=="save"){container=".response__submission__actions"}else if(type=="submit"||type=="peer"||type=="self"||type=="student-training"){container=".step__actions"}else if(type=="feedback_assess"){container=".submission__feedback__actions"}else if(type=="upload"){container="#upload__error"}if(container===null){if(msg!==null){console.log(msg)}}else{var msgHtml=msg===null?"":msg;$(container+" .message__content",element).html("<p>"+msgHtml+"</p>");$(container,element).toggleClass("has--error",msg!==null)}},showLoadError:function(step){var container="#openassessment__"+step;$(container).toggleClass("has--error",true);$(container+" .step__status__value i").removeClass().addClass("ico icon-warning-sign");$(container+" .step__status__value .copy").html(gettext("Unable to Load"))}};function OpenAssessmentBlock(runtime,element){var server=new OpenAssessment.Server(runtime,element);var view=new OpenAssessment.BaseView(runtime,element,server);view.load()}OpenAssessment.StudioView=function(runtime,element,server){this.runtime=runtime;this.server=server;this.codeBox=CodeMirror.fromTextArea($(element).find(".openassessment-editor").first().get(0),{mode:"xml",lineNumbers:true,lineWrapping:true});var view=this;$(element).find(".openassessment-save-button").click(function(eventData){view.save()});$(element).find(".openassessment-cancel-button").click(function(eventData){view.cancel()})};OpenAssessment.StudioView.prototype={load:function(){var view=this;this.server.loadXml().done(function(xml){view.codeBox.setValue(xml)}).fail(function(msg){view.showError(msg)})},save:function(){var view=this;this.server.checkReleased().done(function(isReleased){if(isReleased){view.confirmPostReleaseUpdate($.proxy(view.updateXml,view))}else{view.updateXml()}}).fail(function(errMsg){view.showError(msg)})},confirmPostReleaseUpdate:function(onConfirm){var msg=gettext("This problem has already been released. Any changes will apply only to future assessments.");if(confirm(msg)){onConfirm()}},updateXml:function(){this.runtime.notify("save",{state:"start"});var xml=this.codeBox.getValue();var view=this;this.server.updateXml(xml).done(function(){view.runtime.notify("save",{state:"end"});view.load()}).fail(function(msg){view.showError(msg)})},cancel:function(){this.runtime.notify("cancel",{})},showError:function(errorMsg){this.runtime.notify("error",{msg:errorMsg})}};function OpenAssessmentEditor(runtime,element){var server=new OpenAssessment.Server(runtime,element);var view=new OpenAssessment.StudioView(runtime,element,server);view.load()}OpenAssessment.FileUploader=function(){this.upload=function(url,data,contentType){return $.Deferred(function(defer){$.ajax({url:url,type:"PUT",data:data,async:false,processData:false,contentType:contentType}).done(function(data,textStatus,jqXHR){defer.resolve()}).fail(function(data,textStatus,jqXHR){defer.rejectWith(this,[textStatus])})}).promise()}};OpenAssessment.GradeView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.GradeView.prototype={load:function(){var view=this;var baseView=this.baseView;this.server.render("grade").done(function(html){$("#openassessment__grade",view.element).replaceWith(html);view.installHandlers()}).fail(function(errMsg){baseView.showLoadError("grade",errMsg)})},installHandlers:function(){var sel=$("#openassessment__grade",this.element);this.baseView.setUpCollapseExpand(sel);var view=this;sel.find("#feedback__submit").click(function(eventObject){eventObject.preventDefault();view.submitFeedbackOnAssessment()})},feedbackText:function(text){if(typeof text==="undefined"){return $("#feedback__remarks__value",this.element).val()}else{$("#feedback__remarks__value",this.element).val(text)}},feedbackOptions:function(options){var view=this;if(typeof options==="undefined"){return $.map($(".feedback__overall__value:checked",view.element),function(element,index){return $(element).val()})}else{$(".feedback__overall__value",this.element).prop("checked",false);$.each(options,function(index,opt){$("#feedback__overall__value--"+opt,view.element).prop("checked",true)})}},setHidden:function(sel,hidden){sel.toggleClass("is--hidden",hidden);sel.attr("aria-hidden",hidden?"true":"false")},isHidden:function(sel){return sel.hasClass("is--hidden")&&sel.attr("aria-hidden")=="true"},feedbackState:function(newState){var containerSel=$(".submission__feedback__content",this.element);var instructionsSel=containerSel.find(".submission__feedback__instructions");var fieldsSel=containerSel.find(".submission__feedback__fields");var actionsSel=containerSel.find(".submission__feedback__actions");var transitionSel=containerSel.find(".transition__status");var messageSel=containerSel.find(".message--complete");if(typeof newState==="undefined"){var isSubmitting=containerSel.hasClass("is--transitioning")&&containerSel.hasClass("is--submitting")&&!this.isHidden(transitionSel)&&this.isHidden(messageSel)&&this.isHidden(instructionsSel)&&this.isHidden(fieldsSel)&&this.isHidden(actionsSel);var hasSubmitted=containerSel.hasClass("is--submitted")&&this.isHidden(transitionSel)&&!this.isHidden(messageSel)&&this.isHidden(instructionsSel)&&this.isHidden(fieldsSel)&&this.isHidden(actionsSel);var isOpen=!containerSel.hasClass("is--submitted")&&!containerSel.hasClass("is--transitioning")&&!containerSel.hasClass("is--submitting")&&this.isHidden(transitionSel)&&this.isHidden(messageSel)&&!this.isHidden(instructionsSel)&&!this.isHidden(fieldsSel)&&!this.isHidden(actionsSel);if(isOpen){return"open"}else if(isSubmitting){return"submitting"}else if(hasSubmitted){return"submitted"}else{throw"Invalid feedback state"}}else{if(newState=="open"){containerSel.toggleClass("is--transitioning",false);containerSel.toggleClass("is--submitting",false);containerSel.toggleClass("is--submitted",false);this.setHidden(instructionsSel,false);this.setHidden(fieldsSel,false);this.setHidden(actionsSel,false);this.setHidden(transitionSel,true);this.setHidden(messageSel,true)}else if(newState=="submitting"){containerSel.toggleClass("is--transitioning",true);containerSel.toggleClass("is--submitting",true);containerSel.toggleClass("is--submitted",false);this.setHidden(instructionsSel,true);this.setHidden(fieldsSel,true);this.setHidden(actionsSel,true);this.setHidden(transitionSel,false);this.setHidden(messageSel,true)}else if(newState=="submitted"){containerSel.toggleClass("is--transitioning",false);containerSel.toggleClass("is--submitting",false);containerSel.toggleClass("is--submitted",true);this.setHidden(instructionsSel,true);this.setHidden(fieldsSel,true);this.setHidden(actionsSel,true);this.setHidden(transitionSel,true);this.setHidden(messageSel,false)}}},submitFeedbackOnAssessment:function(){var view=this;var baseView=this.baseView;$("#feedback__submit",this.element).toggleClass("is--disabled",true);view.feedbackState("submitting");this.server.submitFeedbackOnAssessment(this.feedbackText(),this.feedbackOptions()).done(function(){view.feedbackState("submitted")}).fail(function(errMsg){baseView.toggleActionError("feedback_assess",errMsg)})}};OpenAssessment.MessageView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.MessageView.prototype={load:function(){var view=this;var baseView=this.baseView;this.server.render("message").done(function(html){$("#openassessment__message",view.element).replaceWith(html)}).fail(function(errMsg){baseView.showLoadError("message",errMsg)})}};OpenAssessment.PeerView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.PeerView.prototype={load:function(){var view=this;this.server.render("peer_assessment").done(function(html){$("#openassessment__peer-assessment",view.element).replaceWith(html);view.installHandlers(false)}).fail(function(errMsg){view.baseView.showLoadError("peer-assessment")});view.baseView.loadMessageView()},loadContinuedAssessment:function(){var view=this;view.continueAssessmentEnabled(false);this.server.renderContinuedPeer().done(function(html){$("#openassessment__peer-assessment",view.element).replaceWith(html);view.installHandlers(true)}).fail(function(errMsg){view.baseView.showLoadError("peer-assessment");view.continueAssessmentEnabled(true)})},continueAssessmentEnabled:function(enabled){var button=$("#peer-assessment__continue__grading",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},installHandlers:function(isContinuedAssessment){var sel=$("#openassessment__peer-assessment",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#peer-assessment--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(view.peerSubmitEnabled,view))}sel.find("#peer-assessment--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();if(!isContinuedAssessment){view.peerAssess()}else{view.continuedPeerAssess()}});sel.find("#peer-assessment__continue__grading").click(function(eventObject){eventObject.preventDefault();view.loadContinuedAssessment()})},peerSubmitEnabled:function(enabled){var button=$("#peer-assessment--001__assessment__submit",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},peerAssess:function(){var view=this;var baseView=view.baseView;this.peerAssessRequest(function(){view.load();baseView.loadAssessmentModules();baseView.scrollToTop()})},continuedPeerAssess:function(){var view=this;var gradeView=this.baseView.gradeView;var baseView=view.baseView;view.peerAssessRequest(function(){view.loadContinuedAssessment();gradeView.load();baseView.scrollToTop()})},peerAssessRequest:function(successFunction){var view=this;view.baseView.toggleActionError("peer",null);view.peerSubmitEnabled(false);this.server.peerAssess(this.rubric.optionsSelected(),this.rubric.criterionFeedback(),this.rubric.overallFeedback()).done(successFunction).fail(function(errMsg){view.baseView.toggleActionError("peer",errMsg);view.peerSubmitEnabled(true)})}};OpenAssessment.ResponseView=function(element,server,fileUploader,baseView){this.element=element;this.server=server;this.fileUploader=fileUploader;this.baseView=baseView;this.savedResponse="";this.files=null;this.imageType=null;this.lastChangeTime=Date.now();this.errorOnLastSave=false;this.autoSaveTimerId=null};OpenAssessment.ResponseView.prototype={AUTO_SAVE_POLL_INTERVAL:2e3,AUTO_SAVE_WAIT:3e4,MAX_FILE_SIZE:5242880,load:function(){var view=this;this.server.render("submission").done(function(html){$("#openassessment__response",view.element).replaceWith(html);view.installHandlers();view.setAutoSaveEnabled(true)}).fail(function(errMsg){view.baseView.showLoadError("response")})},installHandlers:function(){var sel=$("#openassessment__response",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);this.savedResponse=this.response();var handleChange=function(eventData){view.handleResponseChanged()};sel.find("#submission__answer__value").on("change keyup drop paste",handleChange);var handlePrepareUpload=function(eventData){view.prepareUpload(eventData.target.files)};sel.find("input[type=file]").on("change",handlePrepareUpload);sel.find("#step--response__submit").click(function(eventObject){eventObject.preventDefault();view.submit()});sel.find("#submission__save").click(function(eventObject){eventObject.preventDefault();view.save()});sel.find("#file__upload").click(function(eventObject){eventObject.preventDefault();$(".submission__answer__display__image",view.element).removeClass("is--hidden");view.fileUpload()})},setAutoSaveEnabled:function(enabled){if(enabled){if(this.autoSaveTimerId===null){this.autoSaveTimerId=setInterval($.proxy(this.autoSave,this),this.AUTO_SAVE_POLL_INTERVAL)}}else{if(this.autoSaveTimerId!==null){clearInterval(this.autoSaveTimerId)}}},submitEnabled:function(enabled){var sel=$("#step--response__submit",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},saveEnabled:function(enabled){var sel=$("#submission__save",this.element);if(typeof enabled==="undefined"){return!sel.hasClass("is--disabled")}else{sel.toggleClass("is--disabled",!enabled)}},saveStatus:function(msg){var sel=$("#response__save_status h3",this.element);if(typeof msg==="undefined"){return sel.text()}else{var label=gettext("Status of Your Response");sel.html('<span class="sr">'+label+":"+"</span>\n"+msg)}},unsavedWarningEnabled:function(enabled){if(typeof enabled==="undefined"){return window.onbeforeunload!==null}else{if(enabled){window.onbeforeunload=function(){return gettext("If you leave this page without saving or submitting your response, you'll lose any work you've done on the response.")}}else{window.onbeforeunload=null}}},response:function(text){var sel=$("#submission__answer__value",this.element);if(typeof text==="undefined"){return sel.val()}else{sel.val(text)}},responseChanged:function(){var currentResponse=$.trim(this.response());var savedResponse=$.trim(this.savedResponse);return savedResponse!==currentResponse},autoSave:function(){var timeSinceLastChange=Date.now()-this.lastChangeTime;if(this.responseChanged()&&timeSinceLastChange>this.AUTO_SAVE_WAIT&&!this.errorOnLastSave){this.save()}},handleResponseChanged:function(){var isBlank=$.trim(this.response())!=="";this.submitEnabled(isBlank);if(this.responseChanged()){this.saveEnabled(isBlank);this.saveStatus(gettext("This response has not been saved."));this.unsavedWarningEnabled(true)}this.lastChangeTime=Date.now()},save:function(){this.errorOnLastSave=false;this.saveStatus(gettext("Saving..."));this.baseView.toggleActionError("save",null);this.unsavedWarningEnabled(false);var view=this;var savedResponse=this.response();this.server.save(savedResponse).done(function(){view.savedResponse=savedResponse;var currentResponse=view.response();view.submitEnabled(currentResponse!=="");if(currentResponse==savedResponse){view.saveEnabled(false);view.saveStatus(gettext("This response has been saved but not submitted."))}}).fail(function(errMsg){view.saveStatus(gettext("Error"));view.baseView.toggleActionError("save",errMsg);view.errorOnLastSave=true})},submit:function(){this.submitEnabled(false);var view=this;var baseView=this.baseView;this.confirmSubmission().pipe(function(){var submission=$("#submission__answer__value",view.element).val();baseView.toggleActionError("response",null);return view.server.submit(submission)}).done($.proxy(view.moveToNextStep,view)).fail(function(errCode,errMsg){if(errCode=="ENOMULTI"){view.moveToNextStep()}else{if(errMsg){baseView.toggleActionError("submit",errMsg)}view.submitEnabled(true)}})},moveToNextStep:function(){this.load();this.baseView.loadAssessmentModules();this.unsavedWarningEnabled(false)},confirmSubmission:function(){var msg="You're about to submit your response for this assignment. "+"After you submit this response, you can't change it or submit a new response.";return $.Deferred(function(defer){if(confirm(msg)){defer.resolve()}else{defer.reject()}})},prepareUpload:function(files){this.files=null;this.imageType=files[0].type;if(files[0].size>this.MAX_FILE_SIZE){this.baseView.toggleActionError("upload",gettext("File size must be 5MB or less."))}else if(this.imageType.substring(0,6)!="image/"){this.baseView.toggleActionError("upload",gettext("File must be an image."))}else{this.baseView.toggleActionError("upload",null);this.files=files}$("#file__upload").toggleClass("is--disabled",this.files===null)},fileUpload:function(){var view=this;var fileUpload=$("#file__upload");fileUpload.addClass("is--disabled");var handleError=function(errMsg){view.baseView.toggleActionError("upload",errMsg);fileUpload.removeClass("is--disabled")};this.server.getUploadUrl(view.imageType).done(function(url){var image=view.files[0];view.fileUploader.upload(url,image,view.imageType).done(function(){view.imageUrl();view.baseView.toggleActionError("upload",null)}).fail(handleError)}).fail(handleError)},imageUrl:function(){var view=this;var image=$("#submission__answer__image",view.element);view.server.getDownloadUrl().done(function(url){image.attr("src",url);return url})}};OpenAssessment.Rubric=function(element){this.element=element};OpenAssessment.Rubric.prototype={criterionFeedback:function(criterionFeedback){var selector="textarea.answer__value";var feedback={};$(selector,this.element).each(function(index,sel){if(typeof criterionFeedback!=="undefined"){$(sel).val(criterionFeedback[sel.name]);feedback[sel.name]=criterionFeedback[sel.name]}else{feedback[sel.name]=$(sel).val()}});return feedback},overallFeedback:function(overallFeedback){var selector="#assessment__rubric__question--feedback__value";if(typeof overallFeedback==="undefined"){return $(selector,this.element).val()}else{$(selector,this.element).val(overallFeedback)}},optionsSelected:function(optionsSelected){var selector="input[type=radio]";if(typeof optionsSelected==="undefined"){var options={};$(selector+":checked",this.element).each(function(index,sel){options[sel.name]=sel.value});return options}else{$(selector,this.element).prop("checked",false);$(selector,this.element).each(function(index,sel){if(optionsSelected.hasOwnProperty(sel.name)){if(sel.value==optionsSelected[sel.name]){$(sel).prop("checked",true)}}})}},canSubmitCallback:function(callback){var rubric=this;callback(rubric.canSubmit());$(this.element).on("change keyup drop paste",function(){callback(rubric.canSubmit())})},canSubmit:function(){var numChecked=$("input[type=radio]:checked",this.element).length;var numAvailable=$(".field--radio.assessment__rubric__question.has--options",this.element).length;var completedRequiredComments=true;$("textarea[required]",this.element).each(function(){var trimmedText=$.trim($(this).val());if(trimmedText===""){completedRequiredComments=false}});return numChecked==numAvailable&&completedRequiredComments},showCorrections:function(corrections){var selector="input[type=radio]";var hasErrors=false;$(selector,this.element).each(function(index,sel){var listItem=$(sel).parents(".assessment__rubric__question");if(corrections.hasOwnProperty(sel.name)){hasErrors=true;listItem.find(".message--incorrect").removeClass("is--hidden");listItem.find(".message--correct").addClass("is--hidden")}else{listItem.find(".message--correct").removeClass("is--hidden");listItem.find(".message--incorrect").addClass("is--hidden")}});return hasErrors}};OpenAssessment.SelfView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.SelfView.prototype={load:function(){var view=this;this.server.render("self_assessment").done(function(html){$("#openassessment__self-assessment",view.element).replaceWith(html);view.installHandlers()}).fail(function(errMsg){view.showLoadError("self-assessment")})},installHandlers:function(){var view=this;var sel=$("#openassessment__self-assessment",view.element);this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#self-assessment--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(this.selfSubmitEnabled,this))}sel.find("#self-assessment--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();view.selfAssess()})},selfSubmitEnabled:function(enabled){var button=$("#self-assessment--001__assessment__submit",this.element);if(typeof enabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!enabled)}},selfAssess:function(){var view=this;var baseView=this.baseView;baseView.toggleActionError("self",null);view.selfSubmitEnabled(false);this.server.selfAssess(this.rubric.optionsSelected(),this.rubric.criterionFeedback(),this.rubric.overallFeedback()).done(function(){baseView.loadAssessmentModules();baseView.scrollToTop()}).fail(function(errMsg){baseView.toggleActionError("self",errMsg);view.selfSubmitEnabled(true)})}};OpenAssessment.Server=function(runtime,element){this.runtime=runtime;this.element=element};OpenAssessment.Server.prototype={url:function(handler){return this.runtime.handlerUrl(this.element,handler)},render:function(component){var url=this.url("render_"+component);return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html"}).done(function(data){defer.resolveWith(this,[data])}).fail(function(data){defer.rejectWith(this,[gettext("This section could not be loaded.")])})}).promise()},renderContinuedPeer:function(){var url=this.url("render_peer_assessment");return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html",data:{continue_grading:true}}).done(function(data){defer.resolveWith(this,[data])}).fail(function(data){defer.rejectWith(this,[gettext("This section could not be loaded.")])})}).promise()},studentInfo:function(student_id){var url=this.url("render_student_info");return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html",data:{student_id:student_id}}).done(function(data){defer.resolveWith(this,[data])}).fail(function(data){defer.rejectWith(this,[gettext("This section could not be loaded.")])})}).promise()},submit:function(submission){var url=this.url("submit");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({submission:submission})}).done(function(data){var success=data[0];if(success){var studentId=data[1];var attemptNum=data[2];defer.resolveWith(this,[studentId,attemptNum])}else{var errorNum=data[1];var errorMsg=data[2];defer.rejectWith(this,[errorNum,errorMsg])}}).fail(function(data){defer.rejectWith(this,["AJAX",gettext("This response could not be submitted.")])})}).promise()},save:function(submission){var url=this.url("save_submission");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({submission:submission})}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This response could not be saved.")])})}).promise()},submitFeedbackOnAssessment:function(text,options){var url=this.url("submit_feedback");var payload=JSON.stringify({feedback_text:text,feedback_options:options});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This feedback could not be submitted.")])})}).promise()},peerAssess:function(optionsSelected,criterionFeedback,overallFeedback){var url=this.url("peer_assess");var payload=JSON.stringify({options_selected:optionsSelected,criterion_feedback:criterionFeedback,overall_feedback:overallFeedback});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})}).promise()},selfAssess:function(optionsSelected,criterionFeedback,overallFeedback){var url=this.url("self_assess");var payload=JSON.stringify({options_selected:optionsSelected,criterion_feedback:criterionFeedback,overall_feedback:overallFeedback});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},trainingAssess:function(optionsSelected){var url=this.url("training_assess");var payload=JSON.stringify({options_selected:optionsSelected});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolveWith(this,[data.corrections])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},scheduleTraining:function(){var url=this.url("schedule_training");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""'}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This assessment could not be submitted.")])})})},rescheduleUnfinishedTasks:function(){var url=this.url("reschedule_unfinished_tasks");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""'}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("One or more rescheduling tasks failed.")])})})},loadXml:function(){var url=this.url("xml");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:'""'}).done(function(data){if(data.success){defer.resolveWith(this,[data.xml])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This problem could not be loaded.")])})}).promise()},updateXml:function(xml){var url=this.url("update_xml");var payload=JSON.stringify({xml:xml});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("This problem could not be saved.")])})}).promise()},checkReleased:function(){var url=this.url("check_released");var payload='""';return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload}).done(function(data){if(data.success){defer.resolveWith(this,[data.is_released])}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("The server could not be contacted.")])})}).promise()},getUploadUrl:function(contentType){var url=this.url("upload_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({contentType:contentType})}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("Could not retrieve upload url.")])})}).promise()},getDownloadUrl:function(){var url=this.url("download_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({})}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(data){defer.rejectWith(this,[gettext("Could not retrieve download url.")])})}).promise()}};if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}OpenAssessment.StaffInfoView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView};OpenAssessment.StaffInfoView.prototype={load:function(){var view=this;if($("#openassessment__staff-info",view.element).length>0){this.server.render("staff_info").done(function(html){$("#openassessment__staff-info",view.element).replaceWith(html);view.installHandlers()}).fail(function(errMsg){view.baseView.showLoadError("staff_info")})}},loadStudentInfo:function(){var view=this;var sel=$("#openassessment__staff-info",this.element);var student_id=sel.find("#openassessment__student_id").val();this.server.studentInfo(student_id).done(function(html){$("#openassessment__student-info",view.element).replaceWith(html)}).fail(function(errMsg){view.showLoadError("student_info")})},installHandlers:function(){var sel=$("#openassessment__staff-info",this.element);var view=this;if(sel.length<=0){return}this.baseView.setUpCollapseExpand(sel,function(){});sel.find("#openassessment_student_info_form").submit(function(eventObject){eventObject.preventDefault();view.loadStudentInfo()});sel.find("#submit_student_id").click(function(eventObject){eventObject.preventDefault();view.loadStudentInfo()});sel.find("#schedule_training").click(function(eventObject){eventObject.preventDefault();view.scheduleTraining()});sel.find("#reschedule_unfinished_tasks").click(function(eventObject){eventObject.preventDefault();view.rescheduleUnfinishedTasks()})},scheduleTraining:function(){var view=this;this.server.scheduleTraining().done(function(msg){$("#schedule_training_message",this.element).text(msg)}).fail(function(errMsg){$("#schedule_training_message",this.element).text(errMsg)})},rescheduleUnfinishedTasks:function(){var view=this;this.server.rescheduleUnfinishedTasks().done(function(msg){$("#reschedule_unfinished_tasks_message",this.element).text(msg)}).fail(function(errMsg){$("#reschedule_unfinished_tasks_message",this.element).text(errMsg)})}};OpenAssessment.StudentTrainingView=function(element,server,baseView){this.element=element;this.server=server;this.baseView=baseView;this.rubric=null};OpenAssessment.StudentTrainingView.prototype={load:function(){var view=this;this.server.render("student_training").done(function(html){$("#openassessment__student-training",view.element).replaceWith(html);view.installHandlers()}).fail(function(errMsg){view.baseView.showLoadError("student-training")})},installHandlers:function(){var sel=$("#openassessment__student-training",this.element);var view=this;this.baseView.setUpCollapseExpand(sel);var rubricSelector=$("#student-training--001__assessment",this.element);if(rubricSelector.size()>0){var rubricElement=rubricSelector.get(0);this.rubric=new OpenAssessment.Rubric(rubricElement)}if(this.rubric!==null){this.rubric.canSubmitCallback($.proxy(this.assessButtonEnabled,this))}sel.find("#student-training--001__assessment__submit").click(function(eventObject){eventObject.preventDefault();view.assess()})},assess:function(){this.assessButtonEnabled(false);var options={};if(this.rubric!==null){options=this.rubric.optionsSelected()}var view=this;var baseView=this.baseView;this.server.trainingAssess(options).done(function(corrections){var incorrect=$("#openassessment__student-training--incorrect",this.element);var instructions=$("#openassessment__student-training--instructions",this.element);if(!view.rubric.showCorrections(corrections)){view.load();
incorrect.removeClass("is--hidden")}baseView.scrollToTop()}).fail(function(errMsg){baseView.toggleActionError("student-training",errMsg);view.assessButtonEnabled(true)})},assessButtonEnabled:function(isEnabled){var button=$("#student-training--001__assessment__submit",this.element);if(typeof isEnabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!isEnabled)}}}; baseView.loadAssessmentModules();incorrect.addClass("is--hidden");instructions.removeClass("is--hidden")}else{instructions.addClass("is--hidden");incorrect.removeClass("is--hidden")}baseView.scrollToTop()}).fail(function(errMsg){baseView.toggleActionError("student-training",errMsg);view.assessButtonEnabled(true)})},assessButtonEnabled:function(isEnabled){var button=$("#student-training--001__assessment__submit",this.element);if(typeof isEnabled==="undefined"){return!button.hasClass("is--disabled")}else{button.toggleClass("is--disabled",!isEnabled)}}};
\ No newline at end of file \ No newline at end of file
...@@ -77,7 +77,7 @@ describe("OpenAssessment.PeerView", function() { ...@@ -77,7 +77,7 @@ describe("OpenAssessment.PeerView", function() {
// Provide overall feedback // Provide overall feedback
var overallFeedback = "Good job!"; var overallFeedback = "Good job!";
view.overallFeedback(overallFeedback); view.rubric.overallFeedback(overallFeedback);
// Submit the peer assessment // Submit the peer assessment
view.peerAssess(); view.peerAssess();
......
...@@ -56,8 +56,28 @@ describe("OpenAssessment.SelfView", function() { ...@@ -56,8 +56,28 @@ describe("OpenAssessment.SelfView", function() {
it("Sends a self assessment to the server", function() { it("Sends a self assessment to the server", function() {
spyOn(server, 'selfAssess').andCallThrough(); spyOn(server, 'selfAssess').andCallThrough();
// Select options in the rubric
var optionsSelected = {};
optionsSelected['Criterion 1'] = 'Poor';
optionsSelected['Criterion 2'] = 'Fair';
optionsSelected['Criterion 3'] = 'Good';
view.rubric.optionsSelected(optionsSelected);
// Provide per-criterion feedback
var criterionFeedback = {};
criterionFeedback['Criterion 1'] = "You did a fair job";
criterionFeedback['Criterion 3'] = "You did a good job";
view.rubric.criterionFeedback(criterionFeedback);
// Provide overall feedback
var overallFeedback = "Good job!";
view.rubric.overallFeedback(overallFeedback);
view.selfAssess(); view.selfAssess();
expect(server.selfAssess).toHaveBeenCalled(); expect(server.selfAssess).toHaveBeenCalledWith(
optionsSelected, criterionFeedback, overallFeedback
);
}); });
it("Re-enables the self assess button on error", function() { it("Re-enables the self assess button on error", function() {
......
...@@ -107,6 +107,29 @@ describe("OpenAssessment.Server", function() { ...@@ -107,6 +107,29 @@ describe("OpenAssessment.Server", function() {
}); });
}); });
it("sends a self-assessment to the XBlock", function() {
stubAjax(true, {success: true, msg: ''});
var success = false;
var options = {clarity: "Very clear", precision: "Somewhat precise"};
var criterionFeedback = {clarity: "This essay was very clear."};
server.selfAssess(options, criterionFeedback, "Excellent job!").done(
function() { success = true; }
);
expect(success).toBe(true);
expect($.ajax).toHaveBeenCalledWith({
url: '/self_assess',
type: "POST",
data: JSON.stringify({
options_selected: options,
criterion_feedback: criterionFeedback,
overall_feedback: "Excellent job!"
})
});
});
it("sends a training assessment to the XBlock", function() { it("sends a training assessment to the XBlock", function() {
stubAjax(true, {success: true, msg: '', correct: true}); stubAjax(true, {success: true, msg: '', correct: true});
var success = false; var success = false;
...@@ -241,7 +264,7 @@ describe("OpenAssessment.Server", function() { ...@@ -241,7 +264,7 @@ describe("OpenAssessment.Server", function() {
it("informs the caller of an AJAX error when sending a self assessment", function() { it("informs the caller of an AJAX error when sending a self assessment", function() {
stubAjax(false, null); stubAjax(false, null);
var receivedMsg = null; var receivedMsg = null;
server.selfAssess("Test").fail(function(errorMsg) { receivedMsg = errorMsg; }); server.selfAssess("Test", {}, "Excellent job!").fail(function(errorMsg) { receivedMsg = errorMsg; });
expect(receivedMsg).toContain('This assessment could not be submitted'); expect(receivedMsg).toContain('This assessment could not be submitted');
}); });
......
...@@ -197,7 +197,7 @@ OpenAssessment.PeerView.prototype = { ...@@ -197,7 +197,7 @@ OpenAssessment.PeerView.prototype = {
this.server.peerAssess( this.server.peerAssess(
this.rubric.optionsSelected(), this.rubric.optionsSelected(),
this.rubric.criterionFeedback(), this.rubric.criterionFeedback(),
this.overallFeedback() this.rubric.overallFeedback()
).done( ).done(
successFunction successFunction
).fail(function(errMsg) { ).fail(function(errMsg) {
...@@ -206,28 +206,5 @@ OpenAssessment.PeerView.prototype = { ...@@ -206,28 +206,5 @@ OpenAssessment.PeerView.prototype = {
}); });
}, },
/**
Get or set overall feedback on the submission.
Args:
overallFeedback (string or undefined): The overall feedback text (optional).
Returns:
string or undefined
Example usage:
>>> view.overallFeedback('Good job!'); // Set the feedback text
>>> view.overallFeedback(); // Retrieve the feedback text
'Good job!'
**/
overallFeedback: function(overallFeedback) {
var selector = '#assessment__rubric__question--feedback__value';
if (typeof overallFeedback === 'undefined') {
return $(selector, this.element).val();
}
else {
$(selector, this.element).val(overallFeedback);
}
}
}; };
...@@ -47,6 +47,31 @@ OpenAssessment.Rubric.prototype = { ...@@ -47,6 +47,31 @@ OpenAssessment.Rubric.prototype = {
}, },
/** /**
Get or set overall feedback on the submission.
Args:
overallFeedback (string or undefined): The overall feedback text (optional).
Returns:
string or undefined
Example usage:
>>> view.overallFeedback('Good job!'); // Set the feedback text
>>> view.overallFeedback(); // Retrieve the feedback text
'Good job!'
**/
overallFeedback: function(overallFeedback) {
var selector = '#assessment__rubric__question--feedback__value';
if (typeof overallFeedback === 'undefined') {
return $(selector, this.element).val();
}
else {
$(selector, this.element).val(overallFeedback);
}
},
/**
Get or set the options selected in the rubric. Get or set the options selected in the rubric.
Args: Args:
......
...@@ -103,8 +103,11 @@ OpenAssessment.SelfView.prototype = { ...@@ -103,8 +103,11 @@ OpenAssessment.SelfView.prototype = {
baseView.toggleActionError('self', null); baseView.toggleActionError('self', null);
view.selfSubmitEnabled(false); view.selfSubmitEnabled(false);
var options = this.rubric.optionsSelected(); this.server.selfAssess(
this.server.selfAssess(options).done( this.rubric.optionsSelected(),
this.rubric.criterionFeedback(),
this.rubric.overallFeedback()
).done(
function() { function() {
baseView.loadAssessmentModules(); baseView.loadAssessmentModules();
baseView.scrollToTop(); baseView.scrollToTop();
......
/** /**
Interface for server-side XBlock handlers. Interface for server-side XBlock handlers.
Args: Args:
...@@ -261,6 +262,8 @@ OpenAssessment.Server.prototype = { ...@@ -261,6 +262,8 @@ OpenAssessment.Server.prototype = {
Args: Args:
optionsSelected (object literal): Keys are criteria names, optionsSelected (object literal): Keys are criteria names,
values are the option text the user selected for the criterion. values are the option text the user selected for the criterion.
var criterionFeedback = { clarity: "The essay was very clear." };
var overallFeedback = "Good job!";
Returns: Returns:
A JQuery promise, which resolves with no args if successful A JQuery promise, which resolves with no args if successful
...@@ -274,10 +277,12 @@ OpenAssessment.Server.prototype = { ...@@ -274,10 +277,12 @@ OpenAssessment.Server.prototype = {
function(errorMsg) { console.log(errorMsg); } function(errorMsg) { console.log(errorMsg); }
); );
**/ **/
selfAssess: function(optionsSelected) { selfAssess: function(optionsSelected, criterionFeedback, overallFeedback) {
var url = this.url('self_assess'); var url = this.url('self_assess');
var payload = JSON.stringify({ var payload = JSON.stringify({
options_selected: optionsSelected options_selected: optionsSelected,
criterion_feedback: criterionFeedback,
overall_feedback: overallFeedback
}); });
return $.Deferred(function(defer) { return $.Deferred(function(defer) {
$.ajax({ type: "POST", url: url, data: payload }).done( $.ajax({ type: "POST", url: url, data: payload }).done(
......
...@@ -115,9 +115,16 @@ class TestGrade(XBlockHandlerTestCase): ...@@ -115,9 +115,16 @@ class TestGrade(XBlockHandlerTestCase):
u'𝖋𝖊𝖊𝖉𝖇𝖆𝖈𝖐 𝖔𝖓𝖑𝖞': u"Ṫḧïṡ ïṡ ṡöṁë ḟëëḋḅäċḳ." u'𝖋𝖊𝖊𝖉𝖇𝖆𝖈𝖐 𝖔𝖓𝖑𝖞': u"Ṫḧïṡ ïṡ ṡöṁë ḟëëḋḅäċḳ."
} }
self_assessment = copy.deepcopy(self.ASSESSMENTS[0])
self_assessment['criterion_feedback'] = {
u'𝖋𝖊𝖊𝖉𝖇𝖆𝖈𝖐 𝖔𝖓𝖑𝖞': "Feedback here",
u'Form': 'lots of feedback yes"',
u'𝓒𝓸𝓷𝓬𝓲𝓼𝓮': "such feedback"
}
# Submit, assess, and render the grade view # Submit, assess, and render the grade view
self._create_submission_and_assessments( self._create_submission_and_assessments(
xblock, self.SUBMISSION, self.PEERS, peer_assessments, self.ASSESSMENTS[0] xblock, self.SUBMISSION, self.PEERS, peer_assessments, self_assessment
) )
# Render the grade section # Render the grade section
...@@ -172,11 +179,13 @@ class TestGrade(XBlockHandlerTestCase): ...@@ -172,11 +179,13 @@ class TestGrade(XBlockHandlerTestCase):
# Verify that the context for the grade complete page contains the feedback # Verify that the context for the grade complete page contains the feedback
_, context = xblock.render_grade_complete(xblock.get_workflow_info()) _, context = xblock.render_grade_complete(xblock.get_workflow_info())
criteria = context['rubric_criteria'] criteria = context['rubric_criteria']
self.assertEqual(criteria[0]['feedback'], [
self.assertEqual(criteria[0]['peer_feedback'], [
u'Peer 2: ฝﻉɭɭ ɗѻกﻉ!', u'Peer 2: ฝﻉɭɭ ɗѻกﻉ!',
u'Peer 1: ฝﻉɭɭ ɗѻกﻉ!', u'Peer 1: ฝﻉɭɭ ɗѻกﻉ!',
]) ])
self.assertEqual(criteria[1]['feedback'], [u'Peer 2: ƒαιя נσв']) self.assertEqual(criteria[0]['self_feedback'], u'Peer 1: ฝﻉɭɭ ɗѻกﻉ!')
self.assertEqual(criteria[1]['peer_feedback'], [u'Peer 2: ƒαιя נσв'])
# The order of the peers in the per-criterion feedback needs # The order of the peers in the per-criterion feedback needs
# to match the order of the peer assessments # to match the order of the peer assessments
...@@ -365,5 +374,6 @@ class TestGrade(XBlockHandlerTestCase): ...@@ -365,5 +374,6 @@ class TestGrade(XBlockHandlerTestCase):
if self_assessment is not None: if self_assessment is not None:
self_api.create_assessment( self_api.create_assessment(
submission['uuid'], student_id, self_assessment['options_selected'], submission['uuid'], student_id, self_assessment['options_selected'],
self_assessment['criterion_feedback'], self_assessment['overall_feedback'],
{'criteria': xblock.rubric_criteria} {'criteria': xblock.rubric_criteria}
) )
...@@ -9,6 +9,7 @@ import mock ...@@ -9,6 +9,7 @@ import mock
import pytz import pytz
from openassessment.assessment.api import self as self_api from openassessment.assessment.api import self as self_api
from openassessment.workflow import api as workflow_api from openassessment.workflow import api as workflow_api
from openassessment.xblock.data_conversion import create_rubric_dict
from .base import XBlockHandlerTestCase, scenario from .base import XBlockHandlerTestCase, scenario
...@@ -23,6 +24,8 @@ class TestSelfAssessment(XBlockHandlerTestCase): ...@@ -23,6 +24,8 @@ class TestSelfAssessment(XBlockHandlerTestCase):
ASSESSMENT = { ASSESSMENT = {
'options_selected': {u'𝓒𝓸𝓷𝓬𝓲𝓼𝓮': u'ﻉซƈﻉɭɭﻉกՇ', u'Form': u'Fair'}, 'options_selected': {u'𝓒𝓸𝓷𝓬𝓲𝓼𝓮': u'ﻉซƈﻉɭɭﻉกՇ', u'Form': u'Fair'},
'criterion_feedback': {},
'overall_feedback': ""
} }
@scenario('data/self_assessment_scenario.xml', user_id='Bob') @scenario('data/self_assessment_scenario.xml', user_id='Bob')
...@@ -87,6 +90,10 @@ class TestSelfAssessment(XBlockHandlerTestCase): ...@@ -87,6 +90,10 @@ class TestSelfAssessment(XBlockHandlerTestCase):
# Submit a self assessment for a rubric with a feedback-only criterion # Submit a self assessment for a rubric with a feedback-only criterion
assessment_dict = { assessment_dict = {
'options_selected': {u'vocabulary': u'good'}, 'options_selected': {u'vocabulary': u'good'},
'criterion_feedback': {
u'vocabulary': 'Awesome job!',
u'𝖋𝖊𝖊𝖉𝖇𝖆𝖈𝖐 𝖔𝖓𝖑𝖞': 'fairly illegible.'
},
'overall_feedback': u'' 'overall_feedback': u''
} }
resp = self.request(xblock, 'self_assess', json.dumps(assessment_dict), response_format='json') resp = self.request(xblock, 'self_assess', json.dumps(assessment_dict), response_format='json')
...@@ -99,10 +106,9 @@ class TestSelfAssessment(XBlockHandlerTestCase): ...@@ -99,10 +106,9 @@ class TestSelfAssessment(XBlockHandlerTestCase):
self.assertEqual(assessment['parts'][0]['option']['points'], 1) self.assertEqual(assessment['parts'][0]['option']['points'], 1)
# Check the feedback-only criterion score/feedback # Check the feedback-only criterion score/feedback
# The written feedback should default to an empty string
self.assertEqual(assessment['parts'][1]['criterion']['name'], u'𝖋𝖊𝖊𝖉𝖇𝖆𝖈𝖐 𝖔𝖓𝖑𝖞') self.assertEqual(assessment['parts'][1]['criterion']['name'], u'𝖋𝖊𝖊𝖉𝖇𝖆𝖈𝖐 𝖔𝖓𝖑𝖞')
self.assertIs(assessment['parts'][1]['option'], None) self.assertIs(assessment['parts'][1]['option'], None)
self.assertEqual(assessment['parts'][1]['feedback'], u'') self.assertEqual(assessment['parts'][1]['feedback'], u'fairly illegible.')
@scenario('data/self_assessment_scenario.xml', user_id='Bob') @scenario('data/self_assessment_scenario.xml', user_id='Bob')
def test_self_assess_workflow_error(self, xblock): def test_self_assess_workflow_error(self, xblock):
...@@ -267,7 +273,8 @@ class TestSelfAssessmentRender(XBlockHandlerTestCase): ...@@ -267,7 +273,8 @@ class TestSelfAssessmentRender(XBlockHandlerTestCase):
submission['uuid'], submission['uuid'],
xblock.get_student_item_dict()['student_id'], xblock.get_student_item_dict()['student_id'],
{u'𝓒𝓸𝓷𝓬𝓲𝓼𝓮': u'ﻉซƈﻉɭɭﻉกՇ', u'Form': u'Fair'}, {u'𝓒𝓸𝓷𝓬𝓲𝓼𝓮': u'ﻉซƈﻉɭɭﻉกՇ', u'Form': u'Fair'},
{'criteria': xblock.rubric_criteria} {}, "Good job!",
create_rubric_dict(xblock.prompt, xblock.rubric_criteria)
) )
self._assert_path_and_context( self._assert_path_and_context(
xblock, 'openassessmentblock/self/oa_self_complete.html', {}, xblock, 'openassessmentblock/self/oa_self_complete.html', {},
...@@ -302,7 +309,8 @@ class TestSelfAssessmentRender(XBlockHandlerTestCase): ...@@ -302,7 +309,8 @@ class TestSelfAssessmentRender(XBlockHandlerTestCase):
submission['uuid'], submission['uuid'],
xblock.get_student_item_dict()['student_id'], xblock.get_student_item_dict()['student_id'],
{u'𝓒𝓸𝓷𝓬𝓲𝓼𝓮': u'ﻉซƈﻉɭɭﻉกՇ', u'Form': u'Fair'}, {u'𝓒𝓸𝓷𝓬𝓲𝓼𝓮': u'ﻉซƈﻉɭɭﻉกՇ', u'Form': u'Fair'},
{'criteria': xblock.rubric_criteria} {}, "Good job!",
create_rubric_dict(xblock.prompt, xblock.rubric_criteria)
) )
# This case probably isn't possible, because presumably when we create # This case probably isn't possible, because presumably when we create
......
...@@ -32,6 +32,12 @@ ASSESSMENT_DICT = { ...@@ -32,6 +32,12 @@ ASSESSMENT_DICT = {
"Clear-headed": "Yogi Berra", "Clear-headed": "Yogi Berra",
"Form": "Reddit", "Form": "Reddit",
}, },
'criterion_feedback': {
"Concise": "Not very.",
"Clear-headed": "Indubitably",
"Form": "s ka tter ed"
}
} }
...@@ -209,6 +215,8 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -209,6 +215,8 @@ class TestCourseStaff(XBlockHandlerTestCase):
submission['uuid'], submission['uuid'],
STUDENT_ITEM["student_id"], STUDENT_ITEM["student_id"],
ASSESSMENT_DICT['options_selected'], ASSESSMENT_DICT['options_selected'],
ASSESSMENT_DICT['criterion_feedback'],
ASSESSMENT_DICT['overall_feedback'],
{'criteria': xblock.rubric_criteria}, {'criteria': xblock.rubric_criteria},
) )
...@@ -235,6 +243,13 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -235,6 +243,13 @@ class TestCourseStaff(XBlockHandlerTestCase):
"Content": "Poor", "Content": "Poor",
} }
criterion_feedback = {
"Ideas": "Dear diary: Lots of creativity from my dream journal last night at 2 AM,",
"Content": "Not as insightful as I had thought in the wee hours of the morning!"
}
overall_feedback = "I think I should tell more people about how important worms are for the ecosystem."
bob_item = STUDENT_ITEM.copy() bob_item = STUDENT_ITEM.copy()
bob_item["item_id"] = xblock.scope_ids.usage_id bob_item["item_id"] = xblock.scope_ids.usage_id
...@@ -265,6 +280,8 @@ class TestCourseStaff(XBlockHandlerTestCase): ...@@ -265,6 +280,8 @@ class TestCourseStaff(XBlockHandlerTestCase):
submission['uuid'], submission['uuid'],
STUDENT_ITEM["student_id"], STUDENT_ITEM["student_id"],
options_selected, options_selected,
criterion_feedback,
overall_feedback,
{'criteria': xblock.rubric_criteria}, {'criteria': xblock.rubric_criteria},
) )
......
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