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 = """ + + """.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']: +
+
+ Respond to Feedback +
+
+

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) =>