Commit cf7a34ae by Diana Huang

Merge pull request #1524 from MITx/feature/vik/check-if-submission-allowed

Feature/vik/check if submission allowed
parents 1f0c84ed d5ba5457
...@@ -108,11 +108,13 @@ class CombinedOpenEndedModule(XModule): ...@@ -108,11 +108,13 @@ class CombinedOpenEndedModule(XModule):
instance_state = {} instance_state = {}
self.version = self.metadata.get('version', DEFAULT_VERSION) self.version = self.metadata.get('version', DEFAULT_VERSION)
version_error_string = "Version of combined open ended module {0} is not correct. Going with version {1}"
if not isinstance(self.version, basestring): if not isinstance(self.version, basestring):
try: try:
self.version = str(self.version) self.version = str(self.version)
except: except:
log.error("Version {0} is not correct. Going with version {1}".format(self.version, DEFAULT_VERSION)) #This is a dev_facing_error
log.info(version_error_string.format(self.version, DEFAULT_VERSION))
self.version = DEFAULT_VERSION self.version = DEFAULT_VERSION
versions = [i[0] for i in VERSION_TUPLES] versions = [i[0] for i in VERSION_TUPLES]
...@@ -122,7 +124,8 @@ class CombinedOpenEndedModule(XModule): ...@@ -122,7 +124,8 @@ class CombinedOpenEndedModule(XModule):
try: try:
version_index = versions.index(self.version) version_index = versions.index(self.version)
except: except:
log.error("Version {0} is not correct. Going with version {1}".format(self.version, DEFAULT_VERSION)) #This is a dev_facing_error
log.error(version_error_string.format(self.version, DEFAULT_VERSION))
self.version = DEFAULT_VERSION self.version = DEFAULT_VERSION
version_index = versions.index(self.version) version_index = versions.index(self.version)
......
...@@ -89,6 +89,8 @@ class @CombinedOpenEnded ...@@ -89,6 +89,8 @@ class @CombinedOpenEnded
@can_upload_files = false @can_upload_files = false
@open_ended_child= @$('.open-ended-child') @open_ended_child= @$('.open-ended-child')
@out_of_sync_message = 'The problem state got out of sync. Try reloading the page.'
if @task_number>1 if @task_number>1
@prompt_hide() @prompt_hide()
else if @task_number==1 and @child_state!='initial' else if @task_number==1 and @child_state!='initial'
...@@ -293,7 +295,7 @@ class @CombinedOpenEnded ...@@ -293,7 +295,7 @@ class @CombinedOpenEnded
$.ajaxWithPrefix("#{@ajax_url}/save_answer",settings) $.ajaxWithPrefix("#{@ajax_url}/save_answer",settings)
else else
@errors_area.html('Problem state got out of sync. Try reloading the page.') @errors_area.html(@out_of_sync_message)
save_assessment: (event) => save_assessment: (event) =>
event.preventDefault() event.preventDefault()
...@@ -315,7 +317,7 @@ class @CombinedOpenEnded ...@@ -315,7 +317,7 @@ class @CombinedOpenEnded
else else
@errors_area.html(response.error) @errors_area.html(response.error)
else else
@errors_area.html('Problem state got out of sync. Try reloading the page.') @errors_area.html(@out_of_sync_message)
save_hint: (event) => save_hint: (event) =>
event.preventDefault() event.preventDefault()
...@@ -330,7 +332,7 @@ class @CombinedOpenEnded ...@@ -330,7 +332,7 @@ class @CombinedOpenEnded
else else
@errors_area.html(response.error) @errors_area.html(response.error)
else else
@errors_area.html('Problem state got out of sync. Try reloading the page.') @errors_area.html(@out_of_sync_message)
skip_post_assessment: => skip_post_assessment: =>
if @child_state == 'post_assessment' if @child_state == 'post_assessment'
...@@ -342,7 +344,7 @@ class @CombinedOpenEnded ...@@ -342,7 +344,7 @@ class @CombinedOpenEnded
else else
@errors_area.html(response.error) @errors_area.html(response.error)
else else
@errors_area.html('Problem state got out of sync. Try reloading the page.') @errors_area.html(@out_of_sync_message)
reset: (event) => reset: (event) =>
event.preventDefault() event.preventDefault()
...@@ -362,7 +364,7 @@ class @CombinedOpenEnded ...@@ -362,7 +364,7 @@ class @CombinedOpenEnded
else else
@errors_area.html(response.error) @errors_area.html(response.error)
else else
@errors_area.html('Problem state got out of sync. Try reloading the page.') @errors_area.html(@out_of_sync_message)
next_problem: => next_problem: =>
if @child_state == 'done' if @child_state == 'done'
...@@ -385,7 +387,7 @@ class @CombinedOpenEnded ...@@ -385,7 +387,7 @@ class @CombinedOpenEnded
else else
@errors_area.html(response.error) @errors_area.html(response.error)
else else
@errors_area.html('Problem state got out of sync. Try reloading the page.') @errors_area.html(@out_of_sync_message)
gentle_alert: (msg) => gentle_alert: (msg) =>
if @el.find('.open-ended-alert').length if @el.find('.open-ended-alert').length
......
...@@ -175,6 +175,7 @@ class @PeerGradingProblem ...@@ -175,6 +175,7 @@ class @PeerGradingProblem
@prompt_container = $('.prompt-container') @prompt_container = $('.prompt-container')
@rubric_container = $('.rubric-container') @rubric_container = $('.rubric-container')
@flag_student_container = $('.flag-student-container') @flag_student_container = $('.flag-student-container')
@answer_unknown_container = $('.answer-unknown-container')
@calibration_panel = $('.calibration-panel') @calibration_panel = $('.calibration-panel')
@grading_panel = $('.grading-panel') @grading_panel = $('.grading-panel')
@content_panel = $('.content-panel') @content_panel = $('.content-panel')
...@@ -208,6 +209,7 @@ class @PeerGradingProblem ...@@ -208,6 +209,7 @@ class @PeerGradingProblem
@interstitial_page_button = $('.interstitial-page-button') @interstitial_page_button = $('.interstitial-page-button')
@calibration_interstitial_page_button = $('.calibration-interstitial-page-button') @calibration_interstitial_page_button = $('.calibration-interstitial-page-button')
@flag_student_checkbox = $('.flag-checkbox') @flag_student_checkbox = $('.flag-checkbox')
@answer_unknown_checkbox = $('.answer-unknown-checkbox')
@collapse_question() @collapse_question()
Collapsible.setCollapsibles(@content_panel) Collapsible.setCollapsibles(@content_panel)
...@@ -262,6 +264,7 @@ class @PeerGradingProblem ...@@ -262,6 +264,7 @@ class @PeerGradingProblem
submission_key: @submission_key_input.val() submission_key: @submission_key_input.val()
feedback: @feedback_area.val() feedback: @feedback_area.val()
submission_flagged: @flag_student_checkbox.is(':checked') submission_flagged: @flag_student_checkbox.is(':checked')
answer_unknown: @answer_unknown_checkbox.is(':checked')
return data return data
...@@ -360,6 +363,8 @@ class @PeerGradingProblem ...@@ -360,6 +363,8 @@ class @PeerGradingProblem
@calibration_panel.find('.grading-text').hide() @calibration_panel.find('.grading-text').hide()
@grading_panel.find('.grading-text').hide() @grading_panel.find('.grading-text').hide()
@flag_student_container.hide() @flag_student_container.hide()
@answer_unknown_container.hide()
@feedback_area.val("") @feedback_area.val("")
@submit_button.unbind('click') @submit_button.unbind('click')
...@@ -388,6 +393,7 @@ class @PeerGradingProblem ...@@ -388,6 +393,7 @@ class @PeerGradingProblem
@calibration_panel.find('.grading-text').show() @calibration_panel.find('.grading-text').show()
@grading_panel.find('.grading-text').show() @grading_panel.find('.grading-text').show()
@flag_student_container.show() @flag_student_container.show()
@answer_unknown_container.show()
@feedback_area.val("") @feedback_area.val("")
@submit_button.unbind('click') @submit_button.unbind('click')
......
...@@ -19,7 +19,7 @@ log = logging.getLogger("mitx.courseware") ...@@ -19,7 +19,7 @@ log = logging.getLogger("mitx.courseware")
# Set the default number of max attempts. Should be 1 for production # Set the default number of max attempts. Should be 1 for production
# Set higher for debugging/testing # Set higher for debugging/testing
# attempts specified in xml definition overrides this. # attempts specified in xml definition overrides this.
MAX_ATTEMPTS = 10000 MAX_ATTEMPTS = 1
# Set maximum available number of points. # Set maximum available number of points.
# Overriden by max_score specified in xml. # Overriden by max_score specified in xml.
...@@ -149,6 +149,7 @@ class CombinedOpenEndedV1Module(): ...@@ -149,6 +149,7 @@ class CombinedOpenEndedV1Module():
self.skip_basic_checks = self.metadata.get('skip_spelling_checks', SKIP_BASIC_CHECKS) self.skip_basic_checks = self.metadata.get('skip_spelling_checks', SKIP_BASIC_CHECKS)
display_due_date_string = self.metadata.get('due', None) display_due_date_string = self.metadata.get('due', None)
grace_period_string = self.metadata.get('graceperiod', None) grace_period_string = self.metadata.get('graceperiod', None)
try: try:
self.timeinfo = TimeInfo(display_due_date_string, grace_period_string) self.timeinfo = TimeInfo(display_due_date_string, grace_period_string)
...@@ -645,7 +646,10 @@ class CombinedOpenEndedV1Module(): ...@@ -645,7 +646,10 @@ class CombinedOpenEndedV1Module():
if self.attempts > self.max_attempts: if self.attempts > self.max_attempts:
return { return {
'success': False, 'success': False,
'error': 'Too many attempts.' #This is a student_facing_error
'error': ('You have attempted this question {0} times. '
'You are only allowed to attempt it {1} times.').format(
self.attempts, self.max_attempts)
} }
self.state = self.INITIAL self.state = self.INITIAL
self.allow_reset = False self.allow_reset = False
...@@ -784,7 +788,8 @@ class CombinedOpenEndedV1Descriptor(XmlDescriptor, EditingDescriptor): ...@@ -784,7 +788,8 @@ class CombinedOpenEndedV1Descriptor(XmlDescriptor, EditingDescriptor):
expected_children = ['task', 'rubric', 'prompt'] expected_children = ['task', 'rubric', 'prompt']
for child in expected_children: for child in expected_children:
if len(xml_object.xpath(child)) == 0: if len(xml_object.xpath(child)) == 0:
raise ValueError("Combined Open Ended definition must include at least one '{0}' tag".format(child)) #This is a staff_facing_error
raise ValueError("Combined Open Ended definition must include at least one '{0}' tag. Contact the learning sciences group for assistance.".format(child))
def parse_task(k): def parse_task(k):
"""Assumes that xml_object has child k""" """Assumes that xml_object has child k"""
...@@ -809,4 +814,4 @@ class CombinedOpenEndedV1Descriptor(XmlDescriptor, EditingDescriptor): ...@@ -809,4 +814,4 @@ class CombinedOpenEndedV1Descriptor(XmlDescriptor, EditingDescriptor):
for child in ['task']: for child in ['task']:
add_child(child) add_child(child)
return elt return elt
\ No newline at end of file
...@@ -4,7 +4,6 @@ from lxml import etree ...@@ -4,7 +4,6 @@ from lxml import etree
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
GRADER_TYPE_IMAGE_DICT = { GRADER_TYPE_IMAGE_DICT = {
'8B' : '/static/images/random_grading_icon.png',
'SA' : '/static/images/self_assessment_icon.png', 'SA' : '/static/images/self_assessment_icon.png',
'PE' : '/static/images/peer_grading_icon.png', 'PE' : '/static/images/peer_grading_icon.png',
'ML' : '/static/images/ml_grading_icon.png', 'ML' : '/static/images/ml_grading_icon.png',
...@@ -13,7 +12,6 @@ GRADER_TYPE_IMAGE_DICT = { ...@@ -13,7 +12,6 @@ GRADER_TYPE_IMAGE_DICT = {
} }
HUMAN_GRADER_TYPE = { HUMAN_GRADER_TYPE = {
'8B' : 'Magic-8-Ball-Assessment',
'SA' : 'Self-Assessment', 'SA' : 'Self-Assessment',
'PE' : 'Peer-Assessment', 'PE' : 'Peer-Assessment',
'IN' : 'Instructor-Assessment', 'IN' : 'Instructor-Assessment',
...@@ -71,8 +69,9 @@ class CombinedOpenEndedRubric(object): ...@@ -71,8 +69,9 @@ class CombinedOpenEndedRubric(object):
}) })
success = True success = True
except: except:
error_message = "[render_rubric] Could not parse the rubric with xml: {0}".format(rubric_xml) #This is a staff_facing_error
log.error(error_message) error_message = "[render_rubric] Could not parse the rubric with xml: {0}. Contact the learning sciences group for assistance.".format(rubric_xml)
log.exception(error_message)
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
return {'success' : success, 'html' : html, 'rubric_scores' : rubric_scores} return {'success' : success, 'html' : html, 'rubric_scores' : rubric_scores}
...@@ -81,7 +80,8 @@ class CombinedOpenEndedRubric(object): ...@@ -81,7 +80,8 @@ class CombinedOpenEndedRubric(object):
success = rubric_dict['success'] success = rubric_dict['success']
rubric_feedback = rubric_dict['html'] rubric_feedback = rubric_dict['html']
if not success: if not success:
error_message = "Could not parse rubric : {0} for location {1}".format(rubric_string, location.url()) #This is a staff_facing_error
error_message = "Could not parse rubric : {0} for location {1}. Contact the learning sciences group for assistance.".format(rubric_string, location.url())
log.error(error_message) log.error(error_message)
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
...@@ -90,13 +90,15 @@ class CombinedOpenEndedRubric(object): ...@@ -90,13 +90,15 @@ class CombinedOpenEndedRubric(object):
for category in rubric_categories: for category in rubric_categories:
total = total + len(category['options']) - 1 total = total + len(category['options']) - 1
if len(category['options']) > (max_score_allowed + 1): if len(category['options']) > (max_score_allowed + 1):
error_message = "Number of score points in rubric {0} higher than the max allowed, which is {1}".format( #This is a staff_facing_error
error_message = "Number of score points in rubric {0} higher than the max allowed, which is {1}. Contact the learning sciences group for assistance.".format(
len(category['options']), max_score_allowed) len(category['options']), max_score_allowed)
log.error(error_message) log.error(error_message)
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
if total != max_score: if total != max_score:
error_msg = "The max score {0} for problem {1} does not match the total number of points in the rubric {2}".format( #This is a staff_facing_error
error_msg = "The max score {0} for problem {1} does not match the total number of points in the rubric {2}. Contact the learning sciences group for assistance.".format(
max_score, location, total) max_score, location, total)
log.error(error_msg) log.error(error_msg)
raise RubricParsingError(error_msg) raise RubricParsingError(error_msg)
...@@ -118,7 +120,8 @@ class CombinedOpenEndedRubric(object): ...@@ -118,7 +120,8 @@ class CombinedOpenEndedRubric(object):
categories = [] categories = []
for category in element: for category in element:
if category.tag != 'category': if category.tag != 'category':
raise RubricParsingError("[extract_categories] Expected a <category> tag: got {0} instead".format(category.tag)) #This is a staff_facing_error
raise RubricParsingError("[extract_categories] Expected a <category> tag: got {0} instead. Contact the learning sciences group for assistance.".format(category.tag))
else: else:
categories.append(self.extract_category(category)) categories.append(self.extract_category(category))
return categories return categories
...@@ -144,12 +147,14 @@ class CombinedOpenEndedRubric(object): ...@@ -144,12 +147,14 @@ class CombinedOpenEndedRubric(object):
self.has_score = True self.has_score = True
# if we are missing the score tag and we are expecting one # if we are missing the score tag and we are expecting one
elif self.has_score: elif self.has_score:
raise RubricParsingError("[extract_category] Category {0} is missing a score".format(descriptionxml.text)) #This is a staff_facing_error
raise RubricParsingError("[extract_category] Category {0} is missing a score. Contact the learning sciences group for assistance.".format(descriptionxml.text))
# parse description # parse description
if descriptionxml.tag != 'description': if descriptionxml.tag != 'description':
raise RubricParsingError("[extract_category]: expected description tag, got {0} instead".format(descriptionxml.tag)) #This is a staff_facing_error
raise RubricParsingError("[extract_category]: expected description tag, got {0} instead. Contact the learning sciences group for assistance.".format(descriptionxml.tag))
description = descriptionxml.text description = descriptionxml.text
...@@ -159,7 +164,8 @@ class CombinedOpenEndedRubric(object): ...@@ -159,7 +164,8 @@ class CombinedOpenEndedRubric(object):
# parse options # parse options
for option in optionsxml: for option in optionsxml:
if option.tag != 'option': if option.tag != 'option':
raise RubricParsingError("[extract_category]: expected option tag, got {0} instead".format(option.tag)) #This is a staff_facing_error
raise RubricParsingError("[extract_category]: expected option tag, got {0} instead. Contact the learning sciences group for assistance.".format(option.tag))
else: else:
pointstr = option.get("points") pointstr = option.get("points")
if pointstr: if pointstr:
...@@ -168,7 +174,8 @@ class CombinedOpenEndedRubric(object): ...@@ -168,7 +174,8 @@ class CombinedOpenEndedRubric(object):
try: try:
points = int(pointstr) points = int(pointstr)
except ValueError: except ValueError:
raise RubricParsingError("[extract_category]: expected points to have int, got {0} instead".format(pointstr)) #This is a staff_facing_error
raise RubricParsingError("[extract_category]: expected points to have int, got {0} instead. Contact the learning sciences group for assistance.".format(pointstr))
elif autonumbering: elif autonumbering:
# use the generated one if we're in the right mode # use the generated one if we're in the right mode
points = cur_points points = cur_points
...@@ -200,7 +207,6 @@ class CombinedOpenEndedRubric(object): ...@@ -200,7 +207,6 @@ class CombinedOpenEndedRubric(object):
for grader_type in tuple[3]: for grader_type in tuple[3]:
rubric_categories[i]['options'][j]['grader_types'].append(grader_type) rubric_categories[i]['options'][j]['grader_types'].append(grader_type)
log.debug(rubric_categories)
html = self.system.render_template('open_ended_combined_rubric.html', html = self.system.render_template('open_ended_combined_rubric.html',
{'categories': rubric_categories, {'categories': rubric_categories,
'has_score': True, 'has_score': True,
...@@ -219,13 +225,15 @@ class CombinedOpenEndedRubric(object): ...@@ -219,13 +225,15 @@ class CombinedOpenEndedRubric(object):
Validates a set of options. This can and should be extended to filter out other bad edge cases Validates a set of options. This can and should be extended to filter out other bad edge cases
''' '''
if len(options) == 0: if len(options) == 0:
raise RubricParsingError("[extract_category]: no options associated with this category") #This is a staff_facing_error
raise RubricParsingError("[extract_category]: no options associated with this category. Contact the learning sciences group for assistance.")
if len(options) == 1: if len(options) == 1:
return return
prev = options[0]['points'] prev = options[0]['points']
for option in options[1:]: for option in options[1:]:
if prev == option['points']: if prev == option['points']:
raise RubricParsingError("[extract_category]: found duplicate point values between two different options") #This is a staff_facing_error
raise RubricParsingError("[extract_category]: found duplicate point values between two different options. Contact the learning sciences group for assistance.")
else: else:
prev = option['points'] prev = option['points']
...@@ -241,11 +249,14 @@ class CombinedOpenEndedRubric(object): ...@@ -241,11 +249,14 @@ class CombinedOpenEndedRubric(object):
""" """
success = False success = False
if len(scores)==0: if len(scores)==0:
log.error("Score length is 0.") #This is a dev_facing_error
log.error("Score length is 0 when trying to reformat rubric scores for rendering.")
return success, "" return success, ""
if len(scores) != len(score_types) or len(feedback_types) != len(scores): if len(scores) != len(score_types) or len(feedback_types) != len(scores):
log.error("Length mismatches.") #This is a dev_facing_error
log.error("Length mismatches when trying to reformat rubric scores for rendering. "
"Scores: {0}, Score Types: {1} Feedback Types: {2}".format(scores, score_types, feedback_types))
return success, "" return success, ""
score_lists = [] score_lists = []
......
...@@ -51,6 +51,8 @@ class GradingService(object): ...@@ -51,6 +51,8 @@ class GradingService(object):
r = self._try_with_login(op) r = self._try_with_login(op)
except (RequestException, ConnectionError, HTTPError) as err: except (RequestException, ConnectionError, HTTPError) as err:
# reraise as promised GradingServiceError, but preserve stacktrace. # reraise as promised GradingServiceError, but preserve stacktrace.
#This is a dev_facing_error
log.error("Problem posting data to the grading controller. URL: {0}, data: {1}".format(url, data))
raise GradingServiceError, str(err), sys.exc_info()[2] raise GradingServiceError, str(err), sys.exc_info()[2]
return r.text return r.text
...@@ -67,6 +69,8 @@ class GradingService(object): ...@@ -67,6 +69,8 @@ class GradingService(object):
r = self._try_with_login(op) r = self._try_with_login(op)
except (RequestException, ConnectionError, HTTPError) as err: except (RequestException, ConnectionError, HTTPError) as err:
# reraise as promised GradingServiceError, but preserve stacktrace. # reraise as promised GradingServiceError, but preserve stacktrace.
#This is a dev_facing_error
log.error("Problem getting data from the grading controller. URL: {0}, params: {1}".format(url, params))
raise GradingServiceError, str(err), sys.exc_info()[2] raise GradingServiceError, str(err), sys.exc_info()[2]
return r.text return r.text
...@@ -119,11 +123,13 @@ class GradingService(object): ...@@ -119,11 +123,13 @@ class GradingService(object):
return response_json return response_json
# if we can't parse the rubric into HTML, # if we can't parse the rubric into HTML,
except etree.XMLSyntaxError, RubricParsingError: except etree.XMLSyntaxError, RubricParsingError:
#This is a dev_facing_error
log.exception("Cannot parse rubric string. Raw string: {0}" log.exception("Cannot parse rubric string. Raw string: {0}"
.format(rubric)) .format(rubric))
return {'success': False, return {'success': False,
'error': 'Error displaying submission'} 'error': 'Error displaying submission'}
except ValueError: except ValueError:
#This is a dev_facing_error
log.exception("Error parsing response: {0}".format(response)) log.exception("Error parsing response: {0}".format(response))
return {'success': False, return {'success': False,
'error': "Error displaying submission"} 'error': "Error displaying submission"}
...@@ -251,8 +251,9 @@ def upload_to_s3(file_to_upload, keyname, s3_interface): ...@@ -251,8 +251,9 @@ def upload_to_s3(file_to_upload, keyname, s3_interface):
return True, public_url return True, public_url
except: except:
error_message = "Could not connect to S3." #This is a dev_facing_error
log.exception(error_message) error_message = "Could not connect to S3 to upload peer grading image. Trying to utilize bucket: {0}".format(bucketname.lower())
log.error(error_message)
return False, error_message return False, error_message
......
...@@ -59,12 +59,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -59,12 +59,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
self.submission_id = None self.submission_id = None
self.grader_id = None self.grader_id = None
error_message = "No {0} found in problem xml for open ended problem. Contact the learning sciences group for assistance."
if oeparam is None: if oeparam is None:
raise ValueError("No oeparam found in problem xml.") #This is a staff_facing_error
raise ValueError(error_message.format('oeparam'))
if self.prompt is None: if self.prompt is None:
raise ValueError("No prompt found in problem xml.") raise ValueError(error_message.format('prompt'))
if self.rubric is None: if self.rubric is None:
raise ValueError("No rubric found in problem xml.") raise ValueError(error_message.format('rubric'))
self._parse(oeparam, self.prompt, self.rubric, system) self._parse(oeparam, self.prompt, self.rubric, system)
...@@ -73,6 +75,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -73,6 +75,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
self.send_to_grader(self.latest_answer(), system) self.send_to_grader(self.latest_answer(), system)
self.created = False self.created = False
def _parse(self, oeparam, prompt, rubric, system): def _parse(self, oeparam, prompt, rubric, system):
''' '''
Parse OpenEndedResponse XML: Parse OpenEndedResponse XML:
...@@ -98,7 +101,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -98,7 +101,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# __init__ adds it (easiest way to get problem location into # __init__ adds it (easiest way to get problem location into
# response types) # response types)
except TypeError, ValueError: except TypeError, ValueError:
log.exception("Grader payload %r is not a json object!", grader_payload) #This is a dev_facing_error
log.exception("Grader payload from external open ended grading server is not a json object! Object: {0}".format(grader_payload))
self.initial_display = find_with_default(oeparam, 'initial_display', '') self.initial_display = find_with_default(oeparam, 'initial_display', '')
self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.') self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.')
...@@ -141,17 +145,20 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -141,17 +145,20 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
survey_responses = event_info['survey_responses'] survey_responses = event_info['survey_responses']
for tag in ['feedback', 'submission_id', 'grader_id', 'score']: for tag in ['feedback', 'submission_id', 'grader_id', 'score']:
if tag not in survey_responses: if tag not in survey_responses:
return {'success': False, 'msg': "Could not find needed tag {0}".format(tag)} #This is a student_facing_error
return {'success': False, 'msg': "Could not find needed tag {0} in the survey responses. Please try submitting again.".format(tag)}
try: try:
submission_id = int(survey_responses['submission_id']) submission_id = int(survey_responses['submission_id'])
grader_id = int(survey_responses['grader_id']) grader_id = int(survey_responses['grader_id'])
feedback = str(survey_responses['feedback'].encode('ascii', 'ignore')) feedback = str(survey_responses['feedback'].encode('ascii', 'ignore'))
score = int(survey_responses['score']) score = int(survey_responses['score'])
except: except:
#This is a dev_facing_error
error_message = ("Could not parse submission id, grader id, " error_message = ("Could not parse submission id, grader id, "
"or feedback from message_post ajax call. Here is the message data: {0}".format( "or feedback from message_post ajax call. Here is the message data: {0}".format(
survey_responses)) survey_responses))
log.exception(error_message) log.exception(error_message)
#This is a student_facing_error
return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."} return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."}
qinterface = system.xqueue['interface'] qinterface = system.xqueue['interface']
...@@ -188,6 +195,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -188,6 +195,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
self.state = self.DONE self.state = self.DONE
#This is a student_facing_message
return {'success': success, 'msg': "Successfully submitted your feedback."} return {'success': success, 'msg': "Successfully submitted your feedback."}
def send_to_grader(self, submission, system): def send_to_grader(self, submission, system):
...@@ -337,18 +345,22 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -337,18 +345,22 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
for tag in ['success', 'feedback', 'submission_id', 'grader_id']: for tag in ['success', 'feedback', 'submission_id', 'grader_id']:
if tag not in response_items: if tag not in response_items:
return format_feedback('errors', 'Error getting feedback') #This is a student_facing_error
return format_feedback('errors', 'Error getting feedback from grader.')
feedback_items = response_items['feedback'] feedback_items = response_items['feedback']
try: try:
feedback = json.loads(feedback_items) feedback = json.loads(feedback_items)
except (TypeError, ValueError): except (TypeError, ValueError):
log.exception("feedback_items have invalid json %r", feedback_items) #This is a dev_facing_error
return format_feedback('errors', 'Could not parse feedback') log.exception("feedback_items from external open ended grader have invalid json {0}".format(feedback_items))
#This is a student_facing_error
return format_feedback('errors', 'Error getting feedback from grader.')
if response_items['success']: if response_items['success']:
if len(feedback) == 0: if len(feedback) == 0:
return format_feedback('errors', 'No feedback available') #This is a student_facing_error
return format_feedback('errors', 'No feedback available from grader.')
for tag in do_not_render: for tag in do_not_render:
if tag in feedback: if tag in feedback:
...@@ -357,6 +369,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -357,6 +369,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
feedback_lst = sorted(feedback.items(), key=get_priority) feedback_lst = sorted(feedback.items(), key=get_priority)
feedback_list_part1 = u"\n".join(format_feedback(k, v) for k, v in feedback_lst) feedback_list_part1 = u"\n".join(format_feedback(k, v) for k, v in feedback_lst)
else: else:
#This is a student_facing_error
feedback_list_part1 = format_feedback('errors', response_items['feedback']) feedback_list_part1 = format_feedback('errors', response_items['feedback'])
feedback_list_part2 = (u"\n".join([format_feedback_hidden(feedback_type, value) feedback_list_part2 = (u"\n".join([format_feedback_hidden(feedback_type, value)
...@@ -432,14 +445,16 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -432,14 +445,16 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
try: try:
score_result = json.loads(score_msg) score_result = json.loads(score_msg)
except (TypeError, ValueError): except (TypeError, ValueError):
error_message = ("External grader message should be a JSON-serialized dict." #This is a dev_facing_error
error_message = ("External open ended grader message should be a JSON-serialized dict."
" Received score_msg = {0}".format(score_msg)) " Received score_msg = {0}".format(score_msg))
log.error(error_message) log.error(error_message)
fail['feedback'] = error_message fail['feedback'] = error_message
return fail return fail
if not isinstance(score_result, dict): if not isinstance(score_result, dict):
error_message = ("External grader message should be a JSON-serialized dict." #This is a dev_facing_error
error_message = ("External open ended grader message should be a JSON-serialized dict."
" Received score_result = {0}".format(score_result)) " Received score_result = {0}".format(score_result))
log.error(error_message) log.error(error_message)
fail['feedback'] = error_message fail['feedback'] = error_message
...@@ -447,7 +462,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -447,7 +462,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']: for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']:
if tag not in score_result: if tag not in score_result:
error_message = ("External grader message is missing required tag: {0}" #This is a dev_facing_error
error_message = ("External open ended grader message is missing required tag: {0}"
.format(tag)) .format(tag))
log.error(error_message) log.error(error_message)
fail['feedback'] = error_message fail['feedback'] = error_message
...@@ -564,7 +580,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -564,7 +580,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
} }
if dispatch not in handlers: if dispatch not in handlers:
return 'Error' #This is a dev_facing_error
log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
#This is a dev_facing_error
return json.dumps({'error': 'Error handling action. Please try again.', 'success' : False})
before = self.get_progress() before = self.get_progress()
d = handlers[dispatch](get, system) d = handlers[dispatch](get, system)
...@@ -605,15 +624,21 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -605,15 +624,21 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
success, get = self.append_image_to_student_answer(get) success, get = self.append_image_to_student_answer(get)
error_message = "" error_message = ""
if success: if success:
get['student_answer'] = OpenEndedModule.sanitize_html(get['student_answer']) success, allowed_to_submit, error_message = self.check_if_student_can_submit()
self.new_history_entry(get['student_answer']) if allowed_to_submit:
self.send_to_grader(get['student_answer'], system) get['student_answer'] = OpenEndedModule.sanitize_html(get['student_answer'])
self.change_state(self.ASSESSING) self.new_history_entry(get['student_answer'])
self.send_to_grader(get['student_answer'], system)
self.change_state(self.ASSESSING)
else:
#Error message already defined
success = False
else: else:
#This is a student_facing_error
error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box." error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box."
return { return {
'success': True, 'success': success,
'error': error_message, 'error': error_message,
'student_response': get['student_answer'] 'student_response': get['student_answer']
} }
...@@ -690,7 +715,8 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -690,7 +715,8 @@ class OpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
""" """
for child in ['openendedparam']: for child in ['openendedparam']:
if len(xml_object.xpath(child)) != 1: if len(xml_object.xpath(child)) != 1:
raise ValueError("Open Ended definition must include exactly one '{0}' tag".format(child)) #This is a staff_facing_error
raise ValueError("Open Ended definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format(child))
def parse(k): def parse(k):
"""Assumes that xml_object has child k""" """Assumes that xml_object has child k"""
......
...@@ -22,6 +22,7 @@ from xmodule.stringify import stringify_children ...@@ -22,6 +22,7 @@ from xmodule.stringify import stringify_children
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
from capa.util import * from capa.util import *
from peer_grading_service import PeerGradingService
from datetime import datetime from datetime import datetime
...@@ -104,7 +105,9 @@ class OpenEndedChild(object): ...@@ -104,7 +105,9 @@ class OpenEndedChild(object):
# Used for progress / grading. Currently get credit just for # Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect). # completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = static_data['max_score'] self._max_score = static_data['max_score']
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
self.system = system
self.setup_response(system, location, definition, descriptor) self.setup_response(system, location, definition, descriptor)
def setup_response(self, system, location, definition, descriptor): def setup_response(self, system, location, definition, descriptor):
...@@ -127,12 +130,14 @@ class OpenEndedChild(object): ...@@ -127,12 +130,14 @@ class OpenEndedChild(object):
if self.closed(): if self.closed():
return True, { return True, {
'success': False, 'success': False,
'error': 'This problem is now closed.' #This is a student_facing_error
'error': 'The problem close date has passed, and this problem is now closed.'
} }
elif self.attempts > self.max_attempts: elif self.attempts > self.max_attempts:
return True, { return True, {
'success': False, 'success': False,
'error': 'Too many attempts.' #This is a student_facing_error
'error': 'You have attempted this problem {0} times. You are allowed {1} attempts.'.format(self.attempts, self.max_attempts)
} }
else: else:
return False, {} return False, {}
...@@ -251,7 +256,8 @@ class OpenEndedChild(object): ...@@ -251,7 +256,8 @@ class OpenEndedChild(object):
try: try:
return Progress(self.get_score()['score'], self._max_score) return Progress(self.get_score()['score'], self._max_score)
except Exception as err: except Exception as err:
log.exception("Got bad progress") #This is a dev_facing_error
log.exception("Got bad progress from open ended child module. Max Score: {1}".format(self._max_score))
return None return None
return None return None
...@@ -259,10 +265,12 @@ class OpenEndedChild(object): ...@@ -259,10 +265,12 @@ class OpenEndedChild(object):
""" """
return dict out-of-sync error message, and also log. return dict out-of-sync error message, and also log.
""" """
log.warning("Assessment module state out sync. state: %r, get: %r. %s", #This is a dev_facing_error
log.warning("Open ended child state out sync. state: %r, get: %r. %s",
self.state, get, msg) self.state, get, msg)
#This is a student_facing_error
return {'success': False, return {'success': False,
'error': 'The problem state got out-of-sync'} 'error': 'The problem state got out-of-sync. Please try reloading the page.'}
def get_html(self): def get_html(self):
""" """
...@@ -408,3 +416,33 @@ class OpenEndedChild(object): ...@@ -408,3 +416,33 @@ class OpenEndedChild(object):
success = True success = True
return success, string return success, string
def check_if_student_can_submit(self):
location = self.system.location.url()
student_id = self.system.anonymous_student_id
success = False
allowed_to_submit = True
response = {}
#This is a student_facing_error
error_string = ("You need to peer grade {0} more in order to make another submission. "
"You have graded {1}, and {2} are required. You have made {3} successful peer grading submissions.")
try:
response = self.peer_gs.get_data_for_location(location, student_id)
count_graded = response['count_graded']
count_required = response['count_required']
student_sub_count = response['student_sub_count']
success = True
except:
#This is a dev_facing_error
log.error("Could not contact external open ended graders for location {0} and student {1}".format(location,student_id))
#This is a student_facing_error
error_message = "Could not contact the graders. Please notify course staff."
return success, allowed_to_submit, error_message
if count_graded>=count_required:
return success, allowed_to_submit, ""
else:
allowed_to_submit = False
#This is a student_facing_error
error_message = error_string.format(count_required-count_graded, count_graded, count_required, student_sub_count)
return success, allowed_to_submit, error_message
...@@ -30,8 +30,8 @@ class PeerGradingService(GradingService): ...@@ -30,8 +30,8 @@ class PeerGradingService(GradingService):
self.system = system self.system = system
def get_data_for_location(self, problem_location, student_id): def get_data_for_location(self, problem_location, student_id):
response = self.get(self.get_data_for_location_url, params = {'location': problem_location, 'student_id': student_id}
{'location': problem_location, 'student_id': student_id}) response = self.get(self.get_data_for_location_url, params)
return self.try_to_decode(response) return self.try_to_decode(response)
def get_next_submission(self, problem_location, grader_id): def get_next_submission(self, problem_location, grader_id):
......
...@@ -90,7 +90,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -90,7 +90,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
} }
if dispatch not in handlers: if dispatch not in handlers:
return 'Error' #This is a dev_facing_error
log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
#This is a dev_facing_error
return json.dumps({'error': 'Error handling action. Please try again.', 'success' : False})
before = self.get_progress() before = self.get_progress()
d = handlers[dispatch](get, system) d = handlers[dispatch](get, system)
...@@ -123,7 +126,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -123,7 +126,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
elif self.state in (self.POST_ASSESSMENT, self.DONE): elif self.state in (self.POST_ASSESSMENT, self.DONE):
context['read_only'] = True context['read_only'] = True
else: else:
raise ValueError("Illegal state '%r'" % self.state) #This is a dev_facing_error
raise ValueError("Self assessment module is in an illegal state '{0}'".format(self.state))
return system.render_template('self_assessment_rubric.html', context) return system.render_template('self_assessment_rubric.html', context)
...@@ -148,7 +152,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -148,7 +152,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
elif self.state == self.DONE: elif self.state == self.DONE:
context['read_only'] = True context['read_only'] = True
else: else:
raise ValueError("Illegal state '%r'" % self.state) #This is a dev_facing_error
raise ValueError("Self Assessment module is in an illegal state '{0}'".format(self.state))
return system.render_template('self_assessment_hint.html', context) return system.render_template('self_assessment_hint.html', context)
...@@ -177,10 +182,16 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -177,10 +182,16 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
# add new history element with answer and empty score and hint. # add new history element with answer and empty score and hint.
success, get = self.append_image_to_student_answer(get) success, get = self.append_image_to_student_answer(get)
if success: if success:
get['student_answer'] = SelfAssessmentModule.sanitize_html(get['student_answer']) success, allowed_to_submit, error_message = self.check_if_student_can_submit()
self.new_history_entry(get['student_answer']) if allowed_to_submit:
self.change_state(self.ASSESSING) get['student_answer'] = SelfAssessmentModule.sanitize_html(get['student_answer'])
self.new_history_entry(get['student_answer'])
self.change_state(self.ASSESSING)
else:
#Error message already defined
success = False
else: else:
#This is a student_facing_error
error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box." error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box."
return { return {
...@@ -214,7 +225,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -214,7 +225,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
for i in xrange(0,len(score_list)): for i in xrange(0,len(score_list)):
score_list[i] = int(score_list[i]) score_list[i] = int(score_list[i])
except ValueError: except ValueError:
return {'success': False, 'error': "Non-integer score value, or no score list"} #This is a dev_facing_error
log.error("Non-integer score value passed to save_assessment ,or no score list present.")
#This is a student_facing_error
return {'success': False, 'error': "Error saving your score. Please notify course staff."}
#Record score as assessment and rubric scores as post assessment #Record score as assessment and rubric scores as post assessment
self.record_latest_score(score) self.record_latest_score(score)
...@@ -256,6 +270,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -256,6 +270,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
try: try:
rubric_scores = json.loads(latest_post_assessment) rubric_scores = json.loads(latest_post_assessment)
except: except:
#This is a dev_facing_error
log.error("Cannot parse rubric scores in self assessment module from {0}".format(latest_post_assessment)) log.error("Cannot parse rubric scores in self assessment module from {0}".format(latest_post_assessment))
rubric_scores = [] rubric_scores = []
return [rubric_scores] return [rubric_scores]
...@@ -287,7 +302,8 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -287,7 +302,8 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
expected_children = [] expected_children = []
for child in expected_children: for child in expected_children:
if len(xml_object.xpath(child)) != 1: if len(xml_object.xpath(child)) != 1:
raise ValueError("Self assessment definition must include exactly one '{0}' tag".format(child)) #This is a staff_facing_error
raise ValueError("Self assessment definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format(child))
def parse(k): def parse(k):
"""Assumes that xml_object has child k""" """Assumes that xml_object has child k"""
......
...@@ -24,6 +24,8 @@ TRUE_DICT = [True, "True", "true", "TRUE"] ...@@ -24,6 +24,8 @@ TRUE_DICT = [True, "True", "true", "TRUE"]
MAX_SCORE = 1 MAX_SCORE = 1
IS_GRADED = True IS_GRADED = True
EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please notify course staff."
class PeerGradingModule(XModule): class PeerGradingModule(XModule):
_VERSION = 1 _VERSION = 1
...@@ -145,7 +147,10 @@ class PeerGradingModule(XModule): ...@@ -145,7 +147,10 @@ class PeerGradingModule(XModule):
} }
if dispatch not in handlers: if dispatch not in handlers:
return 'Error' #This is a dev_facing_error
log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
#This is a dev_facing_error
return json.dumps({'error': 'Error handling action. Please try again.', 'success' : False})
d = handlers[dispatch](get) d = handlers[dispatch](get)
...@@ -163,6 +168,7 @@ class PeerGradingModule(XModule): ...@@ -163,6 +168,7 @@ class PeerGradingModule(XModule):
count_required = response['count_required'] count_required = response['count_required']
success = True success = True
except GradingServiceError: except GradingServiceError:
#This is a dev_facing_error
log.exception("Error getting location data from controller for location {0}, student {1}" log.exception("Error getting location data from controller for location {0}, student {1}"
.format(location, student_id)) .format(location, student_id))
...@@ -188,6 +194,7 @@ class PeerGradingModule(XModule): ...@@ -188,6 +194,7 @@ class PeerGradingModule(XModule):
count_graded = response['count_graded'] count_graded = response['count_graded']
count_required = response['count_required'] count_required = response['count_required']
if count_required > 0 and count_graded >= count_required: if count_required > 0 and count_graded >= count_required:
#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 = { score_dict = {
...@@ -237,10 +244,12 @@ class PeerGradingModule(XModule): ...@@ -237,10 +244,12 @@ class PeerGradingModule(XModule):
response = self.peer_gs.get_next_submission(location, grader_id) response = self.peer_gs.get_next_submission(location, grader_id)
return response return response
except GradingServiceError: except GradingServiceError:
#This is a dev_facing_error
log.exception("Error getting next submission. server url: {0} location: {1}, grader_id: {2}" log.exception("Error getting next submission. server url: {0} location: {1}, grader_id: {2}"
.format(self.peer_gs.url, location, grader_id)) .format(self.peer_gs.url, location, grader_id))
#This is a student_facing_error
return {'success': False, return {'success': False,
'error': 'Could not connect to grading service'} 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR}
def save_grade(self, get): def save_grade(self, get):
""" """
...@@ -277,14 +286,16 @@ class PeerGradingModule(XModule): ...@@ -277,14 +286,16 @@ class PeerGradingModule(XModule):
score, feedback, submission_key, rubric_scores, submission_flagged) score, feedback, submission_key, rubric_scores, submission_flagged)
return response return response
except GradingServiceError: except GradingServiceError:
log.exception("""Error saving grade. server url: {0}, location: {1}, submission_id:{2}, #This is a dev_facing_error
log.exception("""Error saving grade to open ended grading service. server url: {0}, location: {1}, submission_id:{2},
submission_key: {3}, score: {4}""" submission_key: {3}, score: {4}"""
.format(self.peer_gs.url, .format(self.peer_gs.url,
location, submission_id, submission_key, score) location, submission_id, submission_key, score)
) )
#This is a student_facing_error
return { return {
'success': False, 'success': False,
'error': 'Could not connect to grading service' 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR
} }
def is_student_calibrated(self, get): def is_student_calibrated(self, get):
...@@ -317,11 +328,13 @@ class PeerGradingModule(XModule): ...@@ -317,11 +328,13 @@ class PeerGradingModule(XModule):
response = self.peer_gs.is_student_calibrated(location, grader_id) response = self.peer_gs.is_student_calibrated(location, grader_id)
return response return response
except GradingServiceError: except GradingServiceError:
log.exception("Error from grading service. server url: {0}, grader_id: {0}, location: {1}" #This is a dev_facing_error
log.exception("Error from open ended grading service. server url: {0}, grader_id: {0}, location: {1}"
.format(self.peer_gs.url, grader_id, location)) .format(self.peer_gs.url, grader_id, location))
#This is a student_facing_error
return { return {
'success': False, 'success': False,
'error': 'Could not connect to grading service' 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR
} }
def show_calibration_essay(self, get): def show_calibration_essay(self, get):
...@@ -360,16 +373,20 @@ class PeerGradingModule(XModule): ...@@ -360,16 +373,20 @@ class PeerGradingModule(XModule):
response = self.peer_gs.show_calibration_essay(location, grader_id) response = self.peer_gs.show_calibration_essay(location, grader_id)
return response return response
except GradingServiceError: except GradingServiceError:
log.exception("Error from grading service. server url: {0}, location: {0}" #This is a dev_facing_error
log.exception("Error from open ended grading service. server url: {0}, location: {0}"
.format(self.peer_gs.url, location)) .format(self.peer_gs.url, location))
#This is a student_facing_error
return {'success': False, return {'success': False,
'error': 'Could not connect to grading service'} 'error': EXTERNAL_GRADER_NO_CONTACT_ERROR}
# 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
log.exception("Cannot parse rubric string. Raw string: {0}" log.exception("Cannot parse rubric string. Raw string: {0}"
.format(rubric)) .format(rubric))
#This is a student_facing_error
return {'success': False, return {'success': False,
'error': 'Error displaying submission'} 'error': 'Error displaying submission. Please notify course staff.'}
def save_calibration_essay(self, get): def save_calibration_essay(self, get):
...@@ -408,8 +425,10 @@ class PeerGradingModule(XModule): ...@@ -408,8 +425,10 @@ class PeerGradingModule(XModule):
submission_key, score, feedback, rubric_scores) submission_key, score, feedback, rubric_scores)
return response return response
except GradingServiceError: except GradingServiceError:
#This is a dev_facing_error
log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id)) log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id))
return self._err_response('Could not connect to grading service') #This is a student_facing_error
return self._err_response('There was an error saving your score. Please notify course staff.')
def peer_grading_closed(self): def peer_grading_closed(self):
''' '''
...@@ -440,11 +459,13 @@ class PeerGradingModule(XModule): ...@@ -440,11 +459,13 @@ class PeerGradingModule(XModule):
problem_list = problem_list_dict['problem_list'] problem_list = problem_list_dict['problem_list']
except GradingServiceError: except GradingServiceError:
error_text = "Error occured while contacting the grading service" #This is a student_facing_error
error_text = EXTERNAL_GRADER_NO_CONTACT_ERROR
success = False success = False
# catch error if if the json loads fails # catch error if if the json loads fails
except ValueError: except ValueError:
error_text = "Could not get problem list" #This is a student_facing_error
error_text = "Could not get list of problems to peer grade. Please notify course staff."
success = False success = False
...@@ -502,6 +523,8 @@ class PeerGradingModule(XModule): ...@@ -502,6 +523,8 @@ class PeerGradingModule(XModule):
if get == None or get.get('location') == None: if get == None or get.get('location') == None:
if not self.use_for_single_location: if not self.use_for_single_location:
#This is an error case, because it must be set to use a single location to be called without get parameters #This is an error case, because it must be set to use a single location to be called without get parameters
#This is a dev_facing_error
log.error("Peer grading problem in peer_grading_module called with no get parameters, but use_for_single_location is False.")
return {'html': "", 'success': False} return {'html': "", 'success': False}
problem_location = self.link_to_location problem_location = self.link_to_location
...@@ -566,7 +589,8 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -566,7 +589,8 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
expected_children = [] expected_children = []
for child in expected_children: for child in expected_children:
if len(xml_object.xpath(child)) == 0: if len(xml_object.xpath(child)) == 0:
raise ValueError("Peer grading definition must include at least one '{0}' tag".format(child)) #This is a staff_facing_error
raise ValueError("Peer grading definition must include at least one '{0}' tag. Contact the learning sciences group for assistance.".format(child))
def parse_task(k): def parse_task(k):
"""Assumes that xml_object has child k""" """Assumes that xml_object has child k"""
......
...@@ -19,6 +19,15 @@ import xmodule ...@@ -19,6 +19,15 @@ import xmodule
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from mock import Mock from mock import Mock
open_ended_grading_interface = {
'url': 'http://sandbox-grader-001.m.edx.org/peer_grading',
'username': 'incorrect_user',
'password': 'incorrect_pass',
'staff_grading' : 'staff_grading',
'peer_grading' : 'peer_grading',
'grading_controller' : 'grading_controller'
}
test_system = ModuleSystem( test_system = ModuleSystem(
ajax_url='courses/course_id/modx/a_location', ajax_url='courses/course_id/modx/a_location',
track_function=Mock(), track_function=Mock(),
...@@ -31,7 +40,8 @@ test_system = ModuleSystem( ...@@ -31,7 +40,8 @@ test_system = ModuleSystem(
debug=True, debug=True,
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10}, xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id='student' anonymous_student_id='student',
open_ended_grading_interface= open_ended_grading_interface
) )
......
...@@ -45,7 +45,8 @@ def staff_grading_notifications(course, user): ...@@ -45,7 +45,8 @@ def staff_grading_notifications(course, user):
except: except:
#Non catastrophic error, so no real action #Non catastrophic error, so no real action
notifications = {} notifications = {}
log.info("Problem with getting notifications from staff grading service.") #This is a dev_facing_error
log.info("Problem with getting notifications from staff grading service for course {0} user {1}.".format(course_id, student_id))
if pending_grading: if pending_grading:
img_path = "/static/images/grading_notification.png" img_path = "/static/images/grading_notification.png"
...@@ -78,7 +79,8 @@ def peer_grading_notifications(course, user): ...@@ -78,7 +79,8 @@ def peer_grading_notifications(course, user):
except: except:
#Non catastrophic error, so no real action #Non catastrophic error, so no real action
notifications = {} notifications = {}
log.info("Problem with getting notifications from peer grading service.") #This is a dev_facing_error
log.info("Problem with getting notifications from peer grading service for course {0} user {1}.".format(course_id, student_id))
if pending_grading: if pending_grading:
img_path = "/static/images/grading_notification.png" img_path = "/static/images/grading_notification.png"
...@@ -123,7 +125,8 @@ def combined_notifications(course, user): ...@@ -123,7 +125,8 @@ def combined_notifications(course, user):
except: except:
#Non catastrophic error, so no real action #Non catastrophic error, so no real action
notifications = {} notifications = {}
log.exception("Problem with getting notifications from controller query service.") #This is a dev_facing_error
log.exception("Problem with getting notifications from controller query service for course {0} user {1}.".format(course_id, student_id))
if pending_grading: if pending_grading:
img_path = "/static/images/grading_notification.png" img_path = "/static/images/grading_notification.png"
......
...@@ -18,6 +18,7 @@ from mitxmako.shortcuts import render_to_string ...@@ -18,6 +18,7 @@ from mitxmako.shortcuts import render_to_string
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
STAFF_ERROR_MESSAGE = 'Could not contact the external grading server. Please contact the development team. If you do not have a point of contact, you can contact Vik at vik@edx.org.'
class MockStaffGradingService(object): class MockStaffGradingService(object):
""" """
...@@ -254,10 +255,12 @@ def get_problem_list(request, course_id): ...@@ -254,10 +255,12 @@ def get_problem_list(request, course_id):
return HttpResponse(response, return HttpResponse(response,
mimetype="application/json") mimetype="application/json")
except GradingServiceError: except GradingServiceError:
log.exception("Error from grading service. server url: {0}" #This is a dev_facing_error
log.exception("Error from staff grading service in open ended grading. server url: {0}"
.format(staff_grading_service().url)) .format(staff_grading_service().url))
#This is a staff_facing_error
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'error': 'Could not connect to grading service'})) 'error': STAFF_ERROR_MESSAGE}))
def _get_next(course_id, grader_id, location): def _get_next(course_id, grader_id, location):
...@@ -267,10 +270,12 @@ def _get_next(course_id, grader_id, location): ...@@ -267,10 +270,12 @@ def _get_next(course_id, grader_id, location):
try: try:
return staff_grading_service().get_next(course_id, location, grader_id) return staff_grading_service().get_next(course_id, location, grader_id)
except GradingServiceError: except GradingServiceError:
log.exception("Error from grading service. server url: {0}" #This is a dev facing error
log.exception("Error from staff grading service in open ended grading. server url: {0}"
.format(staff_grading_service().url)) .format(staff_grading_service().url))
#This is a staff_facing_error
return json.dumps({'success': False, return json.dumps({'success': False,
'error': 'Could not connect to grading service'}) 'error': STAFF_ERROR_MESSAGE})
@expect_json @expect_json
...@@ -316,18 +321,23 @@ def save_grade(request, course_id): ...@@ -316,18 +321,23 @@ def save_grade(request, course_id):
p.getlist('rubric_scores[]'), p.getlist('rubric_scores[]'),
p['submission_flagged']) p['submission_flagged'])
except GradingServiceError: except GradingServiceError:
log.exception("Error saving grade") #This is a dev_facing_error
return _err_response('Could not connect to grading service') log.exception("Error saving grade in the staff grading interface in open ended grading. Request: {0} Course ID: {1}".format(request, course_id))
#This is a staff_facing_error
return _err_response(STAFF_ERROR_MESSAGE)
try: try:
result = json.loads(result_json) result = json.loads(result_json)
except ValueError: except ValueError:
log.exception("save_grade returned broken json: %s", result_json) #This is a dev_facing_error
return _err_response('Grading service returned mal-formatted data.') log.exception("save_grade returned broken json in the staff grading interface in open ended grading: {0}".format(result_json))
#This is a staff_facing_error
return _err_response(STAFF_ERROR_MESSAGE)
if not result.get('success', False): if not result.get('success', False):
log.warning('Got success=False from grading service. Response: %s', result_json) #This is a dev_facing_error
return _err_response('Grading service failed') log.warning('Got success=False from staff grading service in open ended grading. Response: {0}'.format(result_json))
return _err_response(STAFF_ERROR_MESSAGE)
# Ok, save_grade seemed to work. Get the next submission to grade. # Ok, save_grade seemed to work. Get the next submission to grade.
return HttpResponse(_get_next(course_id, grader_id, location), return HttpResponse(_get_next(course_id, grader_id, location),
......
...@@ -60,6 +60,8 @@ ALERT_DICT = { ...@@ -60,6 +60,8 @@ ALERT_DICT = {
'Flagged Submissions': "Submissions have been flagged for review" 'Flagged Submissions': "Submissions have been flagged for review"
} }
STUDENT_ERROR_MESSAGE = "Error occured while contacting the grading service. Please notify course staff."
STAFF_ERROR_MESSAGE = "Error occured while contacting the grading service. Please notify the development team. If you do not have a point of contact, please email Vik at vik@edx.org"
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def staff_grading(request, course_id): def staff_grading(request, course_id):
...@@ -96,7 +98,9 @@ def peer_grading(request, course_id): ...@@ -96,7 +98,9 @@ def peer_grading(request, course_id):
return HttpResponseRedirect(problem_url) return HttpResponseRedirect(problem_url)
except: except:
#This is a student_facing_error
error_message = "Error with initializing peer grading. Centralized module does not exist. Please contact course staff." error_message = "Error with initializing peer grading. Centralized module does not exist. Please contact course staff."
#This is a dev_facing_error
log.exception(error_message + "Current course is: {0}".format(course_id)) log.exception(error_message + "Current course is: {0}".format(course_id))
return HttpResponse(error_message) return HttpResponse(error_message)
...@@ -132,30 +136,34 @@ def student_problem_list(request, course_id): ...@@ -132,30 +136,34 @@ def student_problem_list(request, course_id):
problem_list = [] problem_list = []
base_course_url = reverse('courses') base_course_url = reverse('courses')
#try: try:
problem_list_json = controller_qs.get_grading_status_list(course_id, unique_id_for_user(request.user)) problem_list_json = controller_qs.get_grading_status_list(course_id, unique_id_for_user(request.user))
problem_list_dict = json.loads(problem_list_json) problem_list_dict = json.loads(problem_list_json)
success = problem_list_dict['success'] success = problem_list_dict['success']
if 'error' in problem_list_dict: if 'error' in problem_list_dict:
error_text = problem_list_dict['error'] error_text = problem_list_dict['error']
problem_list = [] problem_list = []
else: else:
problem_list = problem_list_dict['problem_list'] problem_list = problem_list_dict['problem_list']
for i in xrange(0, len(problem_list)): for i in xrange(0, len(problem_list)):
problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location']) problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location'])
problem_url = generate_problem_url(problem_url_parts, base_course_url) problem_url = generate_problem_url(problem_url_parts, base_course_url)
problem_list[i].update({'actual_url': problem_url}) problem_list[i].update({'actual_url': problem_url})
"""
except GradingServiceError: except GradingServiceError:
error_text = "Error occured while contacting the grading service" #This is a student_facing_error
error_text = STUDENT_ERROR_MESSAGE
#This is a dev facing error
log.error("Problem contacting open ended grading service.")
success = False success = False
# catch error if if the json loads fails # catch error if if the json loads fails
except ValueError: except ValueError:
error_text = "Could not get problem list" #This is a student facing error
error_text = STUDENT_ERROR_MESSAGE
#This is a dev_facing_error
log.error("Problem with results from external grading service for open ended.")
success = False success = False
"""
ajax_url = _reverse_with_slash('open_ended_problems', course_id) ajax_url = _reverse_with_slash('open_ended_problems', course_id)
...@@ -195,11 +203,17 @@ def flagged_problem_list(request, course_id): ...@@ -195,11 +203,17 @@ def flagged_problem_list(request, course_id):
problem_list = problem_list_dict['flagged_submissions'] problem_list = problem_list_dict['flagged_submissions']
except GradingServiceError: except GradingServiceError:
error_text = "Error occured while contacting the grading service" #This is a staff_facing_error
error_text = STAFF_ERROR_MESSAGE
#This is a dev_facing_error
log.error("Could not get flagged problem list from external grading service for open ended.")
success = False success = False
# catch error if if the json loads fails # catch error if if the json loads fails
except ValueError: except ValueError:
error_text = "Could not get problem list" #This is a staff_facing_error
error_text = STAFF_ERROR_MESSAGE
#This is a dev_facing_error
log.error("Could not parse problem list from external grading service response.")
success = False success = False
ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_id) ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_id)
...@@ -283,7 +297,8 @@ def take_action_on_flags(request, course_id): ...@@ -283,7 +297,8 @@ def take_action_on_flags(request, course_id):
required = ['submission_id', 'action_type', 'student_id'] required = ['submission_id', 'action_type', 'student_id']
for key in required: for key in required:
if key not in request.POST: if key not in request.POST:
return HttpResponse(json.dumps({'success': False, 'error': 'Missing key {0}'.format(key)}), #This is a staff_facing_error
return HttpResponse(json.dumps({'success': False, 'error': STAFF_ERROR_MESSAGE + 'Missing key {0} from submission. Please reload and try again.'.format(key)}),
mimetype="application/json") mimetype="application/json")
p = request.POST p = request.POST
...@@ -297,5 +312,6 @@ def take_action_on_flags(request, course_id): ...@@ -297,5 +312,6 @@ def take_action_on_flags(request, course_id):
response = controller_qs.take_action_on_flags(course_id, student_id, submission_id, action_type) response = controller_qs.take_action_on_flags(course_id, student_id, submission_id, action_type)
return HttpResponse(response, mimetype="application/json") return HttpResponse(response, mimetype="application/json")
except GradingServiceError: except GradingServiceError:
log.exception("Error saving calibration grade, submission_id: {0}, submission_key: {1}, grader_id: {2}".format(submission_id, submission_key, grader_id)) #This is a dev_facing_error
return _err_response('Could not connect to grading service') log.exception("Error taking action on flagged peer grading submissions, submission_id: {0}, action_type: {1}, grader_id: {2}".format(submission_id, action_type, grader_id))
return _err_response(STAFF_ERROR_MESSAGE)
...@@ -170,4 +170,4 @@ PASSWORD_HASHERS = ( ...@@ -170,4 +170,4 @@ PASSWORD_HASHERS = (
'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.SHA1PasswordHasher',
'django.contrib.auth.hashers.MD5PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher',
# 'django.contrib.auth.hashers.CryptPasswordHasher', # 'django.contrib.auth.hashers.CryptPasswordHasher',
) )
\ No newline at end of file
...@@ -41,7 +41,7 @@ class OpenEnded ...@@ -41,7 +41,7 @@ class OpenEnded
post: (cmd, data, callback) -> post: (cmd, data, callback) ->
# if this post request fails, the error callback will catch it # if this post request fails, the error callback will catch it
$.post(@ajax_url + cmd, data, callback) $.post(@ajax_url + cmd, data, callback)
.error => callback({success: false, error: "Error occured while performing this operation"}) .error => callback({success: false, error: "Error occured while performing javascript ajax post."})
after_action_wrapper: (target, action_type) -> after_action_wrapper: (target, action_type) ->
tr_parent = target.parent().parent() tr_parent = target.parent().parent()
......
...@@ -143,7 +143,7 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t ...@@ -143,7 +143,7 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t
else else
# TODO: replace with postWithPrefix when that's loaded # TODO: replace with postWithPrefix when that's loaded
$.post(@ajax_url + cmd, data, callback) $.post(@ajax_url + cmd, data, callback)
.error => callback({success: false, error: "Error occured while performing this operation"}) .error => callback({success: false, error: "Error occured while performing javascript AJAX post."})
class @StaffGrading class @StaffGrading
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
%else: %else:
${status['human_task']} ${status['human_task']}
%endif %endif
(${status['human_state']}) ${status['score']} / ${status['max_score']} (${status['human_state']})
</div> </div>
%endfor %endfor
</section> </section>
......
...@@ -78,6 +78,7 @@ ...@@ -78,6 +78,7 @@
<p> <p>
Flag as inappropriate content for later review <input class="flag-checkbox" type="checkbox" /> Flag as inappropriate content for later review <input class="flag-checkbox" type="checkbox" />
</p> </p>
</div>
<div class="submission"> <div class="submission">
......
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
<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>
<p class="flag-student-container">Flag this submission for review by course staff (use if the submission contains inappropriate content): <input type="checkbox" class="flag-checkbox" value="student_is_flagged"></p> <p class="flag-student-container">Flag this submission for review by course staff (use if the submission contains inappropriate content): <input type="checkbox" class="flag-checkbox" value="student_is_flagged"></p>
<p class="answer-unknown-container">I do not know how to grade this question: <input type="checkbox" class="answer-unknown-checkbox" value="answer_is_unknown"></p>
</div> </div>
......
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