Commit f7543120 by Eric Fischer Committed by Andy Armstrong

TNL-3714, adding backend support for staff scoring

Changes include:
    -modifies requirements to gt an updated version of edx-submissions
    -adds set_staff_score to the AssessmentWorkflow model, which will record
        a new annotated score using edx-submissions functionality
    -prevents recording non-staff scores if a staff score exists
    -modifies update_from_assessment to call set_staff_score as needed
        -this includes changes to both the workflow model and its api
    -modifies get_score to allow staff overrides as optional
    -modifies the assessment serializer to include id information
        -adds this information to get_score method in self, ai, peer, and staff
            apis, to expose c ontributing_assessments where needed
    -fixes a small bug regarding None vs {} in the peer api
    -adds staff-assessment to the xblock, and makes it always available
    -uses the new force_update_score parameter on the workflow api when
        recording a staff assessment
parent 53afc52b
...@@ -67,7 +67,8 @@ def get_score(submission_uuid, requirements): ...@@ -67,7 +67,8 @@ def get_score(submission_uuid, requirements):
requirements (dict): Not used. requirements (dict): Not used.
Returns: Returns:
A dictionary with the points earned and points possible. A dictionary with the points earned, points possible, and
contributing_assessments information, along with a None staff_id.
""" """
assessment = get_latest_assessment(submission_uuid) assessment = get_latest_assessment(submission_uuid)
...@@ -76,7 +77,9 @@ def get_score(submission_uuid, requirements): ...@@ -76,7 +77,9 @@ def get_score(submission_uuid, requirements):
return { return {
"points_earned": assessment["points_earned"], "points_earned": assessment["points_earned"],
"points_possible": assessment["points_possible"] "points_possible": assessment["points_possible"],
"contributing_assessments": [assessment["id"]],
"staff_id": None,
} }
......
...@@ -45,7 +45,7 @@ def submitter_is_finished(submission_uuid, requirements): ...@@ -45,7 +45,7 @@ def submitter_is_finished(submission_uuid, requirements):
bool bool
""" """
if requirements is None: if not requirements:
return False return False
try: try:
...@@ -80,7 +80,7 @@ def assessment_is_finished(submission_uuid, requirements): ...@@ -80,7 +80,7 @@ def assessment_is_finished(submission_uuid, requirements):
bool bool
""" """
if requirements is None: if not requirements:
return False return False
workflow = PeerWorkflow.get_by_submission_uuid(submission_uuid) workflow = PeerWorkflow.get_by_submission_uuid(submission_uuid)
...@@ -146,7 +146,8 @@ def get_score(submission_uuid, requirements): ...@@ -146,7 +146,8 @@ def get_score(submission_uuid, requirements):
must receive to get a score. must receive to get a score.
Returns: Returns:
dict with keys "points_earned" and "points_possible". A dictionary with the points earned, points possible, and
contributing_assessments information, along with a None staff_id.
""" """
if requirements is None: if requirements is None:
...@@ -183,12 +184,15 @@ def get_score(submission_uuid, requirements): ...@@ -183,12 +184,15 @@ def get_score(submission_uuid, requirements):
for scored_item in items[:requirements["must_be_graded_by"]]: for scored_item in items[:requirements["must_be_graded_by"]]:
scored_item.scored = True scored_item.scored = True
scored_item.save() scored_item.save()
assessments = [item.assessment for item in items]
return { return {
"points_earned": sum( "points_earned": sum(
get_assessment_median_scores(submission_uuid).values() get_assessment_median_scores(submission_uuid).values()
), ),
"points_possible": items[0].assessment.points_possible, "points_possible": assessments[0].points_possible,
"contributing_assessments": [assessment.id for assessment in assessments],
"staff_id": None,
} }
......
...@@ -70,8 +70,8 @@ def get_score(submission_uuid, requirements): ...@@ -70,8 +70,8 @@ def get_score(submission_uuid, requirements):
submission_uuid (str): The unique identifier for the submission submission_uuid (str): The unique identifier for the submission
requirements (dict): Not used. requirements (dict): Not used.
Returns: Returns:
A dict of points earned and points possible for the given submission. A dictionary with the points earned, points possible, and
Returns None if no score can be determined yet. contributing_assessments information, along with a None staff_id.
Examples: Examples:
>>> get_score('222bdf3d-a88e-11e3-859e-040ccee02800', {}) >>> get_score('222bdf3d-a88e-11e3-859e-040ccee02800', {})
{ {
...@@ -85,7 +85,9 @@ def get_score(submission_uuid, requirements): ...@@ -85,7 +85,9 @@ def get_score(submission_uuid, requirements):
return { return {
"points_earned": assessment["points_earned"], "points_earned": assessment["points_earned"],
"points_possible": assessment["points_possible"] "points_possible": assessment["points_possible"],
"contributing_assessments": [assessment["id"]],
"staff_id": None,
} }
...@@ -284,12 +286,15 @@ def get_assessment_scores_by_criteria(submission_uuid): ...@@ -284,12 +286,15 @@ def get_assessment_scores_by_criteria(submission_uuid):
information to form the median scores, an error is raised. information to form the median scores, an error is raised.
""" """
try: try:
# This will always create a list of length 1
assessments = list( assessments = list(
Assessment.objects.filter( Assessment.objects.filter(
score_type=SELF_TYPE, submission_uuid=submission_uuid score_type=SELF_TYPE, submission_uuid=submission_uuid
).order_by('-scored_at')[:1] ).order_by('-scored_at')[:1]
) )
scores = Assessment.scores_by_criterion(assessments) scores = Assessment.scores_by_criterion(assessments)
# Since this is only being sent one score, the median score will be the
# same as the only score.
return Assessment.get_median_score_dict(scores) return Assessment.get_median_score_dict(scores)
except DatabaseError: except DatabaseError:
error_message = ( error_message = (
......
...@@ -17,11 +17,9 @@ from openassessment.assessment.serializers import ( ...@@ -17,11 +17,9 @@ from openassessment.assessment.serializers import (
from openassessment.assessment.errors import ( from openassessment.assessment.errors import (
StaffAssessmentRequestError, StaffAssessmentInternalError StaffAssessmentRequestError, StaffAssessmentInternalError
) )
from submissions import api as sub_api
logger = logging.getLogger("openassessment.assessment.api.staff") logger = logging.getLogger("openassessment.assessment.api.staff")
STAFF_TYPE = "ST" STAFF_TYPE = "ST"
...@@ -43,20 +41,18 @@ def submitter_is_finished(submission_uuid, requirements): ...@@ -43,20 +41,18 @@ def submitter_is_finished(submission_uuid, requirements):
def assessment_is_finished(submission_uuid, requirements): def assessment_is_finished(submission_uuid, requirements):
""" """
Determine if the assessment of the given submission is completed. This Determine if the staff assessment step of the given submission is completed.
checks to see if staff have completed the assessment. This checks to see if staff have completed the assessment.
Args: Args:
submission_uuid (str): The UUID of the submission being graded. submission_uuid (str): The UUID of the submission being graded.
requirements (dict): Any variables that may effect this state. requirements (dict): Any variables that may effect this state.
Returns: Returns:
True if the assessment has been completed for this submission. True if a staff assessment has been completed for this submission or if not required.
""" """
required = requirements.get('staff', {}).get('required', False) if requirements and requirements.get('staff', {}).get('required', False):
if required: return bool(get_latest_staff_assessment(submission_uuid))
return bool(get_latest_assessment(submission_uuid))
return True return True
...@@ -71,20 +67,23 @@ def get_score(submission_uuid, requirements): ...@@ -71,20 +67,23 @@ def get_score(submission_uuid, requirements):
requirements (dict): Not used. requirements (dict): Not used.
Returns: Returns:
A dictionary with the points earned and points possible. A dictionary with the points earned, points possible,
contributing_assessments, and staff_id information.
""" """
assessment = get_latest_assessment(submission_uuid) assessment = get_latest_staff_assessment(submission_uuid)
if not assessment: if not assessment:
return None return None
return { return {
"points_earned": assessment["points_earned"], "points_earned": assessment["points_earned"],
"points_possible": assessment["points_possible"] "points_possible": assessment["points_possible"],
"contributing_assessments": [assessment["id"]],
"staff_id": assessment["scorer_id"],
} }
def get_latest_assessment(submission_uuid): def get_latest_staff_assessment(submission_uuid):
""" """
Retrieve the latest staff assessment for a submission. Retrieve the latest staff assessment for a submission.
...@@ -96,11 +95,11 @@ def get_latest_assessment(submission_uuid): ...@@ -96,11 +95,11 @@ def get_latest_assessment(submission_uuid):
or None if no assessments are available or None if no assessments are available
Raises: Raises:
StaffAssessmentInternalError StaffAssessmentInternalError if there are problems connecting to the database.
Example usage: Example usage:
>>> get_latest_assessment('10df7db776686822e501b05f452dc1e4b9141fe5') >>> get_latest_staff_assessment('10df7db776686822e501b05f452dc1e4b9141fe5')
{ {
'points_earned': 6, 'points_earned': 6,
'points_possible': 12, 'points_possible': 12,
...@@ -130,7 +129,7 @@ def get_latest_assessment(submission_uuid): ...@@ -130,7 +129,7 @@ def get_latest_assessment(submission_uuid):
def get_assessment_scores_by_criteria(submission_uuid): def get_assessment_scores_by_criteria(submission_uuid):
"""Get the score for each rubric criterion """Get the staff score for each rubric criterion
Args: Args:
submission_uuid (str): The submission uuid is used to get the submission_uuid (str): The submission uuid is used to get the
...@@ -145,12 +144,15 @@ def get_assessment_scores_by_criteria(submission_uuid): ...@@ -145,12 +144,15 @@ def get_assessment_scores_by_criteria(submission_uuid):
information from the scores, an error is raised. information from the scores, an error is raised.
""" """
try: try:
# This will always create a list of length 1
assessments = list( assessments = list(
Assessment.objects.filter( Assessment.objects.filter(
score_type=STAFF_TYPE, submission_uuid=submission_uuid score_type=STAFF_TYPE, submission_uuid=submission_uuid
)[:1] )[:1]
) )
scores = Assessment.scores_by_criterion(assessments) scores = Assessment.scores_by_criterion(assessments)
# Since this is only being sent one score, the median score will be the
# same as the only score.
return Assessment.get_median_score_dict(scores) return Assessment.get_median_score_dict(scores)
except DatabaseError: except DatabaseError:
error_message = u"Error getting staff assessment scores for {}".format(submission_uuid) error_message = u"Error getting staff assessment scores for {}".format(submission_uuid)
...@@ -175,6 +177,8 @@ def create_assessment( ...@@ -175,6 +177,8 @@ def create_assessment(
Assumes that the user creating the assessment has the permissions to do so. Assumes that the user creating the assessment has the permissions to do so.
Args: Args:
submission_uuid (str): The submission uuid for the submission being
assessed.
scorer_id (str): The user ID for the user giving this assessment. This scorer_id (str): The user ID for the user giving this assessment. This
is required to create an assessment on a submission. is required to create an assessment on a submission.
options_selected (dict): Dictionary mapping criterion names to the options_selected (dict): Dictionary mapping criterion names to the
...@@ -184,6 +188,10 @@ def create_assessment( ...@@ -184,6 +188,10 @@ def create_assessment(
Since criterion feedback is optional, some criteria may not appear Since criterion feedback is optional, some criteria may not appear
in the dictionary. in the dictionary.
overall_feedback (unicode): Free-form text feedback on the submission overall. overall_feedback (unicode): Free-form text feedback on the submission overall.
rubric_dict (dict): The rubric model associated with this assessment
scored_at (datetime): Optional argument to override the time in which
the assessment took place. If not specified, scored_at is set to
now.
Keyword Args: Keyword Args:
scored_at (datetime): Optional argument to override the time in which scored_at (datetime): Optional argument to override the time in which
...@@ -219,13 +227,13 @@ def create_assessment( ...@@ -219,13 +227,13 @@ def create_assessment(
return full_assessment_dict(assessment) return full_assessment_dict(assessment)
except InvalidRubric: except InvalidRubric:
msg = u"Rubric definition was not valid" error_message = u"Rubric definition was not valid"
logger.exception(msg) logger.exception(error_message)
raise StaffAssessmentRequestError(msg) raise StaffAssessmentRequestError(error_message)
except InvalidRubricSelection: except InvalidRubricSelection:
msg = u"Invalid options selected in the rubric" error_message = u"Invalid options selected in the rubric"
logger.warning(msg, exc_info=True) logger.warning(error_message, exc_info=True)
raise StaffAssessmentRequestError(msg) raise StaffAssessmentRequestError(error_message)
except DatabaseError: except DatabaseError:
error_message = ( error_message = (
u"An error occurred while creating assessment by scorer with ID: {}" u"An error occurred while creating assessment by scorer with ID: {}"
...@@ -249,11 +257,10 @@ def _complete_assessment( ...@@ -249,11 +257,10 @@ def _complete_assessment(
in a single transaction. in a single transaction.
Args: Args:
rubric_dict (dict): The rubric model associated with this assessment
scorer_id (str): The user ID for the user giving this assessment. This
is required to create an assessment on a submission.
submission_uuid (str): The submission uuid for the submission being submission_uuid (str): The submission uuid for the submission being
assessed. assessed.
scorer_id (str): The user ID for the user giving this assessment. This
is required to create an assessment on a submission.
options_selected (dict): Dictionary mapping criterion names to the options_selected (dict): Dictionary mapping criterion names to the
option names the user selected for that criterion. option names the user selected for that criterion.
criterion_feedback (dict): Dictionary mapping criterion names to the criterion_feedback (dict): Dictionary mapping criterion names to the
...@@ -261,6 +268,7 @@ def _complete_assessment( ...@@ -261,6 +268,7 @@ def _complete_assessment(
Since criterion feedback is optional, some criteria may not appear Since criterion feedback is optional, some criteria may not appear
in the dictionary. in the dictionary.
overall_feedback (unicode): Free-form text feedback on the submission overall. overall_feedback (unicode): Free-form text feedback on the submission overall.
rubric_dict (dict): The rubric model associated with this assessment
scored_at (datetime): Optional argument to override the time in which scored_at (datetime): Optional argument to override the time in which
the assessment took place. If not specified, scored_at is set to the assessment took place. If not specified, scored_at is set to
now. now.
......
""" Create generic errors that can be shared across different assessment types. """
class AssessmentError(Exception):
""" A generic error for errors that occur during assessment. """
pass
""" """
Errors for the peer assessment. Errors for the peer assessment.
""" """
from .base import AssessmentError
class PeerAssessmentError(Exception): class PeerAssessmentError(AssessmentError):
"""Generic Peer Assessment Error """Generic Peer Assessment Error
Raised when an error occurs while processing a request related to the Raised when an error occurs while processing a request related to the
......
""" """
Errors for self-assessment Errors for self-assessment
""" """
from .base import AssessmentError
class SelfAssessmentError(Exception):
class SelfAssessmentError(AssessmentError):
"""Generic Self Assessment Error """Generic Self Assessment Error
Raised when an error occurs while processing a request related to the Raised when an error occurs while processing a request related to the
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
Errors for the staff assessment api. Errors for the staff assessment api.
""" """
from .base import AssessmentError
class StaffAssessmentError(Exception):
class StaffAssessmentError(AssessmentError):
"""Generic Staff Assessment Error """Generic Staff Assessment Error
Raised when an error occurs while processing a request related to Raised when an error occurs while processing a request related to
......
...@@ -232,6 +232,7 @@ def full_assessment_dict(assessment, rubric_dict=None): ...@@ -232,6 +232,7 @@ def full_assessment_dict(assessment, rubric_dict=None):
for part_dict in parts for part_dict in parts
) )
assessment_dict["points_possible"] = rubric_dict["points_possible"] assessment_dict["points_possible"] = rubric_dict["points_possible"]
assessment_dict["id"] = assessment.id
cache.set(assessment_cache_key, assessment_dict) cache.set(assessment_cache_key, assessment_dict)
......
...@@ -51,6 +51,34 @@ RUBRIC = { ...@@ -51,6 +51,34 @@ RUBRIC = {
] ]
} }
RUBRIC_POSSIBLE_POINTS = sum(
max(
option["points"] for option in criterion["options"]
) for criterion in RUBRIC["criteria"]
)
# Used to generate OPTIONS_SELECTED_DICT. Indices refer to RUBRIC_OPTIONS.
OPTIONS_SELECTED_CHOICES = {
"none": [0, 0],
"few": [0, 1],
"most": [1, 2],
"all": [2, 2],
}
OPTIONS_SELECTED_DICT = {
# This dict is constructed from OPTIONS_SELECTED_CHOICES.
# 'key' is expected to be a string, such as 'none', 'all', etc.
# 'value' is a list, indicating the indices of the RUBRIC_OPTIONS selections that pertain to that key
key: {
"options": {
RUBRIC["criteria"][i]["name"]: RUBRIC_OPTIONS[j]["name"] for i, j in enumerate(value)
},
"expected_points": sum(
RUBRIC_OPTIONS[i]["points"] for i in value
)
} for key, value in OPTIONS_SELECTED_CHOICES.iteritems()
}
EXAMPLES = [ EXAMPLES = [
{ {
'answer': ( 'answer': (
......
# coding=utf-8
import copy
import mock
from django.db import DatabaseError
from django.test.utils import override_settings
from ddt import ddt, data, file_data, unpack
from nose.tools import raises
from .constants import OPTIONS_SELECTED_DICT, RUBRIC, RUBRIC_OPTIONS, RUBRIC_POSSIBLE_POINTS, STUDENT_ITEM
from openassessment.assessment.test.test_ai import (
ALGORITHM_ID,
AI_ALGORITHMS,
AIGradingTest,
train_classifiers
)
from openassessment.test_utils import CacheResetTest
from openassessment.assessment.api import staff as staff_api, ai as ai_api, peer as peer_api
from openassessment.assessment.api.self import create_assessment as self_assess
from openassessment.assessment.api.peer import create_assessment as peer_assess
from openassessment.assessment.models import Assessment, PeerWorkflow
from openassessment.assessment.errors import StaffAssessmentRequestError, StaffAssessmentInternalError
from openassessment.workflow import api as workflow_api
from submissions import api as sub_api
@ddt
class TestStaffAssessment(CacheResetTest):
"""
Tests for staff assessments made as overrides, when none is required to exist.
"""
STEP_REQUIREMENTS = {}
STEP_REQUIREMENTS_WITH_STAFF = {'staff': {'required': True}}
# This is due to ddt not playing nicely with list comprehensions
ASSESSMENT_SCORES_DDT = [key for key in OPTIONS_SELECTED_DICT]
@staticmethod
@override_settings(ORA2_AI_ALGORITHMS=AI_ALGORITHMS)
def _ai_assess(sub):
# Note that CLASSIFIER_SCORE_OVERRIDES matches OPTIONS_SELECTED_DICT['most'] scores
train_classifiers(RUBRIC, AIGradingTest.CLASSIFIER_SCORE_OVERRIDES)
ai_api.on_init(sub, rubric=RUBRIC, algorithm_id=ALGORITHM_ID)
return ai_api.get_latest_assessment(sub)
@staticmethod
def _peer_assess(sub, scorer_id, scores):
bob_sub, bob = TestStaffAssessment._create_student_and_submission("Bob", "Bob's answer", override_steps=['peer'])
peer_api.get_submission_to_assess(bob_sub["uuid"], 1)
return peer_assess(bob_sub["uuid"], bob["student_id"], scores, dict(), "", RUBRIC, 1)
ASSESSMENT_TYPES_DDT = [
('self', lambda sub, scorer_id, scores: self_assess(sub, scorer_id, scores, dict(), "", RUBRIC)),
('peer', lambda sub, scorer_id, scores: TestStaffAssessment._peer_assess(sub, scorer_id, scores)),
('staff', lambda sub, scorer_id, scores: staff_api.create_assessment(sub, scorer_id, scores, dict(), "", RUBRIC)),
('ai', lambda sub, scorer_id, scores: TestStaffAssessment._ai_assess(sub))
]
@data(*ASSESSMENT_SCORES_DDT)
def test_create_assessment_not_required(self, key):
"""
Simple test to ensure staff assessments are scored properly, for all values of OPTIONS_SELECTED_DICT,
when staff scores are not required.
"""
# Create assessment
tim_sub, tim = self._create_student_and_submission("Tim", "Tim's answer")
# Staff assess it
assessment = staff_api.create_assessment(
tim_sub["uuid"],
"Dumbledore",
OPTIONS_SELECTED_DICT[key]["options"], dict(), "",
RUBRIC,
)
# Ensure points are calculated properly
self.assertEqual(assessment["points_earned"], OPTIONS_SELECTED_DICT[key]["expected_points"])
self.assertEqual(assessment["points_possible"], RUBRIC_POSSIBLE_POINTS)
# ensure submission is marked as finished
self.assertTrue(staff_api.assessment_is_finished(tim_sub["uuid"], self.STEP_REQUIREMENTS))
@data(*ASSESSMENT_SCORES_DDT)
def test_create_assessment_required(self, key):
"""
Simple test to ensure staff assessments are scored properly, for all values of OPTIONS_SELECTED_DICT,
when staff scores are required.
"""
# Create assessment
tim_sub, tim = self._create_student_and_submission("Tim", "Tim's answer", override_steps=['staff'])
# Verify that we're still waiting on a staff assessment
self.assertFalse(staff_api.assessment_is_finished(tim_sub["uuid"], self.STEP_REQUIREMENTS_WITH_STAFF))
# Staff assess
staff_assessment = staff_api.create_assessment(
tim_sub["uuid"],
"Dumbledore",
OPTIONS_SELECTED_DICT[key]["options"], dict(), "",
RUBRIC,
)
# Verify assesment made, score updated, and no longer waiting
self.assertEqual(staff_assessment["points_earned"], OPTIONS_SELECTED_DICT[key]["expected_points"])
self.assertTrue(staff_api.assessment_is_finished(tim_sub["uuid"], self.STEP_REQUIREMENTS_WITH_STAFF))
@data(*ASSESSMENT_SCORES_DDT)
def test_create_assessment_score_overrides(self, key):
"""
Test to ensure that scores can be overriden by a staff assessment using any value.
"""
# Initially, self-asses with an all value
initial_assessment = OPTIONS_SELECTED_DICT["all"]
# Unless we're trying to override with an all value, then start with none
if key == "all":
initial_assessment = OPTIONS_SELECTED_DICT["none"]
# Create assessment
tim_sub, tim = self._create_student_and_submission("Tim", "Tim's answer")
# Self assess it
self_assessment = self_assess(
tim_sub["uuid"],
tim["student_id"],
initial_assessment["options"], dict(), "",
RUBRIC,
)
# Verify both assessment and workflow report correct score
self.assertEqual(self_assessment["points_earned"], initial_assessment["expected_points"])
workflow = workflow_api.get_workflow_for_submission(tim_sub["uuid"], self.STEP_REQUIREMENTS)
self.assertEqual(workflow["score"]["points_earned"], initial_assessment["expected_points"])
# Now override with a staff assessment
staff_assessment = staff_api.create_assessment(
tim_sub["uuid"],
"Dumbledore",
OPTIONS_SELECTED_DICT[key]["options"], dict(), "",
RUBRIC,
)
# Verify both assessment and workflow report correct score
self.assertEqual(staff_assessment["points_earned"], OPTIONS_SELECTED_DICT[key]["expected_points"])
workflow = workflow_api.get_workflow_for_submission(tim_sub["uuid"], self.STEP_REQUIREMENTS)
self.assertEqual(workflow["score"]["points_earned"], OPTIONS_SELECTED_DICT[key]["expected_points"])
@data(*ASSESSMENT_TYPES_DDT)
@unpack
def test_create_assessment_type_overrides(self, initial_type, initial_assess):
"""
Test to ensure that any assesment, even a staff assessment, can be overriden by a staff assessment.
"""
# Initially, asses with a 'most' value
# This was selected to match the value that the ai test will set
initial_assessment = OPTIONS_SELECTED_DICT["most"]
# Create assessment
tim_sub, tim = self._create_student_and_submission("Tim", "Tim's answer", override_steps=[initial_type])
# Initially assess it
assessment = initial_assess(tim_sub["uuid"], tim["student_id"], initial_assessment["options"])
# and update workflow with new scores
requirements = self.STEP_REQUIREMENTS
if initial_type == 'peer':
requirements = {"peer": {"must_grade": 0, "must_be_graded_by": 1}}
# Verify both assessment and workflow report correct score
self.assertEqual(assessment["points_earned"], initial_assessment["expected_points"])
workflow = workflow_api.get_workflow_for_submission(tim_sub["uuid"], requirements)
self.assertEqual(workflow["score"]["points_earned"], initial_assessment["expected_points"])
staff_score = "few"
# Now override with a staff assessment
staff_assessment = staff_api.create_assessment(
tim_sub["uuid"],
"Dumbledore",
OPTIONS_SELECTED_DICT[staff_score]["options"], dict(), "",
RUBRIC,
)
# Verify both assessment and workflow report correct score
self.assertEqual(staff_assessment["points_earned"], OPTIONS_SELECTED_DICT[staff_score]["expected_points"])
workflow = workflow_api.get_workflow_for_submission(tim_sub["uuid"], self.STEP_REQUIREMENTS)
self.assertEqual(workflow["score"]["points_earned"], OPTIONS_SELECTED_DICT[staff_score]["expected_points"])
@data(*ASSESSMENT_TYPES_DDT)
@unpack
def test_create_assessment_does_not_block(self, after_type, after_assess):
"""
Test to ensure that the presence of an override staff assessment only prevents new scores from being recorded;
other assessments can still be made.
"""
# Staff assessments do not block other staff scores from overriding, so skip that test
if after_type == 'staff':
return
# Create assessment
tim_sub, tim = self._create_student_and_submission("Tim", "Tim's answer", override_steps=[after_type])
staff_score = "few"
# Staff assess it
staff_assessment = staff_api.create_assessment(
tim_sub["uuid"],
"Dumbledore",
OPTIONS_SELECTED_DICT[staff_score]['options'], dict(), "",
RUBRIC,
)
# Verify both assessment and workflow report correct score
self.assertEqual(staff_assessment["points_earned"], OPTIONS_SELECTED_DICT[staff_score]["expected_points"])
workflow = workflow_api.get_workflow_for_submission(tim_sub["uuid"], self.STEP_REQUIREMENTS)
self.assertEqual(workflow["score"]["points_earned"], OPTIONS_SELECTED_DICT[staff_score]["expected_points"])
# Now, non-force asses with a 'most' value
# This was selected to match the value that the ai test will set
unscored_assessment = OPTIONS_SELECTED_DICT["most"]
assessment = after_assess(tim_sub["uuid"], tim["student_id"], unscored_assessment["options"])
# and update workflow with new scores
requirements = self.STEP_REQUIREMENTS
if after_type == 'peer':
requirements = {"peer": {"must_grade": 0, "must_be_graded_by": 1}}
# Verify both assessment and workflow report correct score (workflow should report previous value)
self.assertEqual(assessment["points_earned"], unscored_assessment["expected_points"])
workflow = workflow_api.get_workflow_for_submission(tim_sub["uuid"], requirements)
self.assertEqual(workflow["score"]["points_earned"], OPTIONS_SELECTED_DICT[staff_score]["expected_points"])
def test_invalid_rubric_exception(self):
# Create a submission
tim_sub, tim = self._create_student_and_submission("Tim", "Tim's answer")
# Define invalid rubric
invalid_rubric = copy.deepcopy(RUBRIC)
for criterion in invalid_rubric["criteria"]:
for option in criterion["options"]:
option["points"] = -1
# Try to staff assess with invalid rubric
with self.assertRaises(StaffAssessmentRequestError) as context_manager:
staff_assessment = staff_api.create_assessment(
tim_sub["uuid"],
"Dumbledore",
OPTIONS_SELECTED_DICT["most"]["options"], dict(), "",
invalid_rubric,
)
self.assertEqual(str(context_manager.exception), u"Rubric definition was not valid")
@data("criterion_not_found", "option_not_found", "missing_criteria", "some_criteria_not_assessed")
def test_invalid_rubric_options_exception(self, invalid_reason):
# Define invalid options_selected
dict_to_use = copy.deepcopy(OPTIONS_SELECTED_DICT['all']["options"])
if invalid_reason == "criterion_not_found":
dict_to_use["invalid"] = RUBRIC_OPTIONS[0]["name"]
elif invalid_reason == "option_not_found":
dict_to_use[RUBRIC["criteria"][0]["name"]] = "invalid"
elif invalid_reason == "missing_criteria":
del dict_to_use[RUBRIC["criteria"][0]["name"]]
elif invalid_reason == "some_criteria_not_assessed":
dict_to_use[RUBRIC["criteria"][0]["name"]] = None
# Create a submission
tim_sub, tim = self._create_student_and_submission("Tim", "Tim's answer")
# Try to staff assess with invalid options selected
with self.assertRaises(StaffAssessmentRequestError) as context_manager:
staff_assessment = staff_api.create_assessment(
tim_sub["uuid"],
"Dumbledore",
dict_to_use, dict(), "",
RUBRIC,
)
self.assertEqual(str(context_manager.exception), u"Invalid options selected in the rubric")
@mock.patch.object(Assessment.objects, 'filter')
def test_database_filter_error_handling(self, mock_filter):
# Create a submission
tim_sub, tim = self._create_student_and_submission("Tim", "Tim's answer")
# Note that we have to define this side effect *after* creating the submission
mock_filter.side_effect = DatabaseError("KABOOM!")
# Try to get the latest staff assessment, handle database errors
with self.assertRaises(StaffAssessmentInternalError) as context_manager:
staff_api.get_latest_staff_assessment(tim_sub["uuid"])
self.assertEqual(
str(context_manager.exception),
(
u"An error occurred while retrieving staff assessments for the submission with UUID {uuid}: {ex}"
).format(uuid=tim_sub["uuid"], ex="KABOOM!")
)
# Try to get staff assessment scores by criteria, handle database errors
with self.assertRaises(StaffAssessmentInternalError) as context_manager:
staff_api.get_assessment_scores_by_criteria(tim_sub["uuid"])
self.assertEqual(
str(context_manager.exception),
u"Error getting staff assessment scores for {}".format(tim_sub["uuid"])
)
@mock.patch.object(Assessment, 'create')
def test_database_create_error_handling(self, mock_create):
mock_create.side_effect = DatabaseError("KABOOM!")
# Try to create a staff assessment, handle database errors
with self.assertRaises(StaffAssessmentInternalError) as context_manager:
staff_assessment = staff_api.create_assessment(
"000000",
"Dumbledore",
OPTIONS_SELECTED_DICT['most']['options'], dict(), "",
RUBRIC,
)
self.assertEqual(
str(context_manager.exception),
u"An error occurred while creating assessment by scorer with ID: {}".format("Dumbledore")
)
@staticmethod
def _create_student_and_submission(student, answer, date=None, override_steps=None):
"""
Helper method to create a student and submission for use in tests.
"""
new_student_item = STUDENT_ITEM.copy()
new_student_item["student_id"] = student
submission = sub_api.create_submission(new_student_item, answer, date)
steps = ['self']
init_params = {}
if override_steps:
steps = override_steps
if 'peer' in steps:
peer_api.on_start(submission["uuid"])
if 'ai' in steps:
init_params['ai'] = {'rubric':RUBRIC, 'algorithm_id':ALGORITHM_ID}
workflow_api.create_workflow(submission["uuid"], steps, init_params)
return submission, new_student_item
...@@ -183,7 +183,8 @@ def get_workflow_for_submission(submission_uuid, assessment_requirements): ...@@ -183,7 +183,8 @@ def get_workflow_for_submission(submission_uuid, assessment_requirements):
def update_from_assessments(submission_uuid, assessment_requirements): def update_from_assessments(submission_uuid, assessment_requirements):
"""Update our workflow status based on the status of peer and self assessments. """
Update our workflow status based on the status of the underlying assessments.
We pass in the `assessment_requirements` each time we make the request We pass in the `assessment_requirements` each time we make the request
because the canonical requirements are stored in the `OpenAssessmentBlock` because the canonical requirements are stored in the `OpenAssessmentBlock`
......
...@@ -34,7 +34,6 @@ logger = logging.getLogger('openassessment.workflow.models') ...@@ -34,7 +34,6 @@ logger = logging.getLogger('openassessment.workflow.models')
DEFAULT_ASSESSMENT_API_DICT = { DEFAULT_ASSESSMENT_API_DICT = {
'peer': 'openassessment.assessment.api.peer', 'peer': 'openassessment.assessment.api.peer',
'self': 'openassessment.assessment.api.self', 'self': 'openassessment.assessment.api.self',
'staff': 'openassessment.assessment.api.staff',
'training': 'openassessment.assessment.api.student_training', 'training': 'openassessment.assessment.api.student_training',
'ai': 'openassessment.assessment.api.ai', 'ai': 'openassessment.assessment.api.ai',
} }
...@@ -43,20 +42,6 @@ ASSESSMENT_API_DICT = getattr( ...@@ -43,20 +42,6 @@ ASSESSMENT_API_DICT = getattr(
DEFAULT_ASSESSMENT_API_DICT DEFAULT_ASSESSMENT_API_DICT
) )
# For now, we use a simple scoring mechanism:
# Once a student has completed all assessments,
# we search assessment APIs
# in priority order until one of the APIs provides a score.
# We then use that score as the student's overall score.
# This Django setting is a list of assessment steps (defined in `settings.ORA2_ASSESSMENTS`)
# in descending priority order.
DEFAULT_ASSESSMENT_SCORE_PRIORITY = ['staff', 'peer', 'self', 'ai']
ASSESSMENT_SCORE_PRIORITY = getattr(
settings, 'ORA2_ASSESSMENT_SCORE_PRIORITY',
DEFAULT_ASSESSMENT_SCORE_PRIORITY
)
class AssessmentWorkflow(TimeStampedModel, StatusModel): class AssessmentWorkflow(TimeStampedModel, StatusModel):
"""Tracks the open-ended assessment status of a student submission. """Tracks the open-ended assessment status of a student submission.
...@@ -84,6 +69,19 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel): ...@@ -84,6 +69,19 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel):
STATUS = Choices(*STATUS_VALUES) # implicit "status" field STATUS = Choices(*STATUS_VALUES) # implicit "status" field
# For now, we use a simple scoring mechanism:
# Once a student has completed all assessments,
# we search assessment APIs
# in priority order until one of the APIs provides a score.
# We then use that score as the student's overall score.
# This Django setting is a list of assessment steps (defined in `settings.ORA2_ASSESSMENTS`)
# in descending priority order.
DEFAULT_ASSESSMENT_SCORE_PRIORITY = ['peer', 'self', 'ai']
ASSESSMENT_SCORE_PRIORITY = getattr(
settings, 'ORA2_ASSESSMENT_SCORE_PRIORITY',
DEFAULT_ASSESSMENT_SCORE_PRIORITY
)
submission_uuid = models.CharField(max_length=36, db_index=True, unique=True) submission_uuid = models.CharField(max_length=36, db_index=True, unique=True)
uuid = UUIDField(version=1, db_index=True, unique=True) uuid = UUIDField(version=1, db_index=True, unique=True)
...@@ -122,6 +120,23 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel): ...@@ -122,6 +120,23 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel):
""" """
submission_dict = sub_api.get_submission_and_student(submission_uuid) submission_dict = sub_api.get_submission_and_student(submission_uuid)
if 'staff' not in step_names:
new_list = ['staff']
new_list.extend(step_names)
step_names = new_list
if 'staff' not in cls.STEPS:
new_list = ['staff']
new_list.extend(cls.STEPS)
cls.STEPS = new_list
cls.STATUS_VALUES = cls.STEPS + cls.STATUSES
cls.STATUS = Choices(*cls.STATUS_VALUES)
if 'staff' not in cls.ASSESSMENT_SCORE_PRIORITY:
new_list = ['staff']
new_list.extend(cls.ASSESSMENT_SCORE_PRIORITY)
cls.ASSESSMENT_SCORE_PRIORITY = new_list
# Create the workflow and step models in the database # Create the workflow and step models in the database
# For now, set the status to waiting; we'll modify it later # For now, set the status to waiting; we'll modify it later
# based on the first step in the workflow. # based on the first step in the workflow.
...@@ -223,7 +238,7 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel): ...@@ -223,7 +238,7 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel):
score dict. score dict.
""" """
score = None score = None
for assessment_step_name in ASSESSMENT_SCORE_PRIORITY: for assessment_step_name in self.ASSESSMENT_SCORE_PRIORITY:
# Check if the problem contains this assessment type # Check if the problem contains this assessment type
assessment_step = step_for_name.get(assessment_step_name) assessment_step = step_for_name.get(assessment_step_name)
...@@ -240,12 +255,15 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel): ...@@ -240,12 +255,15 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel):
else: else:
requirements = assessment_requirements.get(assessment_step_name, {}) requirements = assessment_requirements.get(assessment_step_name, {})
score = get_score_func(self.submission_uuid, requirements) score = get_score_func(self.submission_uuid, requirements)
if score: if assessment_step_name == self.STATUS.staff and score == None:
break if requirements and requirements.get(assessment_step_name, {}).get('required', False):
break # A staff score was not found, and one is required. Return None
continue # A staff score was not found, but it is not required, so try the next type of score
break
return score return score
def update_from_assessments(self, assessment_requirements, force_update_score=False): def update_from_assessments(self, assessment_requirements):
"""Query assessment APIs and change our status if appropriate. """Query assessment APIs and change our status if appropriate.
If the status is done, we do nothing. Once something is done, we never If the status is done, we do nothing. Once something is done, we never
...@@ -287,15 +305,21 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel): ...@@ -287,15 +305,21 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel):
step_for_name = {step.name: step for step in steps} step_for_name = {step.name: step for step in steps}
# If the status is done or cancelled, check if score has changed. new_staff_score = self.get_score(assessment_requirements, {'staff': step_for_name.get('staff', None)})
if self.status == self.STATUS.done: if new_staff_score:
if force_update_score: old_score = self.score
new_score = self.get_score(assessment_requirements, step_for_name) if not old_score or old_score['points_earned'] != new_staff_score['points_earned']:
self.set_score(new_score) self.set_staff_score(new_staff_score)
self.save() self.save()
logger.info(( logger.info((
u"Workflow for submission UUID {uuid} has updated score." u"Workflow for submission UUID {uuid} has updated score using staff assessment."
).format(uuid=self.submission_uuid)) ).format(uuid=self.submission_uuid))
staff_step = step_for_name.get('staff')
staff_step.assessment_completed_at=now()
staff_step.save()
self.status = self.STATUS.done
if self.status == self.STATUS.done:
return return
# Go through each step and update its status. # Go through each step and update its status.
...@@ -340,18 +364,66 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel): ...@@ -340,18 +364,66 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel):
Simple helper function for retrieving all the steps in the given Simple helper function for retrieving all the steps in the given
Workflow. Workflow.
""" """
# A staff step must always be available, to allow for staff overrides
try:
self.steps.get(name=self.STATUS.staff)
except AssessmentWorkflowStep.DoesNotExist:
for step in list(self.steps.all()):
step.order_num += 1
self.steps.add(
AssessmentWorkflowStep(
name=self.STATUS.staff,
order_num=0,
assessment_completed_at=now(),
)
)
# Do not return steps that are not recognized in the AssessmentWorkflow. # Do not return steps that are not recognized in the AssessmentWorkflow.
steps = list(self.steps.filter(name__in=AssessmentWorkflow.STEPS)) steps = list(self.steps.filter(name__in=AssessmentWorkflow.STEPS))
if not steps: if not steps:
# If no steps exist for this AssessmentWorkflow, assume # If no steps exist for this AssessmentWorkflow, assume
# peer -> self for backwards compatibility # peer -> self for backwards compatibility, with an optional staff override
self.steps.add( self.steps.add(
AssessmentWorkflowStep(name=self.STATUS.peer, order_num=0), AssessmentWorkflowStep(name=self.STATUS.staff, order_num=0, assessment_completed_at=now()),
AssessmentWorkflowStep(name=self.STATUS.self, order_num=1) AssessmentWorkflowStep(name=self.STATUS.peer, order_num=1),
AssessmentWorkflowStep(name=self.STATUS.self, order_num=2)
) )
steps = list(self.steps.all()) steps = list(self.steps.all())
return steps return steps
def set_staff_score(self, score, is_override=False, reason=None):
"""
Set a staff score for the workflow.
Allows for staff scores to be set on a submission, with annotations to provide an audit trail if needed.
This method can be used for both required staff grading, and staff overrides.
Args:
score (dict): A dict containing 'points_earned', 'points_possible', and 'staff_id'.
is_override (bool): Optionally True if staff is overriding a previous score.
reason (string): An optional parameter specifying the reason for the staff grade. A default value
will be used in the event that this parameter is not provided.
"""
annotation_type = "staff_defined"
if reason is None:
reason = "A staff member has defined the score for this submission"
sub_dict = sub_api.get_submission_and_student(self.submission_uuid)
sub_api.reset_score(
sub_dict['student_item']['student_id'],
self.course_id,
self.item_id
)
sub_api.set_score(
self.submission_uuid,
score["points_earned"],
score["points_possible"],
annotation_creator = score["staff_id"],
annotation_type = annotation_type,
annotation_reason = reason
)
def set_score(self, score): def set_score(self, score):
""" """
Set a score for the workflow. Set a score for the workflow.
...@@ -364,11 +436,27 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel): ...@@ -364,11 +436,27 @@ class AssessmentWorkflow(TimeStampedModel, StatusModel):
'points_possible'. 'points_possible'.
""" """
sub_api.set_score( if not self.staff_score_exists():
self.submission_uuid, sub_api.set_score(
score["points_earned"], self.submission_uuid,
score["points_possible"] score["points_earned"],
) score["points_possible"]
)
def staff_score_exists(self):
"""
Check if a staff score exists for this submission.
"""
steps = self._get_steps()
step_for_name = {step.name: step for step in steps}
staff_step = step_for_name.get("staff")
if staff_step is not None:
get_latest_func = getattr(staff_step.api(), 'get_latest_assessment', None)
if get_latest_func is not None:
staff_assessment = get_latest_func(self.submission_uuid)
if staff_assessment is not None:
return True
return False
def cancel(self, assessment_requirements): def cancel(self, assessment_requirements):
""" """
...@@ -527,6 +615,9 @@ class AssessmentWorkflowStep(models.Model): ...@@ -527,6 +615,9 @@ class AssessmentWorkflowStep(models.Model):
api_path = getattr( api_path = getattr(
settings, 'ORA2_ASSESSMENTS', DEFAULT_ASSESSMENT_API_DICT settings, 'ORA2_ASSESSMENTS', DEFAULT_ASSESSMENT_API_DICT
).get(self.name) ).get(self.name)
# Staff API should always be available
if self.name == 'staff' and not api_path:
api_path = 'openassessment.assessment.api.staff'
if api_path is not None: if api_path is not None:
try: try:
return importlib.import_module(api_path) return importlib.import_module(api_path)
...@@ -568,8 +659,6 @@ class AssessmentWorkflowStep(models.Model): ...@@ -568,8 +659,6 @@ class AssessmentWorkflowStep(models.Model):
step_changed = True step_changed = True
# Has the step received a score? # Has the step received a score?
# If staff assessment is optional we will mark assessment as complete immediately.
# But if staff comes and assesses later, this date will not be updated.
if (not self.is_assessment_complete() and assessment_finished(submission_uuid, step_reqs)): if (not self.is_assessment_complete() and assessment_finished(submission_uuid, step_reqs)):
self.assessment_completed_at = now() self.assessment_completed_at = now()
step_changed = True step_changed = True
......
""" """
Data Conversion utility methods for handling ORA2 XBlock data transformations. Data Conversion utility methods for handling ORA2 XBlock data transformations and validation.
""" """
import json import json
...@@ -218,3 +218,30 @@ def make_django_template_key(key): ...@@ -218,3 +218,30 @@ def make_django_template_key(key):
basestring basestring
""" """
return key.replace('-', '_') return key.replace('-', '_')
def verify_assessment_parameters(func):
"""
Verify that the wrapped function receives the given parameters.
Used for the staff_assess, self_assess, peer_assess functions and uses their data types.
Args:
func - the function to be modified
Returns:
the modified function
"""
def verify_and_call(instance, data, suffix):
# Validate the request
if 'options_selected' not in data:
return {'success': False, 'msg': instance._('Must provide options selected in the assessment')}
if 'overall_feedback' not in data:
return {'success': False, 'msg': instance._('Must provide overall feedback in the assessment')}
if 'criterion_feedback' not in data:
return {'success': False, 'msg': instance._('Must provide feedback for criteria in the assessment')}
return func(instance, data, suffix)
return verify_and_call
...@@ -138,10 +138,18 @@ DEFAULT_SELF_ASSESSMENT = { ...@@ -138,10 +138,18 @@ DEFAULT_SELF_ASSESSMENT = {
"due": DEFAULT_DUE, "due": DEFAULT_DUE,
} }
DEFAULT_STAFF_ASSESSMENT = {
"name": "staff-assessment",
"start": DEFAULT_START,
"due": DEFAULT_DUE,
"required": False,
}
DEFAULT_ASSESSMENT_MODULES = [ DEFAULT_ASSESSMENT_MODULES = [
DEFAULT_STUDENT_TRAINING, DEFAULT_STUDENT_TRAINING,
DEFAULT_PEER_ASSESSMENT, DEFAULT_PEER_ASSESSMENT,
DEFAULT_SELF_ASSESSMENT, DEFAULT_SELF_ASSESSMENT,
DEFAULT_STAFF_ASSESSMENT,
] ]
DEFAULT_EDITOR_ASSESSMENTS_ORDER = [ DEFAULT_EDITOR_ASSESSMENTS_ORDER = [
......
...@@ -67,7 +67,7 @@ UI_MODELS = { ...@@ -67,7 +67,7 @@ UI_MODELS = {
"navigation_text": "Your assessment of your response", "navigation_text": "Your assessment of your response",
"title": "Assess Your Response" "title": "Assess Your Response"
}, },
"self-assessment": { "staff-assessment": {
"name": "staff-assessment", "name": "staff-assessment",
"class_id": "openassessment__staff-assessment", "class_id": "openassessment__staff-assessment",
"navigation_text": "Staff assessment of your response", "navigation_text": "Staff assessment of your response",
...@@ -92,6 +92,7 @@ VALID_ASSESSMENT_TYPES = [ ...@@ -92,6 +92,7 @@ VALID_ASSESSMENT_TYPES = [
"example-based-assessment", "example-based-assessment",
"peer-assessment", "peer-assessment",
"self-assessment", "self-assessment",
"staff-assessment"
] ]
...@@ -456,9 +457,10 @@ class OpenAssessmentBlock( ...@@ -456,9 +457,10 @@ class OpenAssessmentBlock(
ui_models = [UI_MODELS["submission"]] ui_models = [UI_MODELS["submission"]]
for assessment in self.valid_assessments: for assessment in self.valid_assessments:
if assessment["name"] == "staff-assessment" and assessment["required"] == False: if assessment["name"] == "staff-assessment" and assessment["required"] == False:
# Check if staff have graded the assessment # If we don't have a staff grade, and it's not required, hide
# else # this UI model.
continue if not self.staff_assessment_exists(self.submission_uuid):
continue
ui_model = UI_MODELS.get(assessment["name"]) ui_model = UI_MODELS.get(assessment["name"])
if ui_model: if ui_model:
ui_models.append(dict(assessment, **ui_model)) ui_models.append(dict(assessment, **ui_model))
......
...@@ -12,7 +12,7 @@ from openassessment.workflow.errors import AssessmentWorkflowError ...@@ -12,7 +12,7 @@ from openassessment.workflow.errors import AssessmentWorkflowError
from openassessment.xblock.defaults import DEFAULT_RUBRIC_FEEDBACK_TEXT from openassessment.xblock.defaults import DEFAULT_RUBRIC_FEEDBACK_TEXT
from .data_conversion import create_rubric_dict from .data_conversion import create_rubric_dict
from .resolve_dates import DISTANT_FUTURE from .resolve_dates import DISTANT_FUTURE
from .data_conversion import clean_criterion_feedback, create_submission_dict from .data_conversion import clean_criterion_feedback, create_submission_dict, verify_assessment_parameters
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -31,6 +31,7 @@ class PeerAssessmentMixin(object): ...@@ -31,6 +31,7 @@ class PeerAssessmentMixin(object):
""" """
@XBlock.json_handler @XBlock.json_handler
@verify_assessment_parameters
def peer_assess(self, data, suffix=''): def peer_assess(self, data, suffix=''):
"""Place a peer assessment into OpenAssessment system """Place a peer assessment into OpenAssessment system
...@@ -50,16 +51,6 @@ class PeerAssessmentMixin(object): ...@@ -50,16 +51,6 @@ class PeerAssessmentMixin(object):
and "msg" (unicode) containing additional information if an error occurs. and "msg" (unicode) containing additional information if an error occurs.
""" """
# Validate the request
if 'options_selected' not in data:
return {'success': False, 'msg': self._('Must provide options selected in the assessment')}
if 'overall_feedback' not in data:
return {'success': False, 'msg': self._('Must provide overall feedback in the assessment')}
if 'criterion_feedback' not in data:
return {'success': False, 'msg': self._('Must provide feedback for criteria in the assessment')}
if self.submission_uuid is None: if self.submission_uuid is None:
return {'success': False, 'msg': self._('You must submit a response before you can peer-assess.')} return {'success': False, 'msg': self._('You must submit a response before you can peer-assess.')}
......
...@@ -6,9 +6,9 @@ from webob import Response ...@@ -6,9 +6,9 @@ from webob import Response
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 submissions import api as submission_api from submissions import api as submission_api
from .data_conversion import create_rubric_dict
from .resolve_dates import DISTANT_FUTURE from .resolve_dates import DISTANT_FUTURE
from .data_conversion import clean_criterion_feedback, create_submission_dict from .data_conversion import (clean_criterion_feedback, create_submission_dict,
create_rubric_dict, verify_assessment_parameters)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -102,6 +102,7 @@ class SelfAssessmentMixin(object): ...@@ -102,6 +102,7 @@ class SelfAssessmentMixin(object):
return path, context return path, context
@XBlock.json_handler @XBlock.json_handler
@verify_assessment_parameters
def self_assess(self, data, suffix=''): def self_assess(self, data, suffix=''):
""" """
Create a self-assessment for a submission. Create a self-assessment for a submission.
...@@ -114,14 +115,6 @@ class SelfAssessmentMixin(object): ...@@ -114,14 +115,6 @@ class SelfAssessmentMixin(object):
Dict with keys "success" (bool) indicating success/failure Dict with keys "success" (bool) indicating success/failure
and "msg" (unicode) containing additional information if an error occurs. and "msg" (unicode) containing additional information if an error occurs.
""" """
if 'options_selected' not in data:
return {'success': False, 'msg': self._(u"Missing options_selected key in request")}
if 'overall_feedback' not in data:
return {'success': False, 'msg': self._('Must provide overall feedback in the assessment')}
if 'criterion_feedback' not in data:
return {'success': False, 'msg': self._('Must provide feedback for criteria in the assessment')}
if self.submission_uuid is None: if self.submission_uuid is None:
return {'success': False, 'msg': self._(u"You must submit a response before you can perform a self-assessment.")} return {'success': False, 'msg': self._(u"You must submit a response before you can perform a self-assessment.")}
......
...@@ -7,13 +7,13 @@ from staff_area_mixin import require_course_staff ...@@ -7,13 +7,13 @@ from staff_area_mixin import require_course_staff
from xblock.core import XBlock from xblock.core import XBlock
from openassessment.assessment.api import staff as staff_api from openassessment.assessment.api import staff as staff_api
from openassessment.workflow import api as workflow_api
from openassessment.assessment.errors import ( from openassessment.assessment.errors import (
StaffAssessmentRequestError, StaffAssessmentInternalError StaffAssessmentRequestError, StaffAssessmentInternalError
) )
from .data_conversion import create_rubric_dict from .data_conversion import create_rubric_dict
from .resolve_dates import DISTANT_FUTURE from .data_conversion import clean_criterion_feedback, create_submission_dict, verify_assessment_parameters
from .data_conversion import clean_criterion_feedback, create_submission_dict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -23,21 +23,20 @@ class StaffAssessmentMixin(object): ...@@ -23,21 +23,20 @@ class StaffAssessmentMixin(object):
This mixin is for all staff-assessment related endpoints. This mixin is for all staff-assessment related endpoints.
""" """
def staff_assessment_exists(self, submission_uuid):
"""
Returns True if there exists a staff assessment for the given uuid. False otherwise.
"""
return staff_api.get_latest_staff_assessment(submission_uuid) is not None
@XBlock.json_handler @XBlock.json_handler
@require_course_staff("STUDENT_INFO") @require_course_staff("STUDENT_INFO")
@verify_assessment_parameters
def staff_assess(self, data, suffix=''): def staff_assess(self, data, suffix=''):
""" """
Create a staff assessment from a staff submission. Create a staff assessment from a staff submission.
""" """
if 'options_selected' not in data:
return {'success': False, 'msg': self._(u"Missing options_selected key in request")}
if 'overall_feedback' not in data:
return {'success': False, 'msg': self._('Must provide overall feedback in the assessment')}
if 'criterion_feedback' not in data:
return {'success': False, 'msg': self._('Must provide feedback for criteria in the assessment')}
if 'submission_uuid' not in data: if 'submission_uuid' not in data:
return {'success': False, 'msg': self._(u"Missing the submission id of the submission being assessed.")} return {'success': False, 'msg': self._(u"Missing the submission id of the submission being assessed.")}
...@@ -51,11 +50,12 @@ class StaffAssessmentMixin(object): ...@@ -51,11 +50,12 @@ class StaffAssessmentMixin(object):
create_rubric_dict(self.prompts, self.rubric_criteria_with_labels) create_rubric_dict(self.prompts, self.rubric_criteria_with_labels)
) )
self.publish_assessment_event("openassessmentblock.staff_assessment", assessment) self.publish_assessment_event("openassessmentblock.staff_assessment", assessment)
workflow_api.update_from_assessments(assessment["submission_uuid"], {})
except StaffAssessmentRequestError: except StaffAssessmentRequestError:
logger.warning( logger.warning(
u"An error occurred while submitting a staff assessment " u"An error occurred while submitting a staff assessment "
u"for the submission {}".format(self.submission_uuid), u"for the submission {}".format(data['submission_uuid']),
exc_info=True exc_info=True
) )
msg = self._(u"Your staff assessment could not be submitted.") msg = self._(u"Your staff assessment could not be submitted.")
...@@ -63,48 +63,9 @@ class StaffAssessmentMixin(object): ...@@ -63,48 +63,9 @@ class StaffAssessmentMixin(object):
except StaffAssessmentInternalError: except StaffAssessmentInternalError:
logger.exception( logger.exception(
u"An error occurred while submitting a staff assessment " u"An error occurred while submitting a staff assessment "
u"for the submission {}".format(self.submission_uuid), u"for the submission {}".format(data['submission_uuid']),
) )
msg = self._(u"Your staff assessment could not be submitted.") msg = self._(u"Your staff assessment could not be submitted.")
return {'success': False, 'msg': msg} return {'success': False, 'msg': msg}
else: else:
return {'success': True, 'msg': u""} return {'success': True, 'msg': u""}
@XBlock.handler
@require_course_staff("STUDENT_INFO")
def render_staff_assessment(self, data, suffix=''):
"""
Render the staff assessment for the given student.
"""
try:
submission_uuid = data.get("submission_uuid")
path, context = self.self_path_and_context(submission_uuid)
except:
msg = u"Could not retrieve staff assessment for submission {}".format(self.submission_uuid)
logger.exception(msg)
return self.render_error(self._(u"An unexpected error occurred."))
else:
return self.render_assessment(path, context)
def staff_path_and_context(self, submission_uuid):
"""
Retrieve the correct template path and template context for the handler to render.
Args:
submission_uuid (str) -
"""
#TODO: add in the workflow for staff grading instead of assuming it's allowed.
submission = submission_api.get_submission(self.submission_uuid)
context = {'allow_latex': self.allow_latex}
context["rubric_criteria"] = self.rubric_criteria_with_labels
context["estimated_time"] = "20 minutes" # TODO: Need to configure this.
context["self_submission"] = create_submission_dict(submission, self.prompts)
# Determine if file upload is supported for this XBlock.
context["allow_file_upload"] = self.allow_file_upload
context['self_file_url'] = self.get_download_url_from_submission(submission)
#TODO: Replace with the staff assessment template when it's been built.
path = 'openassessmentblock/self/oa_self_assessment.html'
return path, context
if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}if(typeof window.ngetgext==="undefined"){window.ngettext=function(singular_text,plural_text,n){if(n>1){return plural_text}else{return singular_text}}}if(typeof window.Logger==="undefined"){window.Logger={log:function(){}}}if(typeof window.MathJax==="undefined"){window.MathJax={Hub:{Typeset:function(){},Queue:function(){}}}}if(typeof OpenAssessment.Server==="undefined"||!OpenAssessment.Server){OpenAssessment.Server=function(runtime,element){this.runtime=runtime;this.element=element};var jsonContentType="application/json; charset=utf-8";OpenAssessment.Server.prototype={url:function(handler){return this.runtime.handlerUrl(this.element,handler)},render:function(component){var view=this;var url=this.url("render_"+component);return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html"}).done(function(data){defer.resolveWith(view,[data])}).fail(function(){defer.rejectWith(view,[gettext("This section could not be loaded.")])})}).promise()},renderLatex:function(element){element.filter(".allow--latex").each(function(){MathJax.Hub.Queue(["Typeset",MathJax.Hub,this])})},renderContinuedPeer:function(){var view=this;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(view,[data])}).fail(function(){defer.rejectWith(view,[gettext("This section could not be loaded.")])})}).promise()},studentInfo:function(student_username){var url=this.url("render_student_info");return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html",data:{student_username:student_username}}).done(function(data){defer.resolveWith(this,[data])}).fail(function(){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}),contentType:jsonContentType}).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(){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}),contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This feedback could not be submitted.")])})}).promise()},peerAssess:function(optionsSelected,criterionFeedback,overallFeedback,uuid){var url=this.url("peer_assess");var payload=JSON.stringify({options_selected:optionsSelected,criterion_feedback:criterionFeedback,overall_feedback:overallFeedback,submission_uuid:uuid});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.corrections])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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:'""',contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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:'""',contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("One or more rescheduling tasks failed.")])})})},updateEditorContext:function(kwargs){var url=this.url("update_editor_context");var payload=JSON.stringify({prompts:kwargs.prompts,feedback_prompt:kwargs.feedbackPrompt,feedback_default_text:kwargs.feedback_default_text,title:kwargs.title,submission_start:kwargs.submissionStart,submission_due:kwargs.submissionDue,criteria:kwargs.criteria,assessments:kwargs.assessments,editor_assessments_order:kwargs.editorAssessmentsOrder,file_upload_type:kwargs.fileUploadType,white_listed_file_types:kwargs.fileTypeWhiteList,allow_latex:kwargs.latexEnabled,leaderboard_show:kwargs.leaderboardNum});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.is_released])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("The server could not be contacted.")])})}).promise()},getUploadUrl:function(contentType,filename){var url=this.url("upload_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({contentType:contentType,filename:filename}),contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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({}),contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("Could not retrieve download url.")])})}).promise()},cancelSubmission:function(submissionUUID,comments){var url=this.url("cancel_submission");var payload=JSON.stringify({submission_uuid:submissionUUID,comments:comments});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("The submission could not be removed from the grading pool.")])})}).promise()}}}if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}if(typeof window.ngetgext==="undefined"){window.ngettext=function(singular_text,plural_text,n){if(n>1){return plural_text}else{return singular_text}}}if(typeof window.Logger==="undefined"){window.Logger={log:function(){}}}if(typeof window.MathJax==="undefined"){window.MathJax={Hub:{Typeset:function(){},Queue:function(){}}}}OpenAssessment.Container=function(ContainerItem,kwargs){this.containerElement=kwargs.containerElement;this.templateElement=kwargs.templateElement;this.addButtonElement=kwargs.addButtonElement;this.removeButtonClass=kwargs.removeButtonClass;this.containerItemClass=kwargs.containerItemClass;this.notifier=kwargs.notifier;this.addRemoveEnabled=typeof kwargs.addRemoveEnabled==="undefined"||kwargs.addRemoveEnabled;var container=this;this.createContainerItem=function(element){return new ContainerItem(element,container.notifier)}};OpenAssessment.Container.prototype={addEventListeners:function(){var container=this;if(this.addRemoveEnabled){$(this.addButtonElement).click($.proxy(this.add,this));$("."+this.removeButtonClass,this.containerElement).click(function(eventData){var item=container.createContainerItem(eventData.target);container.remove(item)})}else{$(this.addButtonElement).addClass("is--disabled");$("."+this.removeButtonClass,this.containerElement).addClass("is--disabled")}$("."+this.containerItemClass,this.containerElement).each(function(index,element){var item=container.createContainerItem(element);item.addEventListeners()})},add:function(){$(this.templateElement).children().first().clone().removeAttr("id").toggleClass("is--hidden",false).toggleClass(this.containerItemClass,true).appendTo($(this.containerElement));var container=this;var containerItem=$("."+this.containerItemClass,this.containerElement).last();if(this.addRemoveEnabled){containerItem.find("."+this.removeButtonClass).click(function(eventData){var containerItem=container.createContainerItem(eventData.target);container.remove(containerItem)})}else{containerItem.find("."+this.removeButtonClass).addClass("is--disabled")}var handlerItem=container.createContainerItem(containerItem);handlerItem.addEventListeners();handlerItem.addHandler()},remove:function(item){var itemElement=$(item.element).closest("."+this.containerItemClass);var containerItem=this.createContainerItem(itemElement);containerItem.removeHandler();itemElement.remove()},getItemValues:function(){var values=[];var container=this;$("."+this.containerItemClass,this.containerElement).each(function(index,element){var containerItem=container.createContainerItem(element);var fieldValues=containerItem.getFieldValues();values.push(fieldValues)});return values},getItem:function(index){var element=$("."+this.containerItemClass,this.containerElement).get(index);return element!==undefined?this.createContainerItem(element):null},getAllItems:function(){var container=this;return $("."+this.containerItemClass,this.containerElement).map(function(){return container.createContainerItem(this)})}};OpenAssessment.ItemUtilities={createUniqueName:function(selector,nameAttribute){var index=0;while(index<=selector.length){if(selector.parent().find("*["+nameAttribute+"='"+index+"']").length===0){return index.toString()}index++}return index.toString()},refreshOptionString:function(element){var points=$(element).attr("data-points");var label=$(element).attr("data-label");var name=$(element).val();if(label===""){label=gettext("Unnamed Option")}var singularString=label+" - "+points+" point";var multipleString=label+" - "+points+" points";var finalLabel="";if(name===""){finalLabel=gettext("Not Selected")}else if(isNaN(points)){finalLabel=label}else{finalLabel=ngettext(singularString,multipleString,points)}$(element).text(finalLabel)}};OpenAssessment.Prompt=function(element,notifier){this.element=element;this.notifier=notifier};OpenAssessment.Prompt.prototype={getFieldValues:function(){var fields={description:this.description()};return fields},description:function(text){var sel=$(".openassessment_prompt_description",this.element);return OpenAssessment.Fields.stringField(sel,text)},addEventListeners:function(){},addHandler:function(){this.notifier.notificationFired("promptAdd",{index:this.element.index()})},removeHandler:function(){this.notifier.notificationFired("promptRemove",{index:this.element.index()})},updateHandler:function(){},validate:function(){return true},validationErrors:function(){return[]},clearValidationErrors:function(){}};OpenAssessment.RubricOption=function(element,notifier){this.element=element;this.notifier=notifier;this.pointsField=new OpenAssessment.IntField($(".openassessment_criterion_option_points",this.element),{min:0,max:999})};OpenAssessment.RubricOption.prototype={addEventListeners:function(){$(this.element).focusout($.proxy(this.updateHandler,this))},getFieldValues:function(){var fields={label:this.label(),points:this.points(),explanation:this.explanation()};var nameString=OpenAssessment.Fields.stringField($(".openassessment_criterion_option_name",this.element));if(nameString!==""){fields.name=nameString}return fields},label:function(label){var sel=$(".openassessment_criterion_option_label",this.element);return OpenAssessment.Fields.stringField(sel,label)},points:function(points){if(points!==undefined){this.pointsField.set(points)}return this.pointsField.get()},explanation:function(explanation){var sel=$(".openassessment_criterion_option_explanation",this.element);return OpenAssessment.Fields.stringField(sel,explanation)},addHandler:function(){var criterionElement=$(this.element).closest(".openassessment_criterion");var criterionName=$(criterionElement).data("criterion");var criterionLabel=$(".openassessment_criterion_label",criterionElement).val();var options=$(".openassessment_criterion_option",this.element.parent());var name=OpenAssessment.ItemUtilities.createUniqueName(options,"data-option");$(this.element).attr("data-criterion",criterionName).attr("data-option",name);$(".openassessment_criterion_option_name",this.element).attr("value",name);var fields=this.getFieldValues();this.notifier.notificationFired("optionAdd",{criterionName:criterionName,criterionLabel:criterionLabel,name:name,label:fields.label,points:fields.points})},removeHandler:function(){var criterionName=$(this.element).data("criterion");var optionName=$(this.element).data("option");this.notifier.notificationFired("optionRemove",{criterionName:criterionName,name:optionName})},updateHandler:function(){var fields=this.getFieldValues();var criterionName=$(this.element).data("criterion");var optionName=$(this.element).data("option");var optionLabel=fields.label;var optionPoints=fields.points;this.notifier.notificationFired("optionUpdated",{criterionName:criterionName,name:optionName,label:optionLabel,points:optionPoints})},validate:function(){return this.pointsField.validate()},validationErrors:function(){var hasError=this.pointsField.validationErrors().length>0;return hasError?["Option points are invalid"]:[]},clearValidationErrors:function(){this.pointsField.clearValidationErrors()}};OpenAssessment.RubricCriterion=function(element,notifier){this.element=element;this.notifier=notifier;this.labelSel=$(".openassessment_criterion_label",this.element);this.promptSel=$(".openassessment_criterion_prompt",this.element);this.optionContainer=new OpenAssessment.Container(OpenAssessment.RubricOption,{containerElement:$(".openassessment_criterion_option_list",this.element).get(0),templateElement:$("#openassessment_option_template").get(0),addButtonElement:$(".openassessment_criterion_add_option",this.element).get(0),removeButtonClass:"openassessment_criterion_option_remove_button",containerItemClass:"openassessment_criterion_option",notifier:this.notifier})};OpenAssessment.RubricCriterion.prototype={addEventListeners:function(){this.optionContainer.addEventListeners();$(this.element).focusout($.proxy(this.updateHandler,this))},getFieldValues:function(){var fields={label:this.label(),prompt:this.prompt(),feedback:this.feedback(),options:this.optionContainer.getItemValues()};var nameString=OpenAssessment.Fields.stringField($(".openassessment_criterion_name",this.element));if(nameString!==""){fields.name=nameString}return fields},label:function(label){return OpenAssessment.Fields.stringField(this.labelSel,label)},prompt:function(prompt){return OpenAssessment.Fields.stringField(this.promptSel,prompt)},feedback:function(){return $(".openassessment_criterion_feedback",this.element).val()},addOption:function(){this.optionContainer.add()},addHandler:function(){var criteria=$(".openassessment_criterion",this.element.parent());var name=OpenAssessment.ItemUtilities.createUniqueName(criteria,"data-criterion");$(this.element).attr("data-criterion",name);$(".openassessment_criterion_name",this.element).attr("value",name)},removeHandler:function(){var criterionName=$(this.element).data("criterion");this.notifier.notificationFired("criterionRemove",{criterionName:criterionName})},updateHandler:function(){var fields=this.getFieldValues();var criterionName=fields.name;var criterionLabel=fields.label;this.notifier.notificationFired("criterionUpdated",{criterionName:criterionName,criterionLabel:criterionLabel})},validate:function(){var isValid=this.prompt()!=="";if(!isValid){this.promptSel.addClass("openassessment_highlighted_field")}$.each(this.optionContainer.getAllItems(),function(){isValid=this.validate()&&isValid});return isValid},validationErrors:function(){var errors=[];if(this.promptSel.hasClass("openassessment_highlighted_field")){errors.push("Criterion prompt is invalid.")}$.each(this.optionContainer.getAllItems(),function(){errors=errors.concat(this.validationErrors())});return errors},clearValidationErrors:function(){this.promptSel.removeClass("openassessment_highlighted_field");$.each(this.optionContainer.getAllItems(),function(){this.clearValidationErrors()})}};OpenAssessment.TrainingExample=function(element){this.element=element;this.criteria=$(".openassessment_training_example_criterion_option",this.element);this.answer=$(".openassessment_training_example_essay_part textarea",this.element)};OpenAssessment.TrainingExample.prototype={getFieldValues:function(){var optionsSelected=this.criteria.map(function(){return{criterion:$(this).data("criterion"),option:$(this).prop("value")}}).get();return{answer:this.answer.map(function(){return $(this).prop("value")}).get(),options_selected:optionsSelected}},addHandler:function(){$(".openassessment_training_example_criterion_option",this.element).each(function(){$("option",this).each(function(){OpenAssessment.ItemUtilities.refreshOptionString($(this))})})},addEventListeners:function(){},removeHandler:function(){},updateHandler:function(){},validate:function(){var isValid=true;this.criteria.each(function(){var isOptionValid=$(this).prop("value")!=="";isValid=isOptionValid&&isValid;if(!isOptionValid){$(this).addClass("openassessment_highlighted_field")}});return isValid},validationErrors:function(){var errors=[];this.criteria.each(function(){var hasError=$(this).hasClass("openassessment_highlighted_field");if(hasError){errors.push("Student training example is invalid.")}});return errors},clearValidationErrors:function(){this.criteria.each(function(){$(this).removeClass("openassessment_highlighted_field")})}};OpenAssessment.StudioView=function(runtime,element,server,data){this.element=element;this.runtime=runtime;this.server=server;this.data=data;this.fixModalHeight();this.initializeTabs();this.alert=(new OpenAssessment.ValidationAlert).install();var studentTrainingListener=new OpenAssessment.StudentTrainingListener;this.promptsView=new OpenAssessment.EditPromptsView($("#oa_prompts_editor_wrapper",this.element).get(0),new OpenAssessment.Notifier([studentTrainingListener]));var studentTrainingView=new OpenAssessment.EditStudentTrainingView($("#oa_student_training_editor",this.element).get(0));var peerAssessmentView=new OpenAssessment.EditPeerAssessmentView($("#oa_peer_assessment_editor",this.element).get(0));var selfAssessmentView=new OpenAssessment.EditSelfAssessmentView($("#oa_self_assessment_editor",this.element).get(0));var exampleBasedAssessmentView=new OpenAssessment.EditExampleBasedAssessmentView($("#oa_ai_assessment_editor",this.element).get(0));var assessmentLookupDictionary={};assessmentLookupDictionary[studentTrainingView.getID()]=studentTrainingView;assessmentLookupDictionary[peerAssessmentView.getID()]=peerAssessmentView;assessmentLookupDictionary[selfAssessmentView.getID()]=selfAssessmentView;assessmentLookupDictionary[exampleBasedAssessmentView.getID()]=exampleBasedAssessmentView;this.settingsView=new OpenAssessment.EditSettingsView($("#oa_basic_settings_editor",this.element).get(0),assessmentLookupDictionary,data);this.rubricView=new OpenAssessment.EditRubricView($("#oa_rubric_editor_wrapper",this.element).get(0),new OpenAssessment.Notifier([studentTrainingListener]));$(".openassessment_save_button",this.element).click($.proxy(this.save,this));$(".openassessment_cancel_button",this.element).click($.proxy(this.cancel,this))};OpenAssessment.StudioView.prototype={fixModalHeight:function(){$(this.element).addClass("openassessment_full_height").parentsUntil(".modal-window").addClass("openassessment_full_height");$(this.element).closest(".modal-window").addClass("openassessment_modal_window")},initializeTabs:function(){if(typeof OpenAssessment.lastOpenEditingTab==="undefined"){OpenAssessment.lastOpenEditingTab=2}$(".openassessment_editor_content_and_tabs",this.element).tabs({active:OpenAssessment.lastOpenEditingTab})},saveTabState:function(){var tabElement=$(".openassessment_editor_content_and_tabs",this.element);OpenAssessment.lastOpenEditingTab=tabElement.tabs("option","active")},save:function(){var view=this;this.saveTabState();this.clearValidationErrors();if(!this.validate()){this.alert.setMessage(gettext("Couldn't Save This Assignment"),gettext("Please correct the outlined fields.")).show()}else{this.alert.hide();this.server.checkReleased().done(function(isReleased){if(isReleased){view.confirmPostReleaseUpdate($.proxy(view.updateEditorContext,view))}else{view.updateEditorContext()}}).fail(function(errMsg){view.showError(errMsg)})}},confirmPostReleaseUpdate:function(onConfirm){var msg=gettext("This problem has already been released. Any changes will apply only to future assessments.");if(confirm(msg)){onConfirm()}},updateEditorContext:function(){this.runtime.notify("save",{state:"start"});var view=this;this.server.updateEditorContext({prompts:view.promptsView.promptsDefinition(),feedbackPrompt:view.rubricView.feedbackPrompt(),feedback_default_text:view.rubricView.feedback_default_text(),criteria:view.rubricView.criteriaDefinition(),title:view.settingsView.displayName(),submissionStart:view.settingsView.submissionStart(),submissionDue:view.settingsView.submissionDue(),assessments:view.settingsView.assessmentsDescription(),fileUploadType:view.settingsView.fileUploadType(),fileTypeWhiteList:view.settingsView.fileTypeWhiteList(),latexEnabled:view.settingsView.latexEnabled(),leaderboardNum:view.settingsView.leaderboardNum(),editorAssessmentsOrder:view.settingsView.editorAssessmentsOrder()}).done(function(){view.runtime.notify("save",{state:"end"})}).fail(function(msg){view.showError(msg)})},cancel:function(){this.saveTabState();this.runtime.notify("cancel",{})},showError:function(errorMsg){this.runtime.notify("error",{msg:errorMsg})},validate:function(){var settingsValid=this.settingsView.validate();var rubricValid=this.rubricView.validate();var promptsValid=this.promptsView.validate();return settingsValid&&rubricValid&&promptsValid},validationErrors:function(){return this.settingsView.validationErrors().concat(this.rubricView.validationErrors().concat(this.promptsView.validationErrors()))},clearValidationErrors:function(){this.settingsView.clearValidationErrors();this.rubricView.clearValidationErrors();this.promptsView.clearValidationErrors()}};function OpenAssessmentEditor(runtime,element,data){var server=new OpenAssessment.Server(runtime,element);new OpenAssessment.StudioView(runtime,element,server,data)}OpenAssessment.EditPeerAssessmentView=function(element){this.element=element;this.name="peer-assessment";this.mustGradeField=new OpenAssessment.IntField($("#peer_assessment_must_grade",this.element),{min:0,max:99});this.mustBeGradedByField=new OpenAssessment.IntField($("#peer_assessment_graded_by",this.element),{min:0,max:99});new OpenAssessment.ToggleControl($("#include_peer_assessment",this.element),$("#peer_assessment_settings_editor",this.element),$("#peer_assessment_description_closed",this.element),new OpenAssessment.Notifier([new OpenAssessment.AssessmentToggleListener])).install();this.startDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#peer_assessment_start_date","#peer_assessment_start_time").install();this.dueDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#peer_assessment_due_date","#peer_assessment_due_time").install()};OpenAssessment.EditPeerAssessmentView.prototype={description:function(){return{must_grade:this.mustGradeNum(),must_be_graded_by:this.mustBeGradedByNum(),start:this.startDatetime(),due:this.dueDatetime()}},isEnabled:function(isEnabled){var sel=$("#include_peer_assessment",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},toggleEnabled:function(){$("#include_peer_assessment",this.element).click()},mustGradeNum:function(num){if(num!==undefined){this.mustGradeField.set(num)}return this.mustGradeField.get()},mustBeGradedByNum:function(num){if(num!==undefined){this.mustBeGradedByField.set(num)}return this.mustBeGradedByField.get()},startDatetime:function(dateString,timeString){return this.startDatetimeControl.datetime(dateString,timeString)},dueDatetime:function(dateString,timeString){return this.dueDatetimeControl.datetime(dateString,timeString)},getID:function(){return $(this.element).attr("id")},validate:function(){var startValid=this.startDatetimeControl.validate();var dueValid=this.dueDatetimeControl.validate();var mustGradeValid=this.mustGradeField.validate();var mustBeGradedByValid=this.mustBeGradedByField.validate();return startValid&&dueValid&&mustGradeValid&&mustBeGradedByValid},validationErrors:function(){var errors=[];if(this.startDatetimeControl.validationErrors().length>0){errors.push("Peer assessment start is invalid")}if(this.dueDatetimeControl.validationErrors().length>0){errors.push("Peer assessment due is invalid")}if(this.mustGradeField.validationErrors().length>0){errors.push("Peer assessment must grade is invalid")}if(this.mustBeGradedByField.validationErrors().length>0){errors.push("Peer assessment must be graded by is invalid")}return errors},clearValidationErrors:function(){this.startDatetimeControl.clearValidationErrors();this.dueDatetimeControl.clearValidationErrors();this.mustGradeField.clearValidationErrors();this.mustBeGradedByField.clearValidationErrors()}};OpenAssessment.EditSelfAssessmentView=function(element){this.element=element;this.name="self-assessment";new OpenAssessment.ToggleControl($("#include_self_assessment",this.element),$("#self_assessment_settings_editor",this.element),$("#self_assessment_description_closed",this.element),new OpenAssessment.Notifier([new OpenAssessment.AssessmentToggleListener])).install();this.startDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#self_assessment_start_date","#self_assessment_start_time").install();this.dueDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#self_assessment_due_date","#self_assessment_due_time").install()};OpenAssessment.EditSelfAssessmentView.prototype={description:function(){return{start:this.startDatetime(),due:this.dueDatetime()}},isEnabled:function(isEnabled){var sel=$("#include_self_assessment",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},toggleEnabled:function(){$("#include_self_assessment",this.element).click()},startDatetime:function(dateString,timeString){return this.startDatetimeControl.datetime(dateString,timeString)},dueDatetime:function(dateString,timeString){return this.dueDatetimeControl.datetime(dateString,timeString)},getID:function(){return $(this.element).attr("id")},validate:function(){var startValid=this.startDatetimeControl.validate();var dueValid=this.dueDatetimeControl.validate();return startValid&&dueValid},validationErrors:function(){var errors=[];if(this.startDatetimeControl.validationErrors().length>0){errors.push("Self assessment start is invalid")}if(this.dueDatetimeControl.validationErrors().length>0){errors.push("Self assessment due is invalid")}return errors},clearValidationErrors:function(){this.startDatetimeControl.clearValidationErrors();this.dueDatetimeControl.clearValidationErrors()}};OpenAssessment.EditStudentTrainingView=function(element){this.element=element;this.name="student-training";new OpenAssessment.ToggleControl($("#include_student_training",this.element),$("#student_training_settings_editor",this.element),$("#student_training_description_closed",this.element),new OpenAssessment.Notifier([new OpenAssessment.AssessmentToggleListener])).install();this.exampleContainer=new OpenAssessment.Container(OpenAssessment.TrainingExample,{containerElement:$("#openassessment_training_example_list",this.element).get(0),templateElement:$("#openassessment_training_example_template",this.element).get(0),addButtonElement:$(".openassessment_add_training_example",this.element).get(0),removeButtonClass:"openassessment_training_example_remove",containerItemClass:"openassessment_training_example"});this.exampleContainer.addEventListeners()};OpenAssessment.EditStudentTrainingView.prototype={description:function(){return{examples:this.exampleContainer.getItemValues()}},isEnabled:function(isEnabled){var sel=$("#include_student_training",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},toggleEnabled:function(){$("#include_student_training",this.element).click()},getID:function(){return $(this.element).attr("id")},validate:function(){var isValid=true;$.each(this.exampleContainer.getAllItems(),function(){isValid=this.validate()&&isValid});return isValid},validationErrors:function(){var errors=[];$.each(this.exampleContainer.getAllItems(),function(){errors=errors.concat(this.validationErrors())});return errors},clearValidationErrors:function(){$.each(this.exampleContainer.getAllItems(),function(){this.clearValidationErrors()})},addTrainingExample:function(){this.exampleContainer.add()}};OpenAssessment.EditExampleBasedAssessmentView=function(element){this.element=element;this.name="example-based-assessment";new OpenAssessment.ToggleControl($("#include_ai_assessment",this.element),$("#ai_assessment_settings_editor",this.element),$("#ai_assessment_description_closed",this.element),new OpenAssessment.Notifier([new OpenAssessment.AssessmentToggleListener])).install()};OpenAssessment.EditExampleBasedAssessmentView.prototype={description:function(){return{examples_xml:this.exampleDefinitions()}},isEnabled:function(isEnabled){var sel=$("#include_ai_assessment",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},toggleEnabled:function(){$("#include_ai_assessment",this.element).click()},exampleDefinitions:function(xml){var sel=$("#ai_training_examples",this.element);return OpenAssessment.Fields.stringField(sel,xml)},getID:function(){return $(this.element).attr("id")},validate:function(){return true},validationErrors:function(){return[]},clearValidationErrors:function(){}};OpenAssessment.Fields={stringField:function(sel,value){if(value!==undefined){sel.val(value) if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}if(typeof window.ngetgext==="undefined"){window.ngettext=function(singular_text,plural_text,n){if(n>1){return plural_text}else{return singular_text}}}if(typeof window.Logger==="undefined"){window.Logger={log:function(){}}}if(typeof window.MathJax==="undefined"){window.MathJax={Hub:{Typeset:function(){},Queue:function(){}}}}if(typeof OpenAssessment.Server==="undefined"||!OpenAssessment.Server){OpenAssessment.Server=function(runtime,element){this.runtime=runtime;this.element=element};var jsonContentType="application/json; charset=utf-8";OpenAssessment.Server.prototype={url:function(handler){return this.runtime.handlerUrl(this.element,handler)},render:function(component){var view=this;var url=this.url("render_"+component);return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html"}).done(function(data){defer.resolveWith(view,[data])}).fail(function(){defer.rejectWith(view,[gettext("This section could not be loaded.")])})}).promise()},renderLatex:function(element){element.filter(".allow--latex").each(function(){MathJax.Hub.Queue(["Typeset",MathJax.Hub,this])})},renderContinuedPeer:function(){var view=this;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(view,[data])}).fail(function(){defer.rejectWith(view,[gettext("This section could not be loaded.")])})}).promise()},studentInfo:function(student_username){var url=this.url("render_student_info");return $.Deferred(function(defer){$.ajax({url:url,type:"POST",dataType:"html",data:{student_username:student_username}}).done(function(data){defer.resolveWith(this,[data])}).fail(function(){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}),contentType:jsonContentType}).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(){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}),contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("This feedback could not be submitted.")])})}).promise()},peerAssess:function(optionsSelected,criterionFeedback,overallFeedback,uuid){var url=this.url("peer_assess");var payload=JSON.stringify({options_selected:optionsSelected,criterion_feedback:criterionFeedback,overall_feedback:overallFeedback,submission_uuid:uuid});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.corrections])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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:'""',contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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:'""',contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("One or more rescheduling tasks failed.")])})})},updateEditorContext:function(kwargs){var url=this.url("update_editor_context");var payload=JSON.stringify({prompts:kwargs.prompts,feedback_prompt:kwargs.feedbackPrompt,feedback_default_text:kwargs.feedback_default_text,title:kwargs.title,submission_start:kwargs.submissionStart,submission_due:kwargs.submissionDue,criteria:kwargs.criteria,assessments:kwargs.assessments,editor_assessments_order:kwargs.editorAssessmentsOrder,file_upload_type:kwargs.fileUploadType,white_listed_file_types:kwargs.fileTypeWhiteList,allow_latex:kwargs.latexEnabled,leaderboard_show:kwargs.leaderboardNum});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve()}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.is_released])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("The server could not be contacted.")])})}).promise()},getUploadUrl:function(contentType,filename){var url=this.url("upload_url");return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:JSON.stringify({contentType:contentType,filename:filename}),contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(){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({}),contentType:jsonContentType}).done(function(data){if(data.success){defer.resolve(data.url)}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("Could not retrieve download url.")])})}).promise()},cancelSubmission:function(submissionUUID,comments){var url=this.url("cancel_submission");var payload=JSON.stringify({submission_uuid:submissionUUID,comments:comments});return $.Deferred(function(defer){$.ajax({type:"POST",url:url,data:payload,contentType:jsonContentType}).done(function(data){if(data.success){defer.resolveWith(this,[data.msg])}else{defer.rejectWith(this,[data.msg])}}).fail(function(){defer.rejectWith(this,[gettext("The submission could not be removed from the grading pool.")])})}).promise()}}}if(typeof OpenAssessment=="undefined"||!OpenAssessment){OpenAssessment={}}if(typeof window.gettext==="undefined"){window.gettext=function(text){return text}}if(typeof window.ngetgext==="undefined"){window.ngettext=function(singular_text,plural_text,n){if(n>1){return plural_text}else{return singular_text}}}if(typeof window.Logger==="undefined"){window.Logger={log:function(){}}}if(typeof window.MathJax==="undefined"){window.MathJax={Hub:{Typeset:function(){},Queue:function(){}}}}OpenAssessment.Container=function(ContainerItem,kwargs){this.containerElement=kwargs.containerElement;this.templateElement=kwargs.templateElement;this.addButtonElement=kwargs.addButtonElement;this.removeButtonClass=kwargs.removeButtonClass;this.containerItemClass=kwargs.containerItemClass;this.notifier=kwargs.notifier;this.addRemoveEnabled=typeof kwargs.addRemoveEnabled==="undefined"||kwargs.addRemoveEnabled;var container=this;this.createContainerItem=function(element){return new ContainerItem(element,container.notifier)}};OpenAssessment.Container.prototype={addEventListeners:function(){var container=this;if(this.addRemoveEnabled){$(this.addButtonElement).click($.proxy(this.add,this));$("."+this.removeButtonClass,this.containerElement).click(function(eventData){var item=container.createContainerItem(eventData.target);container.remove(item)})}else{$(this.addButtonElement).addClass("is--disabled");$("."+this.removeButtonClass,this.containerElement).addClass("is--disabled")}$("."+this.containerItemClass,this.containerElement).each(function(index,element){var item=container.createContainerItem(element);item.addEventListeners()})},add:function(){$(this.templateElement).children().first().clone().removeAttr("id").toggleClass("is--hidden",false).toggleClass(this.containerItemClass,true).appendTo($(this.containerElement));var container=this;var containerItem=$("."+this.containerItemClass,this.containerElement).last();if(this.addRemoveEnabled){containerItem.find("."+this.removeButtonClass).click(function(eventData){var containerItem=container.createContainerItem(eventData.target);container.remove(containerItem)})}else{containerItem.find("."+this.removeButtonClass).addClass("is--disabled")}var handlerItem=container.createContainerItem(containerItem);handlerItem.addEventListeners();handlerItem.addHandler()},remove:function(item){var itemElement=$(item.element).closest("."+this.containerItemClass);var containerItem=this.createContainerItem(itemElement);containerItem.removeHandler();itemElement.remove()},getItemValues:function(){var values=[];var container=this;$("."+this.containerItemClass,this.containerElement).each(function(index,element){var containerItem=container.createContainerItem(element);var fieldValues=containerItem.getFieldValues();values.push(fieldValues)});return values},getItem:function(index){var element=$("."+this.containerItemClass,this.containerElement).get(index);return element!==undefined?this.createContainerItem(element):null},getAllItems:function(){var container=this;return $("."+this.containerItemClass,this.containerElement).map(function(){return container.createContainerItem(this)})}};OpenAssessment.ItemUtilities={createUniqueName:function(selector,nameAttribute){var index=0;while(index<=selector.length){if(selector.parent().find("*["+nameAttribute+"='"+index+"']").length===0){return index.toString()}index++}return index.toString()},refreshOptionString:function(element){var points=$(element).attr("data-points");var label=$(element).attr("data-label");var name=$(element).val();if(label===""){label=gettext("Unnamed Option")}var singularString=label+" - "+points+" point";var multipleString=label+" - "+points+" points";var finalLabel="";if(name===""){finalLabel=gettext("Not Selected")}else if(isNaN(points)){finalLabel=label}else{finalLabel=ngettext(singularString,multipleString,points)}$(element).text(finalLabel)}};OpenAssessment.Prompt=function(element,notifier){this.element=element;this.notifier=notifier};OpenAssessment.Prompt.prototype={getFieldValues:function(){var fields={description:this.description()};return fields},description:function(text){var sel=$(".openassessment_prompt_description",this.element);return OpenAssessment.Fields.stringField(sel,text)},addEventListeners:function(){},addHandler:function(){this.notifier.notificationFired("promptAdd",{index:this.element.index()})},removeHandler:function(){this.notifier.notificationFired("promptRemove",{index:this.element.index()})},updateHandler:function(){},validate:function(){return true},validationErrors:function(){return[]},clearValidationErrors:function(){}};OpenAssessment.RubricOption=function(element,notifier){this.element=element;this.notifier=notifier;this.pointsField=new OpenAssessment.IntField($(".openassessment_criterion_option_points",this.element),{min:0,max:999})};OpenAssessment.RubricOption.prototype={addEventListeners:function(){$(this.element).focusout($.proxy(this.updateHandler,this))},getFieldValues:function(){var fields={label:this.label(),points:this.points(),explanation:this.explanation()};var nameString=OpenAssessment.Fields.stringField($(".openassessment_criterion_option_name",this.element));if(nameString!==""){fields.name=nameString}return fields},label:function(label){var sel=$(".openassessment_criterion_option_label",this.element);return OpenAssessment.Fields.stringField(sel,label)},points:function(points){if(points!==undefined){this.pointsField.set(points)}return this.pointsField.get()},explanation:function(explanation){var sel=$(".openassessment_criterion_option_explanation",this.element);return OpenAssessment.Fields.stringField(sel,explanation)},addHandler:function(){var criterionElement=$(this.element).closest(".openassessment_criterion");var criterionName=$(criterionElement).data("criterion");var criterionLabel=$(".openassessment_criterion_label",criterionElement).val();var options=$(".openassessment_criterion_option",this.element.parent());var name=OpenAssessment.ItemUtilities.createUniqueName(options,"data-option");$(this.element).attr("data-criterion",criterionName).attr("data-option",name);$(".openassessment_criterion_option_name",this.element).attr("value",name);var fields=this.getFieldValues();this.notifier.notificationFired("optionAdd",{criterionName:criterionName,criterionLabel:criterionLabel,name:name,label:fields.label,points:fields.points})},removeHandler:function(){var criterionName=$(this.element).data("criterion");var optionName=$(this.element).data("option");this.notifier.notificationFired("optionRemove",{criterionName:criterionName,name:optionName})},updateHandler:function(){var fields=this.getFieldValues();var criterionName=$(this.element).data("criterion");var optionName=$(this.element).data("option");var optionLabel=fields.label;var optionPoints=fields.points;this.notifier.notificationFired("optionUpdated",{criterionName:criterionName,name:optionName,label:optionLabel,points:optionPoints})},validate:function(){return this.pointsField.validate()},validationErrors:function(){var hasError=this.pointsField.validationErrors().length>0;return hasError?["Option points are invalid"]:[]},clearValidationErrors:function(){this.pointsField.clearValidationErrors()}};OpenAssessment.RubricCriterion=function(element,notifier){this.element=element;this.notifier=notifier;this.labelSel=$(".openassessment_criterion_label",this.element);this.promptSel=$(".openassessment_criterion_prompt",this.element);this.optionContainer=new OpenAssessment.Container(OpenAssessment.RubricOption,{containerElement:$(".openassessment_criterion_option_list",this.element).get(0),templateElement:$("#openassessment_option_template").get(0),addButtonElement:$(".openassessment_criterion_add_option",this.element).get(0),removeButtonClass:"openassessment_criterion_option_remove_button",containerItemClass:"openassessment_criterion_option",notifier:this.notifier})};OpenAssessment.RubricCriterion.prototype={addEventListeners:function(){this.optionContainer.addEventListeners();$(this.element).focusout($.proxy(this.updateHandler,this))},getFieldValues:function(){var fields={label:this.label(),prompt:this.prompt(),feedback:this.feedback(),options:this.optionContainer.getItemValues()};var nameString=OpenAssessment.Fields.stringField($(".openassessment_criterion_name",this.element));if(nameString!==""){fields.name=nameString}return fields},label:function(label){return OpenAssessment.Fields.stringField(this.labelSel,label)},prompt:function(prompt){return OpenAssessment.Fields.stringField(this.promptSel,prompt)},feedback:function(){return $(".openassessment_criterion_feedback",this.element).val()},addOption:function(){this.optionContainer.add()},addHandler:function(){var criteria=$(".openassessment_criterion",this.element.parent());var name=OpenAssessment.ItemUtilities.createUniqueName(criteria,"data-criterion");$(this.element).attr("data-criterion",name);$(".openassessment_criterion_name",this.element).attr("value",name)},removeHandler:function(){var criterionName=$(this.element).data("criterion");this.notifier.notificationFired("criterionRemove",{criterionName:criterionName})},updateHandler:function(){var fields=this.getFieldValues();var criterionName=fields.name;var criterionLabel=fields.label;this.notifier.notificationFired("criterionUpdated",{criterionName:criterionName,criterionLabel:criterionLabel})},validate:function(){var isValid=this.prompt()!=="";if(!isValid){this.promptSel.addClass("openassessment_highlighted_field")}$.each(this.optionContainer.getAllItems(),function(){isValid=this.validate()&&isValid});return isValid},validationErrors:function(){var errors=[];if(this.promptSel.hasClass("openassessment_highlighted_field")){errors.push("Criterion prompt is invalid.")}$.each(this.optionContainer.getAllItems(),function(){errors=errors.concat(this.validationErrors())});return errors},clearValidationErrors:function(){this.promptSel.removeClass("openassessment_highlighted_field");$.each(this.optionContainer.getAllItems(),function(){this.clearValidationErrors()})}};OpenAssessment.TrainingExample=function(element){this.element=element;this.criteria=$(".openassessment_training_example_criterion_option",this.element);this.answer=$(".openassessment_training_example_essay_part textarea",this.element)};OpenAssessment.TrainingExample.prototype={getFieldValues:function(){var optionsSelected=this.criteria.map(function(){return{criterion:$(this).data("criterion"),option:$(this).prop("value")}}).get();return{answer:this.answer.map(function(){return $(this).prop("value")}).get(),options_selected:optionsSelected}},addHandler:function(){$(".openassessment_training_example_criterion_option",this.element).each(function(){$("option",this).each(function(){OpenAssessment.ItemUtilities.refreshOptionString($(this))})})},addEventListeners:function(){},removeHandler:function(){},updateHandler:function(){},validate:function(){var isValid=true;this.criteria.each(function(){var isOptionValid=$(this).prop("value")!=="";isValid=isOptionValid&&isValid;if(!isOptionValid){$(this).addClass("openassessment_highlighted_field")}});return isValid},validationErrors:function(){var errors=[];this.criteria.each(function(){var hasError=$(this).hasClass("openassessment_highlighted_field");if(hasError){errors.push("Student training example is invalid.")}});return errors},clearValidationErrors:function(){this.criteria.each(function(){$(this).removeClass("openassessment_highlighted_field")})}};OpenAssessment.StudioView=function(runtime,element,server,data){this.element=element;this.runtime=runtime;this.server=server;this.data=data;this.fixModalHeight();this.initializeTabs();this.alert=(new OpenAssessment.ValidationAlert).install();var studentTrainingListener=new OpenAssessment.StudentTrainingListener;this.promptsView=new OpenAssessment.EditPromptsView($("#oa_prompts_editor_wrapper",this.element).get(0),new OpenAssessment.Notifier([studentTrainingListener]));var studentTrainingView=new OpenAssessment.EditStudentTrainingView($("#oa_student_training_editor",this.element).get(0));var peerAssessmentView=new OpenAssessment.EditPeerAssessmentView($("#oa_peer_assessment_editor",this.element).get(0));var selfAssessmentView=new OpenAssessment.EditSelfAssessmentView($("#oa_self_assessment_editor",this.element).get(0));var exampleBasedAssessmentView=new OpenAssessment.EditExampleBasedAssessmentView($("#oa_ai_assessment_editor",this.element).get(0));var assessmentLookupDictionary={};assessmentLookupDictionary[studentTrainingView.getID()]=studentTrainingView;assessmentLookupDictionary[peerAssessmentView.getID()]=peerAssessmentView;assessmentLookupDictionary[selfAssessmentView.getID()]=selfAssessmentView;assessmentLookupDictionary[exampleBasedAssessmentView.getID()]=exampleBasedAssessmentView;this.settingsView=new OpenAssessment.EditSettingsView($("#oa_basic_settings_editor",this.element).get(0),assessmentLookupDictionary,data);this.rubricView=new OpenAssessment.EditRubricView($("#oa_rubric_editor_wrapper",this.element).get(0),new OpenAssessment.Notifier([studentTrainingListener]));$(".openassessment_save_button",this.element).click($.proxy(this.save,this));$(".openassessment_cancel_button",this.element).click($.proxy(this.cancel,this))};OpenAssessment.StudioView.prototype={fixModalHeight:function(){$(this.element).addClass("openassessment_full_height").parentsUntil(".modal-window").addClass("openassessment_full_height");$(this.element).closest(".modal-window").addClass("openassessment_modal_window")},initializeTabs:function(){if(typeof OpenAssessment.lastOpenEditingTab==="undefined"){OpenAssessment.lastOpenEditingTab=2}$(".openassessment_editor_content_and_tabs",this.element).tabs({active:OpenAssessment.lastOpenEditingTab})},saveTabState:function(){var tabElement=$(".openassessment_editor_content_and_tabs",this.element);OpenAssessment.lastOpenEditingTab=tabElement.tabs("option","active")},save:function(){var view=this;this.saveTabState();this.clearValidationErrors();if(!this.validate()){this.alert.setMessage(gettext("Couldn't Save This Assignment"),gettext("Please correct the outlined fields.")).show()}else{this.alert.hide();this.server.checkReleased().done(function(isReleased){if(isReleased){view.confirmPostReleaseUpdate($.proxy(view.updateEditorContext,view))}else{view.updateEditorContext()}}).fail(function(errMsg){view.showError(errMsg)})}},confirmPostReleaseUpdate:function(onConfirm){var msg=gettext("This problem has already been released. Any changes will apply only to future assessments.");if(confirm(msg)){onConfirm()}},updateEditorContext:function(){this.runtime.notify("save",{state:"start"});var view=this;this.server.updateEditorContext({prompts:view.promptsView.promptsDefinition(),feedbackPrompt:view.rubricView.feedbackPrompt(),feedback_default_text:view.rubricView.feedback_default_text(),criteria:view.rubricView.criteriaDefinition(),title:view.settingsView.displayName(),submissionStart:view.settingsView.submissionStart(),submissionDue:view.settingsView.submissionDue(),assessments:view.settingsView.assessmentsDescription(),fileUploadType:view.settingsView.fileUploadType(),fileTypeWhiteList:view.settingsView.fileTypeWhiteList(),latexEnabled:view.settingsView.latexEnabled(),leaderboardNum:view.settingsView.leaderboardNum(),editorAssessmentsOrder:view.settingsView.editorAssessmentsOrder()}).done(function(){view.runtime.notify("save",{state:"end"})}).fail(function(msg){view.showError(msg)})},cancel:function(){this.saveTabState();this.runtime.notify("cancel",{})},showError:function(errorMsg){this.runtime.notify("error",{msg:errorMsg})},validate:function(){var settingsValid=this.settingsView.validate();var rubricValid=this.rubricView.validate();var promptsValid=this.promptsView.validate();return settingsValid&&rubricValid&&promptsValid},validationErrors:function(){return this.settingsView.validationErrors().concat(this.rubricView.validationErrors().concat(this.promptsView.validationErrors()))},clearValidationErrors:function(){this.settingsView.clearValidationErrors();this.rubricView.clearValidationErrors();this.promptsView.clearValidationErrors()}};function OpenAssessmentEditor(runtime,element,data){var server=new OpenAssessment.Server(runtime,element);new OpenAssessment.StudioView(runtime,element,server,data)}OpenAssessment.EditPeerAssessmentView=function(element){this.element=element;this.name="peer-assessment";this.mustGradeField=new OpenAssessment.IntField($("#peer_assessment_must_grade",this.element),{min:0,max:99});this.mustBeGradedByField=new OpenAssessment.IntField($("#peer_assessment_graded_by",this.element),{min:0,max:99});new OpenAssessment.ToggleControl($("#include_peer_assessment",this.element),$("#peer_assessment_settings_editor",this.element),$("#peer_assessment_description_closed",this.element),new OpenAssessment.Notifier([new OpenAssessment.AssessmentToggleListener])).install();this.startDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#peer_assessment_start_date","#peer_assessment_start_time").install();this.dueDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#peer_assessment_due_date","#peer_assessment_due_time").install()};OpenAssessment.EditPeerAssessmentView.prototype={description:function(){return{must_grade:this.mustGradeNum(),must_be_graded_by:this.mustBeGradedByNum(),start:this.startDatetime(),due:this.dueDatetime()}},isEnabled:function(isEnabled){var sel=$("#include_peer_assessment",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},toggleEnabled:function(){$("#include_peer_assessment",this.element).click()},mustGradeNum:function(num){if(num!==undefined){this.mustGradeField.set(num)}return this.mustGradeField.get()},mustBeGradedByNum:function(num){if(num!==undefined){this.mustBeGradedByField.set(num)}return this.mustBeGradedByField.get()},startDatetime:function(dateString,timeString){return this.startDatetimeControl.datetime(dateString,timeString)},dueDatetime:function(dateString,timeString){return this.dueDatetimeControl.datetime(dateString,timeString)},getID:function(){return $(this.element).attr("id")},validate:function(){var startValid=this.startDatetimeControl.validate();var dueValid=this.dueDatetimeControl.validate();var mustGradeValid=this.mustGradeField.validate();var mustBeGradedByValid=this.mustBeGradedByField.validate();return startValid&&dueValid&&mustGradeValid&&mustBeGradedByValid},validationErrors:function(){var errors=[];if(this.startDatetimeControl.validationErrors().length>0){errors.push("Peer assessment start is invalid")}if(this.dueDatetimeControl.validationErrors().length>0){errors.push("Peer assessment due is invalid")}if(this.mustGradeField.validationErrors().length>0){errors.push("Peer assessment must grade is invalid")}if(this.mustBeGradedByField.validationErrors().length>0){errors.push("Peer assessment must be graded by is invalid")}return errors},clearValidationErrors:function(){this.startDatetimeControl.clearValidationErrors();this.dueDatetimeControl.clearValidationErrors();this.mustGradeField.clearValidationErrors();this.mustBeGradedByField.clearValidationErrors()}};OpenAssessment.EditSelfAssessmentView=function(element){this.element=element;this.name="self-assessment";new OpenAssessment.ToggleControl($("#include_self_assessment",this.element),$("#self_assessment_settings_editor",this.element),$("#self_assessment_description_closed",this.element),new OpenAssessment.Notifier([new OpenAssessment.AssessmentToggleListener])).install();this.startDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#self_assessment_start_date","#self_assessment_start_time").install();this.dueDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#self_assessment_due_date","#self_assessment_due_time").install()};OpenAssessment.EditSelfAssessmentView.prototype={description:function(){return{start:this.startDatetime(),due:this.dueDatetime()}},isEnabled:function(isEnabled){var sel=$("#include_self_assessment",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},toggleEnabled:function(){$("#include_self_assessment",this.element).click()},startDatetime:function(dateString,timeString){return this.startDatetimeControl.datetime(dateString,timeString)},dueDatetime:function(dateString,timeString){return this.dueDatetimeControl.datetime(dateString,timeString)},getID:function(){return $(this.element).attr("id")},validate:function(){var startValid=this.startDatetimeControl.validate();var dueValid=this.dueDatetimeControl.validate();return startValid&&dueValid},validationErrors:function(){var errors=[];if(this.startDatetimeControl.validationErrors().length>0){errors.push("Self assessment start is invalid")}if(this.dueDatetimeControl.validationErrors().length>0){errors.push("Self assessment due is invalid")}return errors},clearValidationErrors:function(){this.startDatetimeControl.clearValidationErrors();this.dueDatetimeControl.clearValidationErrors()}};OpenAssessment.EditStudentTrainingView=function(element){this.element=element;this.name="student-training";new OpenAssessment.ToggleControl($("#include_student_training",this.element),$("#student_training_settings_editor",this.element),$("#student_training_description_closed",this.element),new OpenAssessment.Notifier([new OpenAssessment.AssessmentToggleListener])).install();this.exampleContainer=new OpenAssessment.Container(OpenAssessment.TrainingExample,{containerElement:$("#openassessment_training_example_list",this.element).get(0),templateElement:$("#openassessment_training_example_template",this.element).get(0),addButtonElement:$(".openassessment_add_training_example",this.element).get(0),removeButtonClass:"openassessment_training_example_remove",containerItemClass:"openassessment_training_example"});this.exampleContainer.addEventListeners()};OpenAssessment.EditStudentTrainingView.prototype={description:function(){return{examples:this.exampleContainer.getItemValues()}},isEnabled:function(isEnabled){var sel=$("#include_student_training",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},toggleEnabled:function(){$("#include_student_training",this.element).click()},getID:function(){return $(this.element).attr("id")},validate:function(){var isValid=true;$.each(this.exampleContainer.getAllItems(),function(){isValid=this.validate()&&isValid});return isValid},validationErrors:function(){var errors=[];$.each(this.exampleContainer.getAllItems(),function(){errors=errors.concat(this.validationErrors())});return errors},clearValidationErrors:function(){$.each(this.exampleContainer.getAllItems(),function(){this.clearValidationErrors()})},addTrainingExample:function(){this.exampleContainer.add()}};OpenAssessment.EditExampleBasedAssessmentView=function(element){this.element=element;this.name="example-based-assessment";new OpenAssessment.ToggleControl($("#include_ai_assessment",this.element),$("#ai_assessment_settings_editor",this.element),$("#ai_assessment_description_closed",this.element),new OpenAssessment.Notifier([new OpenAssessment.AssessmentToggleListener])).install()};OpenAssessment.EditExampleBasedAssessmentView.prototype={description:function(){return{examples_xml:this.exampleDefinitions()}},isEnabled:function(isEnabled){var sel=$("#include_ai_assessment",this.element);return OpenAssessment.Fields.booleanField(sel,isEnabled)},toggleEnabled:function(){$("#include_ai_assessment",this.element).click()},exampleDefinitions:function(xml){var sel=$("#ai_training_examples",this.element);return OpenAssessment.Fields.stringField(sel,xml)},getID:function(){return $(this.element).attr("id")},validate:function(){return true},validationErrors:function(){return[]},clearValidationErrors:function(){}};OpenAssessment.Fields={stringField:function(sel,value){if(value!==undefined){sel.val(value)
}return sel.val()},booleanField:function(sel,value){if(value!==undefined){sel.prop("checked",value)}return sel.prop("checked")}};OpenAssessment.IntField=function(inputSel,restrictions){this.max=restrictions.max;this.min=restrictions.min;this.input=$(inputSel)};OpenAssessment.IntField.prototype={get:function(){return parseInt(this.input.val().trim(),10)},set:function(val){this.input.val(val)},validate:function(){var value=this.get();var isValid=!isNaN(value)&&value>=this.min&&value<=this.max;if(this.input.val().indexOf(".")!==-1){isValid=false}if(!isValid){this.input.addClass("openassessment_highlighted_field")}return isValid},clearValidationErrors:function(){this.input.removeClass("openassessment_highlighted_field")},validationErrors:function(){var hasError=this.input.hasClass("openassessment_highlighted_field");return hasError?["Int field is invalid"]:[]}};OpenAssessment.ToggleControl=function(checkboxSel,shownSel,hiddenSel,notifier){this.checkbox=checkboxSel;this.shownSection=shownSel;this.hiddenSection=hiddenSel;this.notifier=notifier};OpenAssessment.ToggleControl.prototype={install:function(){this.checkbox.change(this,function(event){var control=event.data;if(this.checked){control.notifier.notificationFired("toggleOn",{});control.show()}else{control.notifier.notificationFired("toggleOff",{});control.hide()}});return this},show:function(){this.shownSection.removeClass("is--hidden");this.hiddenSection.addClass("is--hidden")},hide:function(){this.shownSection.addClass("is--hidden");this.hiddenSection.removeClass("is--hidden")}};OpenAssessment.DatetimeControl=function(element,datePicker,timePicker){this.element=element;this.datePicker=datePicker;this.timePicker=timePicker};OpenAssessment.DatetimeControl.prototype={install:function(){var dateString=$(this.datePicker,this.element).val();$(this.datePicker,this.element).datepicker({showButtonPanel:true}).datepicker("option","dateFormat","yy-mm-dd").datepicker("setDate",dateString);$(this.timePicker,this.element).timepicker({timeFormat:"H:i",step:60});return this},datetime:function(dateString,timeString){var datePickerSel=$(this.datePicker,this.element);var timePickerSel=$(this.timePicker,this.element);if(typeof dateString!=="undefined"){datePickerSel.val(dateString)}if(typeof timeString!=="undefined"){timePickerSel.val(timeString)}return datePickerSel.val()+"T"+timePickerSel.val()},validate:function(){var dateString=$(this.datePicker,this.element).val();var timeString=$(this.timePicker,this.element).val();var isDateValid=false;try{var parsedDate=$.datepicker.parseDate($.datepicker.ISO_8601,dateString);isDateValid=parsedDate instanceof Date}catch(err){}if(!isDateValid){$(this.datePicker,this.element).addClass("openassessment_highlighted_field")}var matches=timeString.match(/^\d{2}:\d{2}$/g);var isTimeValid=matches!==null;if(!isTimeValid){$(this.timePicker,this.element).addClass("openassessment_highlighted_field")}return isDateValid&&isTimeValid},clearValidationErrors:function(){$(this.datePicker,this.element).removeClass("openassessment_highlighted_field");$(this.timePicker,this.element).removeClass("openassessment_highlighted_field")},validationErrors:function(){var errors=[];var dateHasError=$(this.datePicker,this.element).hasClass("openassessment_highlighted_field");var timeHasError=$(this.timePicker,this.element).hasClass("openassessment_highlighted_field");if(dateHasError){errors.push("Date is invalid")}if(timeHasError){errors.push("Time is invalid")}return errors}};OpenAssessment.SelectControl=function(selectSel,mapping,notifier){this.select=selectSel;this.mapping=mapping;this.notifier=notifier};OpenAssessment.SelectControl.prototype={install:function(){this.select.change(this,function(event){var control=event.data;control.notifier.notificationFired("selectionChanged",{selected:this.value});control.change(this.value)});return this},change:function(selected){$.each(this.mapping,function(option,sel){if(option===selected){sel.removeClass("is--hidden")}else{sel.addClass("is--hidden")}})}};OpenAssessment.InputControl=function(inputSel,validator){this.input=$(inputSel);this.validator=validator;this.errors=[]};OpenAssessment.InputControl.prototype={get:function(){return this.input.val()},set:function(val){this.input.val(val)},validate:function(){this.errors=this.validator(this.get());if(this.errors.length){this.input.addClass("openassessment_highlighted_field");this.input.parent().nextAll(".message-status").text(this.errors.join(";"));this.input.parent().nextAll(".message-status").addClass("is-shown")}return this.errors.length===0},clearValidationErrors:function(){this.input.removeClass("openassessment_highlighted_field");this.input.parent().nextAll(".message-status").removeClass("is-shown")},validationErrors:function(){return this.errors}};OpenAssessment.StudentTrainingListener=function(){this.element=$("#oa_student_training_editor");this.alert=new OpenAssessment.ValidationAlert};OpenAssessment.StudentTrainingListener.prototype={promptAdd:function(){var view=this.element;$("#openassessment_training_example_part_template").children().first().clone().removeAttr("id").toggleClass("is--hidden",false).appendTo(".openassessment_training_example_essay",view)},promptRemove:function(data){var view=this.element;$(".openassessment_training_example_essay li:nth-child("+(data.index+1)+")",view).remove()},optionUpdated:function(data){this._optionSel(data.criterionName).each(function(){var criterion=this;var option=$('option[value="'+data.name+'"]',criterion).attr("data-points",data.points).attr("data-label",data.label);OpenAssessment.ItemUtilities.refreshOptionString(option)})},optionAdd:function(data){var criterionAdded=false;if(this._optionSel(data.criterionName).length===0){this.criterionAdd(data);criterionAdded=true}this._optionSel(data.criterionName).each(function(){var criterion=this;var option=$("<option></option>").attr("value",data.name).attr("data-points",data.points).attr("data-label",data.label);OpenAssessment.ItemUtilities.refreshOptionString(option);$(criterion).append(option)});if(criterionAdded){this.displayAlertMsg(gettext("Criterion Added"),gettext("You have added a criterion. You will need to select an option for the criterion in the Learner Training step. To do this, click the Settings tab."))}},optionRemove:function(data){var handler=this;var invalidated=false;this._optionSel(data.criterionName).each(function(){var criterionOption=this;if($(criterionOption).val()===data.name.toString()){$(criterionOption).val("").addClass("openassessment_highlighted_field").click(function(){$(criterionOption).removeClass("openassessment_highlighted_field")});invalidated=true}$('option[value="'+data.name+'"]',criterionOption).remove();if($("option",criterionOption).length===1){handler.removeAllOptions(data);invalidated=false}});if(invalidated){this.displayAlertMsg(gettext("Option Deleted"),gettext("You have deleted an option. That option has been removed from its criterion in the sample responses in the Learner Training step. You might have to select a new option for the criterion."))}},_optionSel:function(criterionName){return $('.openassessment_training_example_criterion_option[data-criterion="'+criterionName+'"]',this.element)},removeAllOptions:function(data){var changed=false;$(".openassessment_training_example_criterion",this.element).each(function(){var criterion=this;if($(criterion).data("criterion")===data.criterionName){$(criterion).remove();changed=true}});if(changed){this.displayAlertMsg(gettext("Option Deleted"),gettext("You have deleted all the options for this criterion. The criterion has been removed from the sample responses in the Learner Training step."))}},criterionRemove:function(data){var changed=false;var sel='.openassessment_training_example_criterion[data-criterion="'+data.criterionName+'"]';$(sel,this.element).each(function(){$(this).remove();changed=true});if(changed){this.displayAlertMsg(gettext("Criterion Deleted"),gettext("You have deleted a criterion. The criterion has been removed from the example responses in the Learner Training step."))}},displayAlertMsg:function(title,msg){if($("#include_student_training",this.element).is(":checked")&&$(".openassessment_training_example",this.element).length>1){this.alert.setMessage(title,msg).show()}},criterionUpdated:function(data){var sel='.openassessment_training_example_criterion[data-criterion="'+data.criterionName+'"]';$(sel,this.element).each(function(){$(".openassessment_training_example_criterion_name_wrapper",this).text(data.criterionLabel)})},criterionAdd:function(data){var view=this.element;var criterion=$("#openassessment_training_example_criterion_template").children().first().clone().removeAttr("id").attr("data-criterion",data.criterionName).toggleClass("is--hidden",false).appendTo(".openassessment_training_example_criteria_selections",view);criterion.find(".openassessment_training_example_criterion_option").attr("data-criterion",data.criterionName);criterion.find(".openassessment_training_example_criterion_name_wrapper").text(data.label)},examplesCriteriaLabels:function(){var examples=[];$(".openassessment_training_example_criteria_selections",this.element).each(function(){var exampleDescription={};$(".openassessment_training_example_criterion",this).each(function(){var criterionName=$(this).data("criterion");var criterionLabel=$(".openassessment_training_example_criterion_name_wrapper",this).text().trim();exampleDescription[criterionName]=criterionLabel});examples.push(exampleDescription)});return examples},examplesOptionsLabels:function(){var examples=[];$(".openassessment_training_example_criteria_selections",this.element).each(function(){var exampleDescription={};$(".openassessment_training_example_criterion_option",this).each(function(){var criterionName=$(this).data("criterion");exampleDescription[criterionName]={};$("option",this).each(function(){var optionName=$(this).val();var optionLabel=$(this).text().trim();exampleDescription[criterionName][optionName]=optionLabel})});examples.push(exampleDescription)});return examples}};OpenAssessment.AssessmentToggleListener=function(){this.alert=new OpenAssessment.ValidationAlert};OpenAssessment.AssessmentToggleListener.prototype={toggleOff:function(){this.alert.setMessage(gettext("Warning"),gettext("Changes to steps that are not selected as part of the assignment will not be saved.")).show()},toggleOn:function(){this.alert.hide()}};OpenAssessment.Notifier=function(listeners){this.listeners=listeners};OpenAssessment.Notifier.prototype={notificationFired:function(name,data){for(var i=0;i<this.listeners.length;i++){if(typeof this.listeners[i][name]==="function"){this.listeners[i][name](data)}}}};OpenAssessment.EditPromptsView=function(element,notifier){this.element=element;this.editorElement=$(this.element).closest("#openassessment-editor");this.addRemoveEnabled=this.editorElement.attr("data-is-released")!=="true";this.promptsContainer=new OpenAssessment.Container(OpenAssessment.Prompt,{containerElement:$("#openassessment_prompts_list",this.element).get(0),templateElement:$("#openassessment_prompt_template",this.element).get(0),addButtonElement:$("#openassessment_prompts_add_prompt",this.element).get(0),removeButtonClass:"openassessment_prompt_remove_button",containerItemClass:"openassessment_prompt",notifier:notifier,addRemoveEnabled:this.addRemoveEnabled});this.promptsContainer.addEventListeners()};OpenAssessment.EditPromptsView.prototype={promptsDefinition:function(){var prompts=this.promptsContainer.getItemValues();return prompts},addPrompt:function(){if(this.addRemoveEnabled){this.promptsContainer.add()}},removePrompt:function(item){if(this.addRemoveEnabled){this.promptsContainer.remove(item)}},getAllPrompts:function(){return this.promptsContainer.getAllItems()},getPromptItem:function(index){return this.promptsContainer.getItem(index)},validate:function(){return true},validationErrors:function(){var errors=[];return errors},clearValidationErrors:function(){}};OpenAssessment.EditRubricView=function(element,notifier){this.element=element;this.criterionAddButton=$("#openassessment_rubric_add_criterion",this.element);this.criteriaContainer=new OpenAssessment.Container(OpenAssessment.RubricCriterion,{containerElement:$("#openassessment_criterion_list",this.element).get(0),templateElement:$("#openassessment_criterion_template",this.element).get(0),addButtonElement:$("#openassessment_rubric_add_criterion",this.element).get(0),removeButtonClass:"openassessment_criterion_remove_button",containerItemClass:"openassessment_criterion",notifier:notifier});this.criteriaContainer.addEventListeners()};OpenAssessment.EditRubricView.prototype={criteriaDefinition:function(){var criteria=this.criteriaContainer.getItemValues();for(var criterion_idx=0;criterion_idx<criteria.length;criterion_idx++){var criterion=criteria[criterion_idx];criterion.order_num=criterion_idx;for(var option_idx=0;option_idx<criterion.options.length;option_idx++){var option=criterion.options[option_idx];option.order_num=option_idx}}return criteria},feedbackPrompt:function(text){var sel=$("#openassessment_rubric_feedback",this.element);return OpenAssessment.Fields.stringField(sel,text)},feedback_default_text:function(text){var sel=$("#openassessment_rubric_feedback_default_text",this.element);return OpenAssessment.Fields.stringField(sel,text)},addCriterion:function(){this.criteriaContainer.add()},removeCriterion:function(item){this.criteriaContainer.remove(item)},getAllCriteria:function(){return this.criteriaContainer.getAllItems()},getCriterionItem:function(index){return this.criteriaContainer.getItem(index)},addOption:function(criterionIndex){var criterionItem=this.getCriterionItem(criterionIndex);criterionItem.optionContainer.add()},removeOption:function(criterionIndex,item){var criterionItem=this.getCriterionItem(criterionIndex);criterionItem.optionContainer.remove(item)},getAllOptions:function(criterionIndex){var criterionItem=this.getCriterionItem(criterionIndex);return criterionItem.optionContainer.getAllItems()},getOptionItem:function(criterionIndex,optionIndex){var criterionItem=this.getCriterionItem(criterionIndex);return criterionItem.optionContainer.getItem(optionIndex)},validate:function(){var criteria=this.getAllCriteria();var isValid=criteria.length>0;if(!isValid){this.criterionAddButton.addClass("openassessment_highlighted_field").click(function(){$(this).removeClass("openassessment_highlighted_field")})}$.each(criteria,function(){isValid=this.validate()&&isValid});return isValid},validationErrors:function(){var errors=[];if(this.criterionAddButton.hasClass("openassessment_highlighted_field")){errors.push("The rubric must contain at least one criterion")}$.each(this.getAllCriteria(),function(){errors=errors.concat(this.validationErrors())});return errors},clearValidationErrors:function(){this.criterionAddButton.removeClass("openassessment_highlighted_field");$.each(this.getAllCriteria(),function(){this.clearValidationErrors()})}};OpenAssessment.EditSettingsView=function(element,assessmentViews,data){this.settingsElement=element;this.assessmentsElement=$(element).siblings("#openassessment_assessment_module_settings_editors").get(0);this.assessmentViews=assessmentViews;this.startDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#openassessment_submission_start_date","#openassessment_submission_start_time").install();this.dueDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#openassessment_submission_due_date","#openassessment_submission_due_time").install();new OpenAssessment.SelectControl($("#openassessment_submission_upload_selector",this.element),{custom:$("#openassessment_submission_white_listed_file_types_wrapper",this.element)},new OpenAssessment.Notifier([new OpenAssessment.AssessmentToggleListener])).install();this.leaderboardIntField=new OpenAssessment.IntField($("#openassessment_leaderboard_editor",this.element),{min:0,max:100});this.fileTypeWhiteListInputField=new OpenAssessment.InputControl($("#openassessment_submission_white_listed_file_types",this.element),function(value){var badExts=[];var errors=[];if(!value){errors.push(gettext("File types can not be empty."));return errors}var whiteList=$.map(value.replace(/\./g,"").toLowerCase().split(","),$.trim);$.each(whiteList,function(index,ext){if(data.FILE_EXT_BLACK_LIST.indexOf(ext)!==-1){badExts.push(ext)}});if(badExts.length){errors.push(gettext("The following file types are not allowed: ")+badExts.join(","))}return errors});this.initializeSortableAssessments()};OpenAssessment.EditSettingsView.prototype={initializeSortableAssessments:function(){var view=this;$("#openassessment_assessment_module_settings_editors",view.element).sortable({start:function(event,ui){$(".openassessment_assessment_module_editor",view.element).hide();var targetHeight="auto";ui.placeholder.height(targetHeight);ui.helper.height(targetHeight);$("#openassessment_assessment_module_settings_editors",view.element).sortable("refresh").sortable("refreshPositions")},stop:function(){$(".openassessment_assessment_module_editor",view.element).show()},snap:true,axis:"y",handle:".drag-handle",cursorAt:{top:20}});$("#openassessment_assessment_module_settings_editors .drag-handle",view.element).disableSelection()},displayName:function(name){var sel=$("#openassessment_title_editor",this.settingsElement);return OpenAssessment.Fields.stringField(sel,name)},submissionStart:function(dateString,timeString){return this.startDatetimeControl.datetime(dateString,timeString)},submissionDue:function(dateString,timeString){return this.dueDatetimeControl.datetime(dateString,timeString)},fileUploadType:function(uploadType){var sel=$("#openassessment_submission_upload_selector",this.settingsElement);if(uploadType!==undefined){sel.val(uploadType)}return sel.val()},fileTypeWhiteList:function(exts){if(exts!==undefined){this.fileTypeWhiteListInputField.set(exts)}return this.fileTypeWhiteListInputField.get()},latexEnabled:function(isEnabled){var sel=$("#openassessment_submission_latex_editor",this.settingsElement);if(isEnabled!==undefined){if(isEnabled){sel.val(1)}else{sel.val(0)}}return sel.val()===1},leaderboardNum:function(num){if(num!==undefined){this.leaderboardIntField.set(num)}return this.leaderboardIntField.get(num)},assessmentsDescription:function(){var assessmentDescList=[];var view=this;$(".openassessment_assessment_module_settings_editor",this.assessmentsElement).each(function(){var asmntView=view.assessmentViews[$(this).attr("id")];if(asmntView.isEnabled()){var description=asmntView.description();description.name=asmntView.name;assessmentDescList.push(description)}});return assessmentDescList},editorAssessmentsOrder:function(){var editorAssessments=[];var view=this;$(".openassessment_assessment_module_settings_editor",this.assessmentsElement).each(function(){var asmntView=view.assessmentViews[$(this).attr("id")];editorAssessments.push(asmntView.name)});return editorAssessments},validate:function(){var isValid=true;isValid=this.startDatetimeControl.validate()&&isValid;isValid=this.dueDatetimeControl.validate()&&isValid;isValid=this.leaderboardIntField.validate()&&isValid;if(this.fileUploadType()==="custom"){isValid=this.fileTypeWhiteListInputField.validate()&&isValid}else{if(this.fileTypeWhiteListInputField.get()&&!this.fileTypeWhiteListInputField.validate()){this.fileTypeWhiteListInputField.set("")}}$.each(this.assessmentViews,function(){if(this.isEnabled()){isValid=this.validate()&&isValid}});return isValid},validationErrors:function(){var errors=[];if(this.startDatetimeControl.validationErrors().length>0){errors.push("Submission start is invalid")}if(this.dueDatetimeControl.validationErrors().length>0){errors.push("Submission due is invalid")}if(this.leaderboardIntField.validationErrors().length>0){errors.push("Leaderboard number is invalid")}if(this.fileTypeWhiteListInputField.validationErrors().length>0){errors=errors.concat(this.fileTypeWhiteListInputField.validationErrors())}$.each(this.assessmentViews,function(){errors=errors.concat(this.validationErrors())});return errors},clearValidationErrors:function(){this.startDatetimeControl.clearValidationErrors();this.dueDatetimeControl.clearValidationErrors();this.leaderboardIntField.clearValidationErrors();this.fileTypeWhiteListInputField.clearValidationErrors();$.each(this.assessmentViews,function(){this.clearValidationErrors()})}};OpenAssessment.ValidationAlert=function(){this.element=$("#openassessment_validation_alert");this.editorElement=$(this.element).parent();this.title=$(".openassessment_alert_title",this.element);this.message=$(".openassessment_alert_message",this.element);this.closeButton=$(".openassessment_alert_close",this.element);this.ALERT_YELLOW="rgb(192, 172, 0)";this.DARK_GREY="#323232"};OpenAssessment.ValidationAlert.prototype={install:function(){var alert=this;this.closeButton.click(function(eventObject){eventObject.preventDefault();alert.hide()});return this},hide:function(){var headerHeight=$("#openassessment_editor_header",this.editorElement).outerHeight();this.element.addClass("covered");var styles={height:"Calc(100% - "+headerHeight+"px)","border-top-right-radius":"3px","border-top-left-radius":"3px"};$(".oa_editor_content_wrapper",this.editorElement).each(function(){$(this).css(styles)});return this},show:function(){var view=this;if(this.isVisible()){$(this.element).animate({"background-color":view.ALERT_YELLOW},300,"swing",function(){$(this).animate({"background-color":view.DARK_GREY},700,"swing")})}else{this.element.removeClass("covered");var alertHeight=this.element.outerHeight();var headerHeight=$("#openassessment_editor_header",this.editorElement).outerHeight();var heightString="Calc(100% - "+(alertHeight+headerHeight)+"px)";var styles={height:heightString,"border-top-right-radius":"0px","border-top-left-radius":"0px"};$(".oa_editor_content_wrapper",this.editorElement).each(function(){$(this).css(styles)})}return this},setMessage:function(newTitle,newMessage){this.title.text(newTitle);this.message.text(newMessage);return this},isVisible:function(){return!this.element.hasClass("covered")},getTitle:function(){return this.title.text()},getMessage:function(){return this.message.text()}}; }return sel.val()},booleanField:function(sel,value){if(value!==undefined){sel.prop("checked",value)}return sel.prop("checked")}};OpenAssessment.IntField=function(inputSel,restrictions){this.max=restrictions.max;this.min=restrictions.min;this.input=$(inputSel)};OpenAssessment.IntField.prototype={get:function(){return parseInt(this.input.val().trim(),10)},set:function(val){this.input.val(val)},validate:function(){var value=this.get();var isValid=!isNaN(value)&&value>=this.min&&value<=this.max;if(this.input.val().indexOf(".")!==-1){isValid=false}if(!isValid){this.input.addClass("openassessment_highlighted_field")}return isValid},clearValidationErrors:function(){this.input.removeClass("openassessment_highlighted_field")},validationErrors:function(){var hasError=this.input.hasClass("openassessment_highlighted_field");return hasError?["Int field is invalid"]:[]}};OpenAssessment.ToggleControl=function(checkboxSel,shownSel,hiddenSel,notifier){this.checkbox=checkboxSel;this.shownSection=shownSel;this.hiddenSection=hiddenSel;this.notifier=notifier};OpenAssessment.ToggleControl.prototype={install:function(){this.checkbox.change(this,function(event){var control=event.data;if(this.checked){control.notifier.notificationFired("toggleOn",{});control.show()}else{control.notifier.notificationFired("toggleOff",{});control.hide()}});return this},show:function(){this.shownSection.removeClass("is--hidden");this.hiddenSection.addClass("is--hidden")},hide:function(){this.shownSection.addClass("is--hidden");this.hiddenSection.removeClass("is--hidden")}};OpenAssessment.DatetimeControl=function(element,datePicker,timePicker){this.element=element;this.datePicker=datePicker;this.timePicker=timePicker};OpenAssessment.DatetimeControl.prototype={install:function(){var dateString=$(this.datePicker,this.element).val();$(this.datePicker,this.element).datepicker({showButtonPanel:true}).datepicker("option","dateFormat","yy-mm-dd").datepicker("setDate",dateString);$(this.timePicker,this.element).timepicker({timeFormat:"H:i",step:60});return this},datetime:function(dateString,timeString){var datePickerSel=$(this.datePicker,this.element);var timePickerSel=$(this.timePicker,this.element);if(typeof dateString!=="undefined"){datePickerSel.val(dateString)}if(typeof timeString!=="undefined"){timePickerSel.val(timeString)}return datePickerSel.val()+"T"+timePickerSel.val()},validate:function(){var dateString=$(this.datePicker,this.element).val();var timeString=$(this.timePicker,this.element).val();var isDateValid=false;try{var parsedDate=$.datepicker.parseDate($.datepicker.ISO_8601,dateString);isDateValid=parsedDate instanceof Date}catch(err){}if(!isDateValid){$(this.datePicker,this.element).addClass("openassessment_highlighted_field")}var matches=timeString.match(/^\d{2}:\d{2}$/g);var isTimeValid=matches!==null;if(!isTimeValid){$(this.timePicker,this.element).addClass("openassessment_highlighted_field")}return isDateValid&&isTimeValid},clearValidationErrors:function(){$(this.datePicker,this.element).removeClass("openassessment_highlighted_field");$(this.timePicker,this.element).removeClass("openassessment_highlighted_field")},validationErrors:function(){var errors=[];var dateHasError=$(this.datePicker,this.element).hasClass("openassessment_highlighted_field");var timeHasError=$(this.timePicker,this.element).hasClass("openassessment_highlighted_field");if(dateHasError){errors.push("Date is invalid")}if(timeHasError){errors.push("Time is invalid")}return errors}};OpenAssessment.SelectControl=function(selectSel,mapping,notifier){this.select=selectSel;this.mapping=mapping;this.notifier=notifier};OpenAssessment.SelectControl.prototype={install:function(){this.select.change(this,function(event){var control=event.data;control.notifier.notificationFired("selectionChanged",{selected:this.value});control.change(this.value)});return this},change:function(selected){$.each(this.mapping,function(option,sel){if(option===selected){sel.removeClass("is--hidden")}else{sel.addClass("is--hidden")}})}};OpenAssessment.InputControl=function(inputSel,validator){this.input=$(inputSel);this.validator=validator;this.errors=[]};OpenAssessment.InputControl.prototype={get:function(){return this.input.val()},set:function(val){this.input.val(val)},validate:function(){this.errors=this.validator(this.get());if(this.errors.length){this.input.addClass("openassessment_highlighted_field");this.input.parent().nextAll(".message-status").text(this.errors.join(";"));this.input.parent().nextAll(".message-status").addClass("is-shown")}return this.errors.length===0},clearValidationErrors:function(){this.input.removeClass("openassessment_highlighted_field");this.input.parent().nextAll(".message-status").removeClass("is-shown")},validationErrors:function(){return this.errors}};OpenAssessment.StudentTrainingListener=function(){this.element=$("#oa_student_training_editor");this.alert=new OpenAssessment.ValidationAlert};OpenAssessment.StudentTrainingListener.prototype={promptAdd:function(){var view=this.element;$("#openassessment_training_example_part_template").children().first().clone().removeAttr("id").toggleClass("is--hidden",false).appendTo(".openassessment_training_example_essay",view)},promptRemove:function(data){var view=this.element;$(".openassessment_training_example_essay li:nth-child("+(data.index+1)+")",view).remove()},optionUpdated:function(data){this._optionSel(data.criterionName).each(function(){var criterion=this;var option=$('option[value="'+data.name+'"]',criterion).attr("data-points",data.points).attr("data-label",data.label);OpenAssessment.ItemUtilities.refreshOptionString(option)})},optionAdd:function(data){var criterionAdded=false;if(this._optionSel(data.criterionName).length===0){this.criterionAdd(data);criterionAdded=true}this._optionSel(data.criterionName).each(function(){var criterion=this;var option=$("<option></option>").attr("value",data.name).attr("data-points",data.points).attr("data-label",data.label);OpenAssessment.ItemUtilities.refreshOptionString(option);$(criterion).append(option)});if(criterionAdded){this.displayAlertMsg(gettext("Criterion Added"),gettext("You have added a criterion. You will need to select an option for the criterion in the Learner Training step. To do this, click the Settings tab."))}},optionRemove:function(data){var handler=this;var invalidated=false;this._optionSel(data.criterionName).each(function(){var criterionOption=this;if($(criterionOption).val()===data.name.toString()){$(criterionOption).val("").addClass("openassessment_highlighted_field").click(function(){$(criterionOption).removeClass("openassessment_highlighted_field")});invalidated=true}$('option[value="'+data.name+'"]',criterionOption).remove();if($("option",criterionOption).length===1){handler.removeAllOptions(data);invalidated=false}});if(invalidated){this.displayAlertMsg(gettext("Option Deleted"),gettext("You have deleted an option. That option has been removed from its criterion in the sample responses in the Learner Training step. You might have to select a new option for the criterion."))}},_optionSel:function(criterionName){return $('.openassessment_training_example_criterion_option[data-criterion="'+criterionName+'"]',this.element)},removeAllOptions:function(data){var changed=false;$(".openassessment_training_example_criterion",this.element).each(function(){var criterion=this;if($(criterion).data("criterion")===data.criterionName){$(criterion).remove();changed=true}});if(changed){this.displayAlertMsg(gettext("Option Deleted"),gettext("You have deleted all the options for this criterion. The criterion has been removed from the sample responses in the Learner Training step."))}},criterionRemove:function(data){var changed=false;var sel='.openassessment_training_example_criterion[data-criterion="'+data.criterionName+'"]';$(sel,this.element).each(function(){$(this).remove();changed=true});if(changed){this.displayAlertMsg(gettext("Criterion Deleted"),gettext("You have deleted a criterion. The criterion has been removed from the example responses in the Learner Training step."))}},displayAlertMsg:function(title,msg){if($("#include_student_training",this.element).is(":checked")&&$(".openassessment_training_example",this.element).length>1){this.alert.setMessage(title,msg).show()}},criterionUpdated:function(data){var sel='.openassessment_training_example_criterion[data-criterion="'+data.criterionName+'"]';$(sel,this.element).each(function(){$(".openassessment_training_example_criterion_name_wrapper",this).text(data.criterionLabel)})},criterionAdd:function(data){var view=this.element;var criterion=$("#openassessment_training_example_criterion_template").children().first().clone().removeAttr("id").attr("data-criterion",data.criterionName).toggleClass("is--hidden",false).appendTo(".openassessment_training_example_criteria_selections",view);criterion.find(".openassessment_training_example_criterion_option").attr("data-criterion",data.criterionName);criterion.find(".openassessment_training_example_criterion_name_wrapper").text(data.label)},examplesCriteriaLabels:function(){var examples=[];$(".openassessment_training_example_criteria_selections",this.element).each(function(){var exampleDescription={};$(".openassessment_training_example_criterion",this).each(function(){var criterionName=$(this).data("criterion");var criterionLabel=$(".openassessment_training_example_criterion_name_wrapper",this).text().trim();exampleDescription[criterionName]=criterionLabel});examples.push(exampleDescription)});return examples},examplesOptionsLabels:function(){var examples=[];$(".openassessment_training_example_criteria_selections",this.element).each(function(){var exampleDescription={};$(".openassessment_training_example_criterion_option",this).each(function(){var criterionName=$(this).data("criterion");exampleDescription[criterionName]={};$("option",this).each(function(){var optionName=$(this).val();var optionLabel=$(this).text().trim();exampleDescription[criterionName][optionName]=optionLabel})});examples.push(exampleDescription)});return examples}};OpenAssessment.AssessmentToggleListener=function(){this.alert=new OpenAssessment.ValidationAlert};OpenAssessment.AssessmentToggleListener.prototype={toggleOff:function(){this.alert.setMessage(gettext("Warning"),gettext("Changes to steps that are not selected as part of the assignment will not be saved.")).show()},toggleOn:function(){this.alert.hide()}};OpenAssessment.Notifier=function(listeners){this.listeners=listeners};OpenAssessment.Notifier.prototype={notificationFired:function(name,data){for(var i=0;i<this.listeners.length;i++){if(typeof this.listeners[i][name]==="function"){this.listeners[i][name](data)}}}};OpenAssessment.EditPromptsView=function(element,notifier){this.element=element;this.editorElement=$(this.element).closest("#openassessment-editor");this.addRemoveEnabled=this.editorElement.attr("data-is-released")!=="true";this.promptsContainer=new OpenAssessment.Container(OpenAssessment.Prompt,{containerElement:$("#openassessment_prompts_list",this.element).get(0),templateElement:$("#openassessment_prompt_template",this.element).get(0),addButtonElement:$("#openassessment_prompts_add_prompt",this.element).get(0),removeButtonClass:"openassessment_prompt_remove_button",containerItemClass:"openassessment_prompt",notifier:notifier,addRemoveEnabled:this.addRemoveEnabled});this.promptsContainer.addEventListeners()};OpenAssessment.EditPromptsView.prototype={promptsDefinition:function(){var prompts=this.promptsContainer.getItemValues();return prompts},addPrompt:function(){if(this.addRemoveEnabled){this.promptsContainer.add()}},removePrompt:function(item){if(this.addRemoveEnabled){this.promptsContainer.remove(item)}},getAllPrompts:function(){return this.promptsContainer.getAllItems()},getPromptItem:function(index){return this.promptsContainer.getItem(index)},validate:function(){return true},validationErrors:function(){var errors=[];return errors},clearValidationErrors:function(){}};OpenAssessment.EditRubricView=function(element,notifier){this.element=element;this.criterionAddButton=$("#openassessment_rubric_add_criterion",this.element);this.criteriaContainer=new OpenAssessment.Container(OpenAssessment.RubricCriterion,{containerElement:$("#openassessment_criterion_list",this.element).get(0),templateElement:$("#openassessment_criterion_template",this.element).get(0),addButtonElement:$("#openassessment_rubric_add_criterion",this.element).get(0),removeButtonClass:"openassessment_criterion_remove_button",containerItemClass:"openassessment_criterion",notifier:notifier});this.criteriaContainer.addEventListeners()};OpenAssessment.EditRubricView.prototype={criteriaDefinition:function(){var criteria=this.criteriaContainer.getItemValues();for(var criterion_idx=0;criterion_idx<criteria.length;criterion_idx++){var criterion=criteria[criterion_idx];criterion.order_num=criterion_idx;for(var option_idx=0;option_idx<criterion.options.length;option_idx++){var option=criterion.options[option_idx];option.order_num=option_idx}}return criteria},feedbackPrompt:function(text){var sel=$("#openassessment_rubric_feedback",this.element);return OpenAssessment.Fields.stringField(sel,text)},feedback_default_text:function(text){var sel=$("#openassessment_rubric_feedback_default_text",this.element);return OpenAssessment.Fields.stringField(sel,text)},addCriterion:function(){this.criteriaContainer.add()},removeCriterion:function(item){this.criteriaContainer.remove(item)},getAllCriteria:function(){return this.criteriaContainer.getAllItems()},getCriterionItem:function(index){return this.criteriaContainer.getItem(index)},addOption:function(criterionIndex){var criterionItem=this.getCriterionItem(criterionIndex);criterionItem.optionContainer.add()},removeOption:function(criterionIndex,item){var criterionItem=this.getCriterionItem(criterionIndex);criterionItem.optionContainer.remove(item)},getAllOptions:function(criterionIndex){var criterionItem=this.getCriterionItem(criterionIndex);return criterionItem.optionContainer.getAllItems()},getOptionItem:function(criterionIndex,optionIndex){var criterionItem=this.getCriterionItem(criterionIndex);return criterionItem.optionContainer.getItem(optionIndex)},validate:function(){var criteria=this.getAllCriteria();var isValid=criteria.length>0;if(!isValid){this.criterionAddButton.addClass("openassessment_highlighted_field").click(function(){$(this).removeClass("openassessment_highlighted_field")})}$.each(criteria,function(){isValid=this.validate()&&isValid});return isValid},validationErrors:function(){var errors=[];if(this.criterionAddButton.hasClass("openassessment_highlighted_field")){errors.push("The rubric must contain at least one criterion")}$.each(this.getAllCriteria(),function(){errors=errors.concat(this.validationErrors())});return errors},clearValidationErrors:function(){this.criterionAddButton.removeClass("openassessment_highlighted_field");$.each(this.getAllCriteria(),function(){this.clearValidationErrors()})}};OpenAssessment.EditSettingsView=function(element,assessmentViews,data){this.settingsElement=element;this.assessmentsElement=$(element).siblings("#openassessment_assessment_module_settings_editors").get(0);this.assessmentViews=assessmentViews;this.startDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#openassessment_submission_start_date","#openassessment_submission_start_time").install();this.dueDatetimeControl=new OpenAssessment.DatetimeControl(this.element,"#openassessment_submission_due_date","#openassessment_submission_due_time").install();new OpenAssessment.SelectControl($("#openassessment_submission_upload_selector",this.element),{custom:$("#openassessment_submission_white_listed_file_types_wrapper",this.element)},new OpenAssessment.Notifier([new OpenAssessment.AssessmentToggleListener])).install();this.leaderboardIntField=new OpenAssessment.IntField($("#openassessment_leaderboard_editor",this.element),{min:0,max:100});this.fileTypeWhiteListInputField=new OpenAssessment.InputControl($("#openassessment_submission_white_listed_file_types",this.element),function(value){var badExts=[];var errors=[];if(!value){errors.push(gettext("File types can not be empty."));return errors}var whiteList=$.map(value.replace(/\./g,"").toLowerCase().split(","),$.trim);$.each(whiteList,function(index,ext){if(data.FILE_EXT_BLACK_LIST.indexOf(ext)!==-1){badExts.push(ext)}});if(badExts.length){errors.push(gettext("The following file types are not allowed: ")+badExts.join(","))}return errors});this.initializeSortableAssessments()};OpenAssessment.EditSettingsView.prototype={initializeSortableAssessments:function(){var view=this;$("#openassessment_assessment_module_settings_editors",view.element).sortable({start:function(event,ui){$(".openassessment_assessment_module_editor",view.element).hide();var targetHeight="auto";ui.placeholder.height(targetHeight);ui.helper.height(targetHeight);$("#openassessment_assessment_module_settings_editors",view.element).sortable("refresh").sortable("refreshPositions")},stop:function(){$(".openassessment_assessment_module_editor",view.element).show()},snap:true,axis:"y",handle:".drag-handle",cursorAt:{top:20}});$("#openassessment_assessment_module_settings_editors .drag-handle",view.element).disableSelection()},displayName:function(name){var sel=$("#openassessment_title_editor",this.settingsElement);return OpenAssessment.Fields.stringField(sel,name)},submissionStart:function(dateString,timeString){return this.startDatetimeControl.datetime(dateString,timeString)},submissionDue:function(dateString,timeString){return this.dueDatetimeControl.datetime(dateString,timeString)},fileUploadType:function(uploadType){var sel=$("#openassessment_submission_upload_selector",this.settingsElement);if(uploadType!==undefined){sel.val(uploadType)}return sel.val()},fileTypeWhiteList:function(exts){if(exts!==undefined){this.fileTypeWhiteListInputField.set(exts)}return this.fileTypeWhiteListInputField.get()},latexEnabled:function(isEnabled){var sel=$("#openassessment_submission_latex_editor",this.settingsElement);if(isEnabled!==undefined){if(isEnabled){sel.val(1)}else{sel.val(0)}}return sel.val()===1},leaderboardNum:function(num){if(num!==undefined){this.leaderboardIntField.set(num)}return this.leaderboardIntField.get(num)},assessmentsDescription:function(){var assessmentDescList=[];var view=this;$(".openassessment_assessment_module_settings_editor",this.assessmentsElement).each(function(){var asmntView=view.assessmentViews[$(this).attr("id")];if(asmntView.isEnabled()){var description=asmntView.description();description.name=asmntView.name;assessmentDescList.push(description)}});return assessmentDescList},editorAssessmentsOrder:function(){var editorAssessments=[];var view=this;$(".openassessment_assessment_module_settings_editor",this.assessmentsElement).each(function(){var asmntView=view.assessmentViews[$(this).attr("id")];editorAssessments.push(asmntView.name)});return editorAssessments},validate:function(){var isValid=true;isValid=this.startDatetimeControl.validate()&&isValid;isValid=this.dueDatetimeControl.validate()&&isValid;isValid=this.leaderboardIntField.validate()&&isValid;if(this.fileUploadType()==="custom"){isValid=this.fileTypeWhiteListInputField.validate()&&isValid}else{if(this.fileTypeWhiteListInputField.get()&&!this.fileTypeWhiteListInputField.validate()){this.fileTypeWhiteListInputField.set("")}}$.each(this.assessmentViews,function(){if(this.isEnabled()){isValid=this.validate()&&isValid}});return isValid},validationErrors:function(){var errors=[];if(this.startDatetimeControl.validationErrors().length>0){errors.push("Submission start is invalid")}if(this.dueDatetimeControl.validationErrors().length>0){errors.push("Submission due is invalid")}if(this.leaderboardIntField.validationErrors().length>0){errors.push("Leaderboard number is invalid")}if(this.fileTypeWhiteListInputField.validationErrors().length>0){errors=errors.concat(this.fileTypeWhiteListInputField.validationErrors())}$.each(this.assessmentViews,function(){errors=errors.concat(this.validationErrors())});return errors},clearValidationErrors:function(){this.startDatetimeControl.clearValidationErrors();this.dueDatetimeControl.clearValidationErrors();this.leaderboardIntField.clearValidationErrors();this.fileTypeWhiteListInputField.clearValidationErrors();$.each(this.assessmentViews,function(){this.clearValidationErrors()})}};OpenAssessment.ValidationAlert=function(){this.element=$("#openassessment_validation_alert");this.editorElement=$(this.element).parent();this.title=$(".openassessment_alert_title",this.element);this.message=$(".openassessment_alert_message",this.element);this.closeButton=$(".openassessment_alert_close",this.element);this.ALERT_YELLOW="rgb(192, 172, 0)";this.DARK_GREY="#323232"};OpenAssessment.ValidationAlert.prototype={install:function(){var alert=this;this.closeButton.click(function(eventObject){eventObject.preventDefault();alert.hide()});return this},hide:function(){var headerHeight=$("#openassessment_editor_header",this.editorElement).outerHeight();this.element.addClass("covered");var styles={height:"Calc(100% - "+headerHeight+"px)","border-top-right-radius":"3px","border-top-left-radius":"3px"};$(".oa_editor_content_wrapper",this.editorElement).each(function(){$(this).css(styles)});return this},show:function(){var view=this;if(this.isVisible()){$(this.element).animate({"background-color":view.ALERT_YELLOW},300,"swing",function(){$(this).animate({"background-color":view.DARK_GREY},700,"swing")})}else{this.element.removeClass("covered");var alertHeight=this.element.outerHeight();var headerHeight=$("#openassessment_editor_header",this.editorElement).outerHeight();var heightString="Calc(100% - "+(alertHeight+headerHeight)+"px)";var styles={height:heightString,"border-top-right-radius":"0px","border-top-left-radius":"0px"};$(".oa_editor_content_wrapper",this.editorElement).each(function(){$(this).css(styles)})}return this},setMessage:function(newTitle,newMessage){this.title.text(newTitle);this.message.text(newMessage);return this},isVisible:function(){return!this.element.hasClass("covered")},getTitle:function(){return this.title.text()},getMessage:function(){return this.message.text()}};
\ No newline at end of file
...@@ -134,7 +134,7 @@ class TestSelfAssessment(XBlockHandlerTestCase): ...@@ -134,7 +134,7 @@ class TestSelfAssessment(XBlockHandlerTestCase):
del assessment['options_selected'] del assessment['options_selected']
resp = self.request(xblock, 'self_assess', json.dumps(assessment), response_format='json') resp = self.request(xblock, 'self_assess', json.dumps(assessment), response_format='json')
self.assertFalse(resp['success']) self.assertFalse(resp['success'])
self.assertIn('options_selected', resp['msg']) self.assertIn('options', resp['msg'])
@scenario('data/self_assessment_scenario.xml', user_id='Bob') @scenario('data/self_assessment_scenario.xml', user_id='Bob')
def test_self_assess_api_error(self, xblock): def test_self_assess_api_error(self, xblock):
......
...@@ -42,7 +42,7 @@ class TestStaffAssessment(StaffAssessmentTestBase): ...@@ -42,7 +42,7 @@ class TestStaffAssessment(StaffAssessmentTestBase):
self.assertTrue(resp['success']) self.assertTrue(resp['success'])
# Expect that a staff-assessment was created # Expect that a staff-assessment was created
assessment = staff_api.get_latest_assessment(submission['uuid']) assessment = staff_api.get_latest_staff_assessment(submission['uuid'])
self.assertEqual(assessment['submission_uuid'], submission['uuid']) self.assertEqual(assessment['submission_uuid'], submission['uuid'])
self.assertEqual(assessment['points_earned'], 5) self.assertEqual(assessment['points_earned'], 5)
self.assertEqual(assessment['points_possible'], 6) self.assertEqual(assessment['points_possible'], 6)
...@@ -67,7 +67,7 @@ class TestStaffAssessment(StaffAssessmentTestBase): ...@@ -67,7 +67,7 @@ class TestStaffAssessment(StaffAssessmentTestBase):
self.assertEqual(assessment['points_possible'], score['points_possible']) self.assertEqual(assessment['points_possible'], score['points_possible'])
@scenario('data/self_assessment_scenario.xml', user_id='Bob') @scenario('data/self_assessment_scenario.xml', user_id='Bob')
def test_staff_assess_permission_error(self, xblock): def test_permission_error(self, xblock):
# Create a submission for the student # Create a submission for the student
student_item = xblock.get_student_item_dict() student_item = xblock.get_student_item_dict()
xblock.create_submission(student_item, self.SUBMISSION) xblock.create_submission(student_item, self.SUBMISSION)
...@@ -75,7 +75,7 @@ class TestStaffAssessment(StaffAssessmentTestBase): ...@@ -75,7 +75,7 @@ class TestStaffAssessment(StaffAssessmentTestBase):
self.assertIn("You do not have permission", resp) self.assertIn("You do not have permission", resp)
@scenario('data/self_assessment_scenario.xml', user_id='Bob') @scenario('data/self_assessment_scenario.xml', user_id='Bob')
def test_staff_assess_invalid_options(self, xblock): def test_invalid_options(self, xblock):
student_item = xblock.get_student_item_dict() student_item = xblock.get_student_item_dict()
# Create a submission for the student # Create a submission for the student
...@@ -92,7 +92,7 @@ class TestStaffAssessment(StaffAssessmentTestBase): ...@@ -92,7 +92,7 @@ class TestStaffAssessment(StaffAssessmentTestBase):
self.assertIn('msg', resp) self.assertIn('msg', resp)
@scenario('data/self_assessment_scenario.xml', user_id='bob') @scenario('data/self_assessment_scenario.xml', user_id='bob')
def test_staff_assess_assessment_error(self, xblock): def test_assessment_error(self, xblock):
student_item = xblock.get_student_item_dict() student_item = xblock.get_student_item_dict()
# Create a submission for the student # Create a submission for the student
...@@ -112,15 +112,3 @@ class TestStaffAssessment(StaffAssessmentTestBase): ...@@ -112,15 +112,3 @@ class TestStaffAssessment(StaffAssessmentTestBase):
resp = self.request(xblock, 'staff_assess', json.dumps(self.ASSESSMENT), response_format='json') resp = self.request(xblock, 'staff_assess', json.dumps(self.ASSESSMENT), response_format='json')
self.assertFalse(resp['success']) self.assertFalse(resp['success'])
self.assertIn('msg', resp) self.assertIn('msg', resp)
class TestStaffAssessmentRender(StaffAssessmentTestBase):
#TODO: test success when staff assessment template exists
@scenario('data/self_assessment_scenario.xml', user_id='Bob')
def test_render_staff_assessment_permission_error(self, xblock):
# Create a submission for the student
student_item = xblock.get_student_item_dict()
xblock.create_submission(student_item, self.SUBMISSION)
resp = self.request(xblock, 'render_staff_assessment', json.dumps(self.ASSESSMENT))
self.assertIn("You do not have permission", resp)
...@@ -19,6 +19,7 @@ class WorkflowMixin(object): ...@@ -19,6 +19,7 @@ class WorkflowMixin(object):
"self-assessment": "self", "self-assessment": "self",
"peer-assessment": "peer", "peer-assessment": "peer",
"student-training": "training", "student-training": "training",
"staff-assessment": "staff"
} }
@XBlock.json_handler @XBlock.json_handler
......
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