Commit e789c382 by VikParuchuri

Merge pull request #1837 from MITx/fix/vik/sa-and-coe

Fix/vik/sa and coe
parents 2314c609 5125d8b0
...@@ -14,7 +14,7 @@ from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat ...@@ -14,7 +14,7 @@ from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload", V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload",
"skip_spelling_checks", "due", "graceperiod"] "skip_spelling_checks", "due", "graceperiod", "weight"]
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
"student_attempts", "ready_to_reset"] "student_attempts", "ready_to_reset"]
......
...@@ -122,6 +122,7 @@ div.combined-rubric-container { ...@@ -122,6 +122,7 @@ div.combined-rubric-container {
span.rubric-category { span.rubric-category {
font-size: .9em; font-size: .9em;
font-weight: bold;
} }
padding-bottom: 5px; padding-bottom: 5px;
padding-top: 10px; padding-top: 10px;
......
...@@ -90,6 +90,7 @@ class @CombinedOpenEnded ...@@ -90,6 +90,7 @@ class @CombinedOpenEnded
@element=element @element=element
@reinitialize(element) @reinitialize(element)
$(window).keydown @keydown_handler $(window).keydown @keydown_handler
$(window).keyup @keyup_handler
reinitialize: (element) -> reinitialize: (element) ->
@wrapper=$(element).find('section.xmodule_CombinedOpenEndedModule') @wrapper=$(element).find('section.xmodule_CombinedOpenEndedModule')
...@@ -104,6 +105,7 @@ class @CombinedOpenEnded ...@@ -104,6 +105,7 @@ class @CombinedOpenEnded
@location = @el.data('location') @location = @el.data('location')
# set up handlers for click tracking # set up handlers for click tracking
Rubric.initialize(@location) Rubric.initialize(@location)
@is_ctrl = false
@allow_reset = @el.data('allow_reset') @allow_reset = @el.data('allow_reset')
@reset_button = @$('.reset-button') @reset_button = @$('.reset-button')
...@@ -322,6 +324,7 @@ class @CombinedOpenEnded ...@@ -322,6 +324,7 @@ class @CombinedOpenEnded
save_answer: (event) => save_answer: (event) =>
event.preventDefault() event.preventDefault()
max_filesize = 2*1000*1000 #2MB max_filesize = 2*1000*1000 #2MB
pre_can_upload_files = @can_upload_files
if @child_state == 'initial' if @child_state == 'initial'
files = "" files = ""
if @can_upload_files == true if @can_upload_files == true
...@@ -353,6 +356,7 @@ class @CombinedOpenEnded ...@@ -353,6 +356,7 @@ class @CombinedOpenEnded
@find_assessment_elements() @find_assessment_elements()
@rebind() @rebind()
else else
@can_upload_files = pre_can_upload_files
@gentle_alert response.error @gentle_alert response.error
$.ajaxWithPrefix("#{@ajax_url}/save_answer",settings) $.ajaxWithPrefix("#{@ajax_url}/save_answer",settings)
...@@ -360,10 +364,17 @@ class @CombinedOpenEnded ...@@ -360,10 +364,17 @@ class @CombinedOpenEnded
else else
@errors_area.html(@out_of_sync_message) @errors_area.html(@out_of_sync_message)
keydown_handler: (e) => keydown_handler: (event) =>
# only do anything when the key pressed is the 'enter' key #Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed.
if e.which == 13 && @child_state == 'assessing' && Rubric.check_complete() if event.which == 17 && @is_ctrl==false
@save_assessment(e) @is_ctrl=true
else if @is_ctrl==true && event.which == 13 && @child_state == 'assessing' && Rubric.check_complete()
@save_assessment(event)
keyup_handler: (event) =>
#Handle keyup event when ctrl key is released
if event.which == 17 && @is_ctrl==true
@is_ctrl=false
save_assessment: (event) => save_assessment: (event) =>
event.preventDefault() event.preventDefault()
...@@ -482,8 +493,10 @@ class @CombinedOpenEnded ...@@ -482,8 +493,10 @@ class @CombinedOpenEnded
if @accept_file_upload == "True" if @accept_file_upload == "True"
if window.File and window.FileReader and window.FileList and window.Blob if window.File and window.FileReader and window.FileList and window.Blob
@can_upload_files = true @can_upload_files = true
@file_upload_area.html('<input type="file" class="file-upload-box">') @file_upload_area.html('<input type="file" class="file-upload-box"><img class="file-upload-preview" src="#" alt="Uploaded image" />')
@file_upload_area.show() @file_upload_area.show()
$('.file-upload-preview').hide()
$('.file-upload-box').change @preview_image
else else
@gentle_alert 'File uploads are required for this question, but are not supported in this browser. Try the newest version of google chrome. Alternatively, if you have uploaded the image to the web, you can paste a link to it into the answer box.' @gentle_alert 'File uploads are required for this question, but are not supported in this browser. Try the newest version of google chrome. Alternatively, if you have uploaded the image to the web, you can paste a link to it into the answer box.'
...@@ -539,3 +552,28 @@ class @CombinedOpenEnded ...@@ -539,3 +552,28 @@ class @CombinedOpenEnded
log_feedback_selection: (event) -> log_feedback_selection: (event) ->
target_selection = $(event.target).val() target_selection = $(event.target).val()
Logger.log 'oe_feedback_response_selected', {value: target_selection} Logger.log 'oe_feedback_response_selected', {value: target_selection}
remove_attribute: (name) =>
if $('.file-upload-preview').attr(name)
$('.file-upload-preview')[0].removeAttribute(name)
preview_image: () =>
if $('.file-upload-box')[0].files && $('.file-upload-box')[0].files[0]
reader = new FileReader()
reader.onload = (e) =>
max_dim = 150
@remove_attribute('src')
@remove_attribute('height')
@remove_attribute('width')
$('.file-upload-preview').attr('src', e.target.result)
height_px = $('.file-upload-preview')[0].height
width_px = $('.file-upload-preview')[0].width
scale_factor = 0
if height_px>width_px
scale_factor = height_px/max_dim
else
scale_factor = width_px/max_dim
$('.file-upload-preview')[0].width = width_px/scale_factor
$('.file-upload-preview')[0].height = height_px/scale_factor
$('.file-upload-preview').show()
reader.readAsDataURL($('.file-upload-box')[0].files[0])
...@@ -161,6 +161,7 @@ class @PeerGradingProblem ...@@ -161,6 +161,7 @@ class @PeerGradingProblem
constructor: (backend) -> constructor: (backend) ->
@prompt_wrapper = $('.prompt-wrapper') @prompt_wrapper = $('.prompt-wrapper')
@backend = backend @backend = backend
@is_ctrl = false
# get the location of the problem # get the location of the problem
...@@ -183,6 +184,12 @@ class @PeerGradingProblem ...@@ -183,6 +184,12 @@ class @PeerGradingProblem
@grading_message.hide() @grading_message.hide()
@question_header = $('.question-header') @question_header = $('.question-header')
@question_header.click @collapse_question @question_header.click @collapse_question
@flag_submission_confirmation = $('.flag-submission-confirmation')
@flag_submission_confirmation_button = $('.flag-submission-confirmation-button')
@flag_submission_removal_button = $('.flag-submission-removal-button')
@flag_submission_confirmation_button.click @close_dialog_box
@flag_submission_removal_button.click @remove_flag
@grading_wrapper =$('.grading-wrapper') @grading_wrapper =$('.grading-wrapper')
@calibration_feedback_panel = $('.calibration-feedback') @calibration_feedback_panel = $('.calibration-feedback')
...@@ -212,6 +219,7 @@ class @PeerGradingProblem ...@@ -212,6 +219,7 @@ class @PeerGradingProblem
@answer_unknown_checkbox = $('.answer-unknown-checkbox') @answer_unknown_checkbox = $('.answer-unknown-checkbox')
$(window).keydown @keydown_handler $(window).keydown @keydown_handler
$(window).keyup @keyup_handler
@collapse_question() @collapse_question()
...@@ -233,9 +241,13 @@ class @PeerGradingProblem ...@@ -233,9 +241,13 @@ class @PeerGradingProblem
@calibration_interstitial_page.hide() @calibration_interstitial_page.hide()
@is_calibrated_check() @is_calibrated_check()
@flag_student_checkbox.click =>
@flag_box_checked()
@calibration_feedback_button.hide() @calibration_feedback_button.hide()
@calibration_feedback_panel.hide() @calibration_feedback_panel.hide()
@error_container.hide() @error_container.hide()
@flag_submission_confirmation.hide()
@is_calibrated_check() @is_calibrated_check()
...@@ -283,6 +295,17 @@ class @PeerGradingProblem ...@@ -283,6 +295,17 @@ class @PeerGradingProblem
# #
########## ##########
remove_flag: () =>
@flag_student_checkbox.removeAttr("checked")
@close_dialog_box()
close_dialog_box: () =>
$( ".flag-submission-confirmation" ).dialog('close')
flag_box_checked: () =>
if @flag_student_checkbox.is(':checked')
$( ".flag-submission-confirmation" ).dialog({ height: 400, width: 400 })
# called after we perform an is_student_calibrated check # called after we perform an is_student_calibrated check
calibration_check_callback: (response) => calibration_check_callback: (response) =>
if response.success if response.success
...@@ -338,13 +361,19 @@ class @PeerGradingProblem ...@@ -338,13 +361,19 @@ class @PeerGradingProblem
@grade = Rubric.get_total_score() @grade = Rubric.get_total_score()
keydown_handler: (event) => keydown_handler: (event) =>
if event.which == 13 && @submit_button.is(':visible') #Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed.
if event.which == 17 && @is_ctrl==false
@is_ctrl=true
else if event.which == 13 && @submit_button.is(':visible') && @is_ctrl==true
if @calibration if @calibration
@submit_calibration_essay() @submit_calibration_essay()
else else
@submit_grade() @submit_grade()
keyup_handler: (event) =>
#Handle keyup event when ctrl key is released
if event.which == 17 && @is_ctrl==true
@is_ctrl=false
########## ##########
...@@ -443,7 +472,6 @@ class @PeerGradingProblem ...@@ -443,7 +472,6 @@ class @PeerGradingProblem
calibration_wrapper = $('.calibration-feedback-wrapper') calibration_wrapper = $('.calibration-feedback-wrapper')
calibration_wrapper.html("<p>The score you gave was: #{@grade}. The actual score is: #{response.actual_score}</p>") calibration_wrapper.html("<p>The score you gave was: #{@grade}. The actual score is: #{response.actual_score}</p>")
score = parseInt(@grade) score = parseInt(@grade)
actual_score = parseInt(response.actual_score) actual_score = parseInt(response.actual_score)
...@@ -452,6 +480,11 @@ class @PeerGradingProblem ...@@ -452,6 +480,11 @@ class @PeerGradingProblem
else else
calibration_wrapper.append("<p>You may want to review the rubric again.</p>") calibration_wrapper.append("<p>You may want to review the rubric again.</p>")
if response.actual_rubric != undefined
calibration_wrapper.append("<div>Instructor Scored Rubric: #{response.actual_rubric}</div>")
if response.actual_feedback!=undefined
calibration_wrapper.append("<div>Instructor Feedback: #{response.actual_feedback}</div>")
# disable score selection and submission from the grading interface # disable score selection and submission from the grading interface
$("input[name='score-selection']").attr('disabled', true) $("input[name='score-selection']").attr('disabled', true)
@submit_button.hide() @submit_button.hide()
......
...@@ -131,6 +131,7 @@ class CombinedOpenEndedV1Module(): ...@@ -131,6 +131,7 @@ class CombinedOpenEndedV1Module():
self.state = instance_state.get('state', self.INITIAL) self.state = instance_state.get('state', self.INITIAL)
self.student_attempts = instance_state.get('student_attempts', 0) self.student_attempts = instance_state.get('student_attempts', 0)
self.weight = instance_state.get('weight', 1)
#Allow reset is true if student has failed the criteria to move to the next child task #Allow reset is true if student has failed the criteria to move to the next child task
self.ready_to_reset = instance_state.get('ready_to_reset', False) self.ready_to_reset = instance_state.get('ready_to_reset', False)
...@@ -144,7 +145,7 @@ class CombinedOpenEndedV1Module(): ...@@ -144,7 +145,7 @@ class CombinedOpenEndedV1Module():
grace_period_string = self.instance_state.get('graceperiod', None) grace_period_string = self.instance_state.get('graceperiod', None)
try: try:
self.timeinfo = TimeInfo(due_date, grace_period_string) self.timeinfo = TimeInfo(due_date, grace_period_string)
except: except Exception:
log.error("Error parsing due date information in location {0}".format(location)) log.error("Error parsing due date information in location {0}".format(location))
raise raise
self.display_due_date = self.timeinfo.display_due_date self.display_due_date = self.timeinfo.display_due_date
...@@ -362,7 +363,7 @@ class CombinedOpenEndedV1Module(): ...@@ -362,7 +363,7 @@ class CombinedOpenEndedV1Module():
# if link.startswith(XASSET_SRCREF_PREFIX): # if link.startswith(XASSET_SRCREF_PREFIX):
# Placing try except so that if the error is fixed, this code will start working again. # Placing try except so that if the error is fixed, this code will start working again.
return_html = rewrite_links(html, self.rewrite_content_links) return_html = rewrite_links(html, self.rewrite_content_links)
except: except Exception:
pass pass
return return_html return return_html
...@@ -402,6 +403,7 @@ class CombinedOpenEndedV1Module(): ...@@ -402,6 +403,7 @@ class CombinedOpenEndedV1Module():
self.static_data, instance_state=task_state) self.static_data, instance_state=task_state)
last_response = task.latest_answer() last_response = task.latest_answer()
last_score = task.latest_score() last_score = task.latest_score()
all_scores = task.all_scores()
last_post_assessment = task.latest_post_assessment(self.system) last_post_assessment = task.latest_post_assessment(self.system)
last_post_feedback = "" last_post_feedback = ""
feedback_dicts = [{}] feedback_dicts = [{}]
...@@ -417,13 +419,18 @@ class CombinedOpenEndedV1Module(): ...@@ -417,13 +419,18 @@ class CombinedOpenEndedV1Module():
else: else:
last_post_evaluation = task.format_feedback_with_evaluation(self.system, last_post_assessment) last_post_evaluation = task.format_feedback_with_evaluation(self.system, last_post_assessment)
last_post_assessment = last_post_evaluation last_post_assessment = last_post_evaluation
try:
rubric_data = task._parse_score_msg(task.child_history[-1].get('post_assessment', ""), self.system) rubric_data = task._parse_score_msg(task.child_history[-1].get('post_assessment', ""), self.system)
rubric_scores = rubric_data['rubric_scores'] except Exception:
grader_types = rubric_data['grader_types'] log.debug("Could not parse rubric data from child history. "
feedback_items = rubric_data['feedback_items'] "Likely we have not yet initialized a previous step, so this is perfectly fine.")
feedback_dicts = rubric_data['feedback_dicts'] rubric_data = {}
grader_ids = rubric_data['grader_ids'] rubric_scores = rubric_data.get('rubric_scores')
submission_ids = rubric_data['submission_ids'] grader_types = rubric_data.get('grader_types')
feedback_items = rubric_data.get('feedback_items')
feedback_dicts = rubric_data.get('feedback_dicts')
grader_ids = rubric_data.get('grader_ids')
submission_ids = rubric_data.get('submission_ids')
elif task_type == "selfassessment": elif task_type == "selfassessment":
rubric_scores = last_post_assessment rubric_scores = last_post_assessment
grader_types = ['SA'] grader_types = ['SA']
...@@ -441,7 +448,7 @@ class CombinedOpenEndedV1Module(): ...@@ -441,7 +448,7 @@ class CombinedOpenEndedV1Module():
human_state = task.HUMAN_NAMES[state] human_state = task.HUMAN_NAMES[state]
else: else:
human_state = state human_state = state
if len(grader_types) > 0: if grader_types is not None and len(grader_types) > 0:
grader_type = grader_types[0] grader_type = grader_types[0]
else: else:
grader_type = "IN" grader_type = "IN"
...@@ -454,6 +461,7 @@ class CombinedOpenEndedV1Module(): ...@@ -454,6 +461,7 @@ class CombinedOpenEndedV1Module():
last_response_dict = { last_response_dict = {
'response': last_response, 'response': last_response,
'score': last_score, 'score': last_score,
'all_scores': all_scores,
'post_assessment': last_post_assessment, 'post_assessment': last_post_assessment,
'type': task_type, 'type': task_type,
'max_score': max_score, 'max_score': max_score,
...@@ -732,10 +740,37 @@ class CombinedOpenEndedV1Module(): ...@@ -732,10 +740,37 @@ class CombinedOpenEndedV1Module():
""" """
max_score = None max_score = None
score = None score = None
if self.check_if_done_and_scored(): if self.is_scored and self.weight is not None:
last_response = self.get_last_response(self.current_task_number) #Finds the maximum score of all student attempts and keeps it.
max_score = last_response['max_score'] score_mat = []
score = last_response['score'] for i in xrange(0, len(self.task_states)):
#For each task, extract all student scores on that task (each attempt for each task)
last_response = self.get_last_response(i)
max_score = last_response.get('max_score', None)
score = last_response.get('all_scores', None)
if score is not None:
#Convert none scores and weight scores properly
for z in xrange(0, len(score)):
if score[z] is None:
score[z] = 0
score[z] *= float(self.weight)
score_mat.append(score)
if len(score_mat) > 0:
#Currently, assume that the final step is the correct one, and that those are the final scores.
#This will change in the future, which is why the machinery above exists to extract all scores on all steps
#TODO: better final score handling.
scores = score_mat[-1]
score = max(scores)
else:
score = 0
if max_score is not None:
#Weight the max score if it is not None
max_score *= float(self.weight)
else:
#Without a max_score, we cannot have a score!
score = None
score_dict = { score_dict = {
'score': score, 'score': score,
......
...@@ -72,7 +72,8 @@ class OpenEndedChild(object): ...@@ -72,7 +72,8 @@ class OpenEndedChild(object):
try: try:
instance_state = json.loads(instance_state) instance_state = json.loads(instance_state)
except: except:
log.error("Could not load instance state for open ended. Setting it to nothing.: {0}".format(instance_state)) log.error(
"Could not load instance state for open ended. Setting it to nothing.: {0}".format(instance_state))
else: else:
instance_state = {} instance_state = {}
...@@ -81,8 +82,8 @@ class OpenEndedChild(object): ...@@ -81,8 +82,8 @@ class OpenEndedChild(object):
# element. # element.
# Scores are on scale from 0 to max_score # Scores are on scale from 0 to max_score
self.child_history=instance_state.get('child_history',[]) self.child_history = instance_state.get('child_history', [])
self.child_state=instance_state.get('child_state', self.INITIAL) self.child_state = instance_state.get('child_state', self.INITIAL)
self.child_created = instance_state.get('child_created', False) self.child_created = instance_state.get('child_created', False)
self.child_attempts = instance_state.get('child_attempts', 0) self.child_attempts = instance_state.get('child_attempts', 0)
...@@ -162,6 +163,12 @@ class OpenEndedChild(object): ...@@ -162,6 +163,12 @@ class OpenEndedChild(object):
return None return None
return self.child_history[-1].get('score') return self.child_history[-1].get('score')
def all_scores(self):
"""None if not available"""
if not self.child_history:
return None
return [self.child_history[i].get('score') for i in xrange(0, len(self.child_history))]
def latest_post_assessment(self, system): def latest_post_assessment(self, system):
"""Empty string if not available""" """Empty string if not available"""
if not self.child_history: if not self.child_history:
......
...@@ -291,7 +291,7 @@ class SelfAssessmentDescriptor(): ...@@ -291,7 +291,7 @@ class SelfAssessmentDescriptor():
template_dir_name = "selfassessment" template_dir_name = "selfassessment"
def __init__(self, system): def __init__(self, system):
self.system =system self.system = system
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
......
...@@ -15,6 +15,7 @@ from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat ...@@ -15,6 +15,7 @@ from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
from xmodule.fields import Date from xmodule.fields import Date
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
from open_ended_grading_classes import combined_open_ended_rubric
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -178,8 +179,14 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -178,8 +179,14 @@ class PeerGradingModule(PeerGradingFields, XModule):
pass pass
def get_score(self): def get_score(self):
max_score = None
score = None
score_dict = {
'score': score,
'total': max_score,
}
if self.use_for_single_location not in TRUE_DICT or self.is_graded not in TRUE_DICT: if self.use_for_single_location not in TRUE_DICT or self.is_graded not in TRUE_DICT:
return None return score_dict
try: try:
count_graded = self.student_data_for_location['count_graded'] count_graded = self.student_data_for_location['count_graded']
...@@ -198,10 +205,11 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -198,10 +205,11 @@ class PeerGradingModule(PeerGradingFields, XModule):
#Ensures that once a student receives a final score for peer grading, that it does not change. #Ensures that once a student receives a final score for peer grading, that it does not change.
self.student_data_for_location = response self.student_data_for_location = response
score_dict = { if self.weight is not None:
'score': int(count_graded >= count_required and count_graded>0) * int(self.weight), score = int(count_graded >= count_required and count_graded > 0) * float(self.weight)
'total': self.max_grade * int(self.weight), total = self.max_grade * float(self.weight)
} score_dict['score'] = score
score_dict['total'] = total
return score_dict return score_dict
...@@ -384,8 +392,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -384,8 +392,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
# if we can't parse the rubric into HTML, # if we can't parse the rubric into HTML,
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
#This is a dev_facing_error #This is a dev_facing_error
log.exception("Cannot parse rubric string. Raw string: {0}" log.exception("Cannot parse rubric string.")
.format(rubric))
#This is a student_facing_error #This is a student_facing_error
return {'success': False, return {'success': False,
'error': 'Error displaying submission. Please notify course staff.'} 'error': 'Error displaying submission. Please notify course staff.'}
...@@ -425,12 +432,15 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -425,12 +432,15 @@ class PeerGradingModule(PeerGradingFields, XModule):
try: try:
response = self.peer_gs.save_calibration_essay(location, grader_id, calibration_essay_id, response = self.peer_gs.save_calibration_essay(location, grader_id, calibration_essay_id,
submission_key, score, feedback, rubric_scores) submission_key, score, feedback, rubric_scores)
if 'actual_rubric' in response:
rubric_renderer = combined_open_ended_rubric.CombinedOpenEndedRubric(self.system, True)
response['actual_rubric'] = rubric_renderer.render_rubric(response['actual_rubric'])['html']
return response return response
except GradingServiceError: except GradingServiceError:
#This is a dev_facing_error #This is a dev_facing_error
log.exception( log.exception(
"Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format( "Error saving calibration grade, location: {0}, submission_key: {1}, grader_id: {2}".format(
location, submission_id, submission_key, grader_id)) location, submission_key, grader_id))
#This is a student_facing_error #This is a student_facing_error
return self._err_response('There was an error saving your score. Please notify course staff.') return self._err_response('There was an error saving your score. Please notify course staff.')
...@@ -577,5 +587,5 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor): ...@@ -577,5 +587,5 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
stores_state = True stores_state = True
has_score = True has_score = True
always_recalculate_grades=True always_recalculate_grades = True
template_dir_name = "peer_grading" template_dir_name = "peer_grading"
--- ---
metadata: metadata:
display_name: Open Ended Response display_name: Open Ended Response
max_attempts: 1 attempts: 1
is_graded: False is_graded: False
version: 1 version: 1
display_name: Open Ended Response
skip_spelling_checks: False skip_spelling_checks: False
accept_file_upload: False accept_file_upload: False
weight: "" weight: ""
......
--- ---
metadata: metadata:
display_name: Peer Grading Interface display_name: Peer Grading Interface
attempts: 1
use_for_single_location: False use_for_single_location: False
link_to_location: None link_to_location: None
is_graded: False is_graded: False
......
...@@ -323,6 +323,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -323,6 +323,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
's3_interface': test_util_open_ended.S3_INTERFACE, 's3_interface': test_util_open_ended.S3_INTERFACE,
'open_ended_grading_interface': test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE, 'open_ended_grading_interface': test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
'skip_basic_checks': False, 'skip_basic_checks': False,
'is_graded': True,
} }
oeparam = etree.XML(''' oeparam = etree.XML('''
...@@ -358,7 +359,8 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -358,7 +359,8 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
combinedoe_container = CombinedOpenEndedModule(test_system, combinedoe_container = CombinedOpenEndedModule(test_system,
location, location,
descriptor, descriptor,
model_data={'data': full_definition, 'weight' : '1'}) model_data={'data': full_definition, 'weight': '1'})
def setUp(self): def setUp(self):
# TODO: this constructor call is definitely wrong, but neither branch # TODO: this constructor call is definitely wrong, but neither branch
...@@ -369,7 +371,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -369,7 +371,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
self.descriptor, self.descriptor,
static_data=self.static_data, static_data=self.static_data,
metadata=self.metadata, metadata=self.metadata,
instance_state={}) instance_state=self.static_data)
def test_get_tag_name(self): def test_get_tag_name(self):
name = self.combinedoe.get_tag_name("<t>Tag</t>") name = self.combinedoe.get_tag_name("<t>Tag</t>")
...@@ -405,4 +407,65 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -405,4 +407,65 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
def test_container_weight(self): def test_container_weight(self):
weight = self.combinedoe_container.weight weight = self.combinedoe_container.weight
self.assertEqual(weight,1) self.assertEqual(weight, 1)
def test_container_child_weight(self):
weight = self.combinedoe_container.child_module.weight
self.assertEqual(weight, 1)
def test_get_score(self):
score_dict = self.combinedoe.get_score()
self.assertEqual(score_dict['score'], 0)
self.assertEqual(score_dict['total'], 1)
def test_alternate_orderings(self):
t1 = self.task_xml1
t2 = self.task_xml2
xml_to_test = [[t1], [t2], [t1, t1], [t1, t2], [t2, t2], [t2, t1], [t1, t2, t1]]
for xml in xml_to_test:
definition = {'prompt': etree.XML(self.prompt), 'rubric': etree.XML(self.rubric), 'task_xml': xml}
descriptor = Mock(data=definition)
combinedoe = CombinedOpenEndedV1Module(self.test_system,
self.location,
definition,
descriptor,
static_data=self.static_data,
metadata=self.metadata,
instance_state=self.static_data)
changed = combinedoe.update_task_states()
self.assertFalse(changed)
def test_get_score_realistic(self):
instance_state = r"""{"ready_to_reset": false, "skip_spelling_checks": true, "current_task_number": 1, "weight": 5.0, "graceperiod": "1 day 12 hours 59 minutes 59 seconds", "is_graded": "True", "task_states": ["{\"child_created\": false, \"child_attempts\": 4, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the <bg>table below . starting mass</bg> g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups <bg>procedure , describe what additional</bg> information you would need in order to replicate the <bs>expe</bs>\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"\", \"post_assessment\": \"[3]\", \"score\": 3}], \"max_score\": 3, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the <bg>table below . starting mass</bg> g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups <bg>procedure , describe what additional</bg> information you would need in order to replicate the <bs>expe</bs>\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"{\\\"submission_id\\\": 3098, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the <bg>table below . starting mass</bg> g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3235, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"{\\\"submission_id\\\": 3099, \\\"score\\\": 3, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"to replicate the experiment , the procedure would require <bg>more detail . one</bg> piece of information <bg>that is omitted is the</bg> amount of vinegar used in the experiment . it is also important to know what temperature the experiment was kept at during the hours . finally , the procedure needs to include details about the experiment , for example if the whole sample must be submerged .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3237, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>3</score><option points='0'>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"{\\\"submission_id\\\": 3100, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"e the mass of four different samples . pour vinegar in <bg>each of four separate</bg> , but identical , containers . place a sample of one material into one container and label . repeat with remaining samples , placing a single sample into a single container . after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . \\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3239, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}, {\"answer\": \"\", \"post_assessment\": \"{\\\"submission_id\\\": 3101, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"invalid essay .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3241, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"<rubric><category><description>Response Quality</description><score>0</score><option points='0'>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option><option points='1'>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option><option points='2'>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option><option points='3'>The response is correct, complete, and contains evidence of higher-order thinking.</option></category></rubric>\\\"}\", \"score\": 0}], \"max_score\": 3, \"child_state\": \"done\"}"], "attempts": "10000", "student_attempts": 0, "due": null, "state": "done", "accept_file_upload": false, "display_name": "Science Question -- Machine Assessed"}"""
instance_state = json.loads(instance_state)
rubric = """
<rubric>
<rubric>
<category>
<description>Response Quality</description>
<option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option>
<option>The response is a marginal answer to the question. It may contain some elements of a proficient response, but it is inaccurate or incomplete.</option>
<option>The response is a proficient answer to the question. It is generally correct, although it may contain minor inaccuracies. There is limited evidence of higher-order thinking.</option>
<option>The response is correct, complete, and contains evidence of higher-order thinking.</option>
</category>
</rubric>
</rubric>
"""
definition = {'prompt': etree.XML(self.prompt), 'rubric': etree.XML(rubric),
'task_xml': [self.task_xml1, self.task_xml2]}
descriptor = Mock(data=definition)
combinedoe = CombinedOpenEndedV1Module(self.test_system,
self.location,
definition,
descriptor,
static_data=self.static_data,
metadata=self.metadata,
instance_state=instance_state)
score_dict = combinedoe.get_score()
self.assertEqual(score_dict['score'], 15.0)
self.assertEqual(score_dict['total'], 15.0)
...@@ -310,19 +310,24 @@ def save_grade(request, course_id): ...@@ -310,19 +310,24 @@ def save_grade(request, course_id):
if request.method != 'POST': if request.method != 'POST':
raise Http404 raise Http404
p = request.POST
required = set(['score', 'feedback', 'submission_id', 'location', 'submission_flagged', 'rubric_scores[]']) required = set(['score', 'feedback', 'submission_id', 'location', 'submission_flagged'])
actual = set(request.POST.keys()) skipped = 'skipped' in p
#If the instructor has skipped grading the submission, then there will not be any rubric scores.
#Only add in the rubric scores if the instructor has not skipped.
if not skipped:
required|=set(['rubric_scores[]'])
actual = set(p.keys())
missing = required - actual missing = required - actual
if len(missing) > 0: if len(missing) > 0:
return _err_response('Missing required keys {0}'.format( return _err_response('Missing required keys {0}'.format(
', '.join(missing))) ', '.join(missing)))
grader_id = unique_id_for_user(request.user) grader_id = unique_id_for_user(request.user)
p = request.POST
location = p['location'] location = p['location']
skipped = 'skipped' in p
try: try:
result_json = staff_grading_service().save_grade(course_id, result_json = staff_grading_service().save_grade(course_id,
......
...@@ -97,7 +97,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase): ...@@ -97,7 +97,7 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
self.assertIsNotNone(d['rubric']) self.assertIsNotNone(d['rubric'])
def test_save_grade(self): def save_grade_base(self,skip=False):
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id}) url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id})
...@@ -108,12 +108,20 @@ class TestStaffGradingService(LoginEnrollmentTestCase): ...@@ -108,12 +108,20 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
'location': self.location, 'location': self.location,
'submission_flagged': "true", 'submission_flagged': "true",
'rubric_scores[]': ['1', '2']} 'rubric_scores[]': ['1', '2']}
if skip:
data.update({'skipped' : True})
r = self.check_for_post_code(200, url, data) r = self.check_for_post_code(200, url, data)
d = json.loads(r.content) d = json.loads(r.content)
self.assertTrue(d['success'], str(d)) self.assertTrue(d['success'], str(d))
self.assertEquals(d['submission_id'], self.mock_service.cnt) self.assertEquals(d['submission_id'], self.mock_service.cnt)
def test_save_grade(self):
self.save_grade_base(skip=False)
def test_save_grade_skip(self):
self.save_grade_base(skip=True)
def test_get_problem_list(self): def test_get_problem_list(self):
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
......
...@@ -185,6 +185,7 @@ class @StaffGrading ...@@ -185,6 +185,7 @@ class @StaffGrading
$(window).keydown @keydown_handler $(window).keydown @keydown_handler
$(window).keyup @keyup_handler
@question_header = $('.question-header') @question_header = $('.question-header')
@question_header.click @collapse_question @question_header.click @collapse_question
@collapse_question() @collapse_question()
...@@ -206,6 +207,7 @@ class @StaffGrading ...@@ -206,6 +207,7 @@ class @StaffGrading
@num_pending = 0 @num_pending = 0
@score_lst = [] @score_lst = []
@grade = null @grade = null
@is_ctrl = false
@problems = null @problems = null
...@@ -231,10 +233,18 @@ class @StaffGrading ...@@ -231,10 +233,18 @@ class @StaffGrading
@state = state_graded @state = state_graded
@submit_button.show() @submit_button.show()
keydown_handler: (e) => keydown_handler: (event) =>
if e.which == 13 && !@list_view && Rubric.check_complete() #Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed.
if event.which == 17 && @is_ctrl==false
@is_ctrl=true
else if @is_ctrl==true && event.which == 13 && !@list_view && Rubric.check_complete()
@submit_and_get_next() @submit_and_get_next()
keyup_handler: (event) =>
#Handle keyup event when ctrl key is released
if event.which == 17 && @is_ctrl==true
@is_ctrl=false
set_button_text: (text) => set_button_text: (text) =>
@action_button.attr('value', text) @action_button.attr('value', text)
......
...@@ -43,8 +43,8 @@ ...@@ -43,8 +43,8 @@
<p>Please include some written feedback as well.</p> <p>Please include some written feedback as well.</p>
<textarea name="feedback" placeholder="Feedback for student" <textarea name="feedback" placeholder="Feedback for student"
class="feedback-area" cols="70" ></textarea> class="feedback-area" cols="70" ></textarea>
<div class="flag-student-container"> <input type="checkbox" class="flag-checkbox" value="student_is_flagged"> Flag this submission for review by course staff (use if the submission contains inappropriate content) </div> <div class="flag-student-container"> This submission has explicit or pornographic content : <input type="checkbox" class="flag-checkbox" value="student_is_flagged"> </div>
<div class="answer-unknown-container"> <input type="checkbox" class="answer-unknown-checkbox" value="answer_is_unknown"> I do not know how to grade this question </div> <div class="answer-unknown-container"> I do not know how to grade this question : <input type="checkbox" class="answer-unknown-checkbox" value="answer_is_unknown"></div>
</div> </div>
...@@ -82,6 +82,19 @@ ...@@ -82,6 +82,19 @@
<input type="button" class="calibration-interstitial-page-button" value="Start learning to grade" name="calibration-interstitial-page-button" /> <input type="button" class="calibration-interstitial-page-button" value="Start learning to grade" name="calibration-interstitial-page-button" />
</section> </section>
<!-- Flag submission confirmation dialog -->
<section class="flag-submission-confirmation">
<h4> Are you sure that you want to flag this submission?</h4>
<p>
You are about to flag a submission. You should only flag a submission that contains explicit or offensive content. If the submission is not addressed to the question or is incorrect, you should give it a score of zero and accompanying feedback instead of flagging it.
</p>
<div>
<input type="button" class="flag-submission-removal-button" value="Remove Flag" name="calibration-interstitial-page-button" />
<input type="button" class="flag-submission-confirmation-button" value="Keep Flag" name="calibration-interstitial-page-button" />
</div>
</section>
<input type="button" value="Go Back" class="action-button" name="back" /> <input type="button" value="Go Back" class="action-button" name="back" />
</div> </div>
</section> </section>
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