diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index 2eaa0e4286..efc96fc717 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -186,6 +186,24 @@ class LoncapaProblem(object):
maxscore += responder.get_max_score()
return maxscore
+ def message_post(self,event_info):
+ """
+ Handle an ajax post that contains feedback on feedback
+ Returns a boolean success variable
+ Note: This only allows for feedback to be posted back to the grading controller for the first
+ open ended response problem on each page. Multiple problems will cause some sync issues.
+ TODO: Handle multiple problems on one page sync issues.
+ """
+ success=False
+ message = "Could not find a valid responder."
+ log.debug("in lcp")
+ for responder in self.responders.values():
+ if hasattr(responder, 'handle_message_post'):
+ success, message = responder.handle_message_post(event_info)
+ if success:
+ break
+ return success, message
+
def get_score(self):
"""
Compute score for this problem. The score is the number of points awarded.
diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py
index 73056bc09e..e3eb47acc5 100644
--- a/common/lib/capa/capa/inputtypes.py
+++ b/common/lib/capa/capa/inputtypes.py
@@ -748,7 +748,7 @@ class OpenEndedInput(InputTypeBase):
# pulled out for testing
submitted_msg = ("Feedback not yet available. Reload to check again. "
"Once the problem is graded, this message will be "
- "replaced with the grader's feedback")
+ "replaced with the grader's feedback.")
@classmethod
def get_attributes(cls):
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index 8517e71d04..3e79ca2084 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -1836,6 +1836,7 @@ class OpenEndedResponse(LoncapaResponse):
"""
DEFAULT_QUEUE = 'open-ended'
+ DEFAULT_MESSAGE_QUEUE = 'open-ended-message'
response_tag = 'openendedresponse'
allowed_inputfields = ['openendedinput']
max_inputfields = 1
@@ -1847,12 +1848,17 @@ class OpenEndedResponse(LoncapaResponse):
xml = self.xml
self.url = xml.get('url', None)
self.queue_name = xml.get('queuename', self.DEFAULT_QUEUE)
+ self.message_queue_name = xml.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE)
# The openendedparam tag encapsulates all grader settings
oeparam = self.xml.find('openendedparam')
prompt = self.xml.find('prompt')
rubric = self.xml.find('openendedrubric')
+ #This is needed to attach feedback to specific responses later
+ self.submission_id=None
+ self.grader_id=None
+
if oeparam is None:
raise ValueError("No oeparam found in problem xml.")
if prompt is None:
@@ -1899,23 +1905,81 @@ class OpenEndedResponse(LoncapaResponse):
# response types)
except TypeError, ValueError:
log.exception("Grader payload %r is not a json object!", grader_payload)
+
+ self.initial_display = find_with_default(oeparam, 'initial_display', '')
+ self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.')
+
parsed_grader_payload.update({
'location' : self.system.location,
'course_id' : self.system.course_id,
'prompt' : prompt_string,
'rubric' : rubric_string,
- })
+ 'initial_display' : self.initial_display,
+ 'answer' : self.answer,
+ })
updated_grader_payload = json.dumps(parsed_grader_payload)
self.payload = {'grader_payload': updated_grader_payload}
- self.initial_display = find_with_default(oeparam, 'initial_display', '')
- self.answer = find_with_default(oeparam, 'answer_display', 'No answer given.')
try:
self.max_score = int(find_with_default(oeparam, 'max_score', 1))
except ValueError:
self.max_score = 1
+ def handle_message_post(self,event_info):
+ """
+ Handles a student message post (a reaction to the grade they received from an open ended grader type)
+ Returns a boolean success/fail and an error message
+ """
+ survey_responses=event_info['survey_responses']
+ for tag in ['feedback', 'submission_id', 'grader_id', 'score']:
+ if tag not in survey_responses:
+ return False, "Could not find needed tag {0}".format(tag)
+ try:
+ submission_id=int(survey_responses['submission_id'])
+ grader_id = int(survey_responses['grader_id'])
+ feedback = str(survey_responses['feedback'].encode('ascii', 'ignore'))
+ score = int(survey_responses['score'])
+ except:
+ error_message=("Could not parse submission id, grader id, "
+ "or feedback from message_post ajax call. Here is the message data: {0}".format(survey_responses))
+ log.exception(error_message)
+ return False, "There was an error saving your feedback. Please contact course staff."
+
+ qinterface = self.system.xqueue['interface']
+ qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
+ anonymous_student_id = self.system.anonymous_student_id
+ queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
+ anonymous_student_id +
+ self.answer_id)
+
+ xheader = xqueue_interface.make_xheader(
+ lms_callback_url=self.system.xqueue['callback_url'],
+ lms_key=queuekey,
+ queue_name=self.message_queue_name
+ )
+
+ student_info = {'anonymous_student_id': anonymous_student_id,
+ 'submission_time': qtime,
+ }
+ contents= {
+ 'feedback' : feedback,
+ 'submission_id' : submission_id,
+ 'grader_id' : grader_id,
+ 'score': score,
+ 'student_info' : json.dumps(student_info),
+ }
+
+ (error, msg) = qinterface.send_to_queue(header=xheader,
+ body=json.dumps(contents))
+
+ #Convert error to a success value
+ success=True
+ if error:
+ success=False
+
+ return success, "Successfully submitted your feedback."
+
def get_score(self, student_answers):
try:
@@ -1956,7 +2020,7 @@ class OpenEndedResponse(LoncapaResponse):
contents.update({
'student_info': json.dumps(student_info),
'student_response': submission,
- 'max_score' : self.max_score
+ 'max_score' : self.max_score,
})
# Submit request. When successful, 'msg' is the prior length of the queue
@@ -2056,18 +2120,36 @@ class OpenEndedResponse(LoncapaResponse):
"""
return priorities.get(elt[0], default_priority)
+ def encode_values(feedback_type,value):
+ feedback_type=str(feedback_type).encode('ascii', 'ignore')
+ if not isinstance(value,basestring):
+ value=str(value)
+ value=value.encode('ascii', 'ignore')
+ return feedback_type,value
+
def format_feedback(feedback_type, value):
- return """
+ feedback_type,value=encode_values(feedback_type,value)
+ feedback= """
{value}
""".format(feedback_type=feedback_type, value=value)
+ return feedback
+
+ def format_feedback_hidden(feedback_type , value):
+ feedback_type,value=encode_values(feedback_type,value)
+ feedback = """
+
+ {value}
+
+ """.format(feedback_type=feedback_type, value=value)
+ return feedback
# TODO (vshnayder): design and document the details of this format so
# that we can do proper escaping here (e.g. are the graders allowed to
# include HTML?)
- for tag in ['success', 'feedback']:
+ for tag in ['success', 'feedback', 'submission_id', 'grader_id']:
if tag not in response_items:
return format_feedback('errors', 'Error getting feedback')
@@ -2083,10 +2165,15 @@ class OpenEndedResponse(LoncapaResponse):
return format_feedback('errors', 'No feedback available')
feedback_lst = sorted(feedback.items(), key=get_priority)
- return 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:
- return 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)
+ for feedback_type,value in response_items.items()
+ if feedback_type in ['submission_id', 'grader_id']]))
+
+ return u"\n".join([feedback_list_part1,feedback_list_part2])
def _format_feedback(self, response_items):
"""
@@ -2104,7 +2191,7 @@ class OpenEndedResponse(LoncapaResponse):
feedback_template = self.system.render_template("open_ended_feedback.html", {
'grader_type': response_items['grader_type'],
- 'score': response_items['score'],
+ 'score': "{0} / {1}".format(response_items['score'], self.max_score),
'feedback': feedback,
})
@@ -2138,17 +2225,19 @@ class OpenEndedResponse(LoncapaResponse):
" Received score_result = {0}".format(score_result))
return fail
- for tag in ['score', 'feedback', 'grader_type', 'success']:
+ for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']:
if tag not in score_result:
log.error("External grader message is missing required tag: {0}"
.format(tag))
return fail
feedback = self._format_feedback(score_result)
+ self.submission_id=score_result['submission_id']
+ self.grader_id=score_result['grader_id']
# HACK: for now, just assume it's correct if you got more than 2/3.
# Also assumes that score_result['score'] is an integer.
- score_ratio = int(score_result['score']) / self.max_score
+ score_ratio = int(score_result['score']) / float(self.max_score)
correct = (score_ratio >= 0.66)
#Currently ignore msg and only return feedback (which takes the place of msg)
diff --git a/common/lib/capa/capa/templates/openendedinput.html b/common/lib/capa/capa/templates/openendedinput.html
index 65fc7fb9bb..c42ad73faf 100644
--- a/common/lib/capa/capa/templates/openendedinput.html
+++ b/common/lib/capa/capa/templates/openendedinput.html
@@ -27,6 +27,30 @@
% endif
- ${msg|n}
+ ${msg|n}
+ % if status in ['correct','incorrect']:
+
+
+
+ How accurate do you find this feedback?
+
+ Additional comments:
+
+
+
+
+
+
+ % endif
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 4c10a1703a..d65fa1f40a 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -380,6 +380,7 @@ class CapaModule(XModule):
'problem_save': self.save_problem,
'problem_show': self.get_answer,
'score_update': self.update_score,
+ 'message_post' : self.message_post,
}
if dispatch not in handlers:
@@ -394,6 +395,20 @@ class CapaModule(XModule):
})
return json.dumps(d, cls=ComplexEncoder)
+ def message_post(self, get):
+ """
+ Posts a message from a form to an appropriate location
+ """
+ event_info = dict()
+ event_info['state'] = self.lcp.get_state()
+ event_info['problem_id'] = self.location.url()
+ event_info['student_id'] = self.system.anonymous_student_id
+ event_info['survey_responses']= get
+
+ success, message = self.lcp.message_post(event_info)
+
+ return {'success' : success, 'message' : message}
+
def closed(self):
''' Is the student still allowed to submit answers? '''
if self.attempts == self.max_attempts:
diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss
index b25ab3d3a2..929b6dcb48 100644
--- a/common/lib/xmodule/xmodule/css/capa/display.scss
+++ b/common/lib/xmodule/xmodule/css/capa/display.scss
@@ -297,6 +297,51 @@ section.problem {
float: left;
}
}
+
+ }
+ .evaluation {
+ p {
+ margin-bottom: 4px;
+ }
+ }
+
+
+ .feedback-on-feedback {
+ height: 100px;
+ margin-right: 20px;
+ }
+
+ .evaluation-response {
+ header {
+ text-align: right;
+ a {
+ font-size: .85em;
+ }
+ }
+ }
+
+ .evaluation-scoring {
+ .scoring-list {
+ list-style-type: none;
+ margin-left: 3px;
+
+ li {
+ &:first-child {
+ margin-left: 0px;
+ }
+ display:inline;
+ margin-left: 50px;
+
+ label {
+ font-size: .9em;
+ }
+
+ }
+ }
+
+ }
+ .submit-message-container {
+ margin: 10px 0px ;
}
}
@@ -634,6 +679,10 @@ section.problem {
color: #2C2C2C;
font-family: monospace;
font-size: 1em;
+ padding-top: 10px;
+ header {
+ font-size: 1.4em;
+ }
.shortform {
font-weight: bold;
diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
index 1c0ace9e59..ba746fecb8 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -25,6 +25,7 @@ class @Problem
@$('section.action input.reset').click @reset
@$('section.action input.show').click @show
@$('section.action input.save').click @save
+ @$('section.evaluation input.submit-message').click @message_post
# Collapsibles
Collapsible.setCollapsibles(@el)
@@ -197,6 +198,35 @@ class @Problem
else
@gentle_alert response.success
+ message_post: =>
+ Logger.log 'message_post', @answers
+
+ fd = new FormData()
+ feedback = @$('section.evaluation textarea.feedback-on-feedback')[0].value
+ submission_id = $('div.external-grader-message div.submission_id')[0].innerHTML
+ grader_id = $('div.external-grader-message div.grader_id')[0].innerHTML
+ score = $(".evaluation-scoring input:radio[name='evaluation-score']:checked").val()
+ fd.append('feedback', feedback)
+ fd.append('submission_id', submission_id)
+ fd.append('grader_id', grader_id)
+ if(!score)
+ @gentle_alert "You need to pick a rating before you can submit."
+ return
+ else
+ fd.append('score', score)
+
+
+ settings =
+ type: "POST"
+ data: fd
+ processData: false
+ contentType: false
+ success: (response) =>
+ @gentle_alert response.message
+ @$('section.evaluation').slideToggle()
+
+ $.ajaxWithPrefix("#{@url}/message_post", settings)
+
reset: =>
Logger.log 'problem_reset', @answers
$.postWithPrefix "#{@url}/problem_reset", id: @id, (response) =>