Commit 559bf887 by David Ormsbee

Merge pull request #1132 from MITx/vik/modify_open_ended

Vik/modify open ended
parents ecd2304c 90d902d1
...@@ -186,6 +186,24 @@ class LoncapaProblem(object): ...@@ -186,6 +186,24 @@ class LoncapaProblem(object):
maxscore += responder.get_max_score() maxscore += responder.get_max_score()
return maxscore 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): def get_score(self):
""" """
Compute score for this problem. The score is the number of points awarded. Compute score for this problem. The score is the number of points awarded.
......
...@@ -748,7 +748,7 @@ class OpenEndedInput(InputTypeBase): ...@@ -748,7 +748,7 @@ class OpenEndedInput(InputTypeBase):
# pulled out for testing # pulled out for testing
submitted_msg = ("Feedback not yet available. Reload to check again. " submitted_msg = ("Feedback not yet available. Reload to check again. "
"Once the problem is graded, this message will be " "Once the problem is graded, this message will be "
"replaced with the grader's feedback") "replaced with the grader's feedback.")
@classmethod @classmethod
def get_attributes(cls): def get_attributes(cls):
......
...@@ -1836,6 +1836,7 @@ class OpenEndedResponse(LoncapaResponse): ...@@ -1836,6 +1836,7 @@ class OpenEndedResponse(LoncapaResponse):
""" """
DEFAULT_QUEUE = 'open-ended' DEFAULT_QUEUE = 'open-ended'
DEFAULT_MESSAGE_QUEUE = 'open-ended-message'
response_tag = 'openendedresponse' response_tag = 'openendedresponse'
allowed_inputfields = ['openendedinput'] allowed_inputfields = ['openendedinput']
max_inputfields = 1 max_inputfields = 1
...@@ -1847,12 +1848,17 @@ class OpenEndedResponse(LoncapaResponse): ...@@ -1847,12 +1848,17 @@ class OpenEndedResponse(LoncapaResponse):
xml = self.xml xml = self.xml
self.url = xml.get('url', None) self.url = xml.get('url', None)
self.queue_name = xml.get('queuename', self.DEFAULT_QUEUE) 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 # The openendedparam tag encapsulates all grader settings
oeparam = self.xml.find('openendedparam') oeparam = self.xml.find('openendedparam')
prompt = self.xml.find('prompt') prompt = self.xml.find('prompt')
rubric = self.xml.find('openendedrubric') 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: if oeparam is None:
raise ValueError("No oeparam found in problem xml.") raise ValueError("No oeparam found in problem xml.")
if prompt is None: if prompt is None:
...@@ -1899,23 +1905,81 @@ class OpenEndedResponse(LoncapaResponse): ...@@ -1899,23 +1905,81 @@ class OpenEndedResponse(LoncapaResponse):
# response types) # response types)
except TypeError, ValueError: except TypeError, ValueError:
log.exception("Grader payload %r is not a json object!", grader_payload) 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({ parsed_grader_payload.update({
'location' : self.system.location, 'location' : self.system.location,
'course_id' : self.system.course_id, 'course_id' : self.system.course_id,
'prompt' : prompt_string, 'prompt' : prompt_string,
'rubric' : rubric_string, 'rubric' : rubric_string,
}) 'initial_display' : self.initial_display,
'answer' : self.answer,
})
updated_grader_payload = json.dumps(parsed_grader_payload) updated_grader_payload = json.dumps(parsed_grader_payload)
self.payload = {'grader_payload': updated_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: try:
self.max_score = int(find_with_default(oeparam, 'max_score', 1)) self.max_score = int(find_with_default(oeparam, 'max_score', 1))
except ValueError: except ValueError:
self.max_score = 1 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): def get_score(self, student_answers):
try: try:
...@@ -1956,7 +2020,7 @@ class OpenEndedResponse(LoncapaResponse): ...@@ -1956,7 +2020,7 @@ class OpenEndedResponse(LoncapaResponse):
contents.update({ contents.update({
'student_info': json.dumps(student_info), 'student_info': json.dumps(student_info),
'student_response': submission, '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 # Submit request. When successful, 'msg' is the prior length of the queue
...@@ -2056,18 +2120,36 @@ class OpenEndedResponse(LoncapaResponse): ...@@ -2056,18 +2120,36 @@ class OpenEndedResponse(LoncapaResponse):
""" """
return priorities.get(elt[0], default_priority) 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): def format_feedback(feedback_type, value):
return """ feedback_type,value=encode_values(feedback_type,value)
feedback= """
<div class="{feedback_type}"> <div class="{feedback_type}">
{value} {value}
</div> </div>
""".format(feedback_type=feedback_type, value=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 = """
<div class="{feedback_type}" style="display: none;">
{value}
</div>
""".format(feedback_type=feedback_type, value=value)
return feedback
# TODO (vshnayder): design and document the details of this format so # 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 # that we can do proper escaping here (e.g. are the graders allowed to
# include HTML?) # include HTML?)
for tag in ['success', 'feedback']: 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') return format_feedback('errors', 'Error getting feedback')
...@@ -2083,10 +2165,15 @@ class OpenEndedResponse(LoncapaResponse): ...@@ -2083,10 +2165,15 @@ class OpenEndedResponse(LoncapaResponse):
return format_feedback('errors', 'No feedback available') return format_feedback('errors', 'No feedback available')
feedback_lst = sorted(feedback.items(), key=get_priority) 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: 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): def _format_feedback(self, response_items):
""" """
...@@ -2104,7 +2191,7 @@ class OpenEndedResponse(LoncapaResponse): ...@@ -2104,7 +2191,7 @@ class OpenEndedResponse(LoncapaResponse):
feedback_template = self.system.render_template("open_ended_feedback.html", { feedback_template = self.system.render_template("open_ended_feedback.html", {
'grader_type': response_items['grader_type'], 'grader_type': response_items['grader_type'],
'score': response_items['score'], 'score': "{0} / {1}".format(response_items['score'], self.max_score),
'feedback': feedback, 'feedback': feedback,
}) })
...@@ -2138,17 +2225,19 @@ class OpenEndedResponse(LoncapaResponse): ...@@ -2138,17 +2225,19 @@ class OpenEndedResponse(LoncapaResponse):
" Received score_result = {0}".format(score_result)) " Received score_result = {0}".format(score_result))
return fail 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: if tag not in score_result:
log.error("External grader message is missing required tag: {0}" log.error("External grader message is missing required tag: {0}"
.format(tag)) .format(tag))
return fail return fail
feedback = self._format_feedback(score_result) 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. # HACK: for now, just assume it's correct if you got more than 2/3.
# Also assumes that score_result['score'] is an integer. # 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) correct = (score_ratio >= 0.66)
#Currently ignore msg and only return feedback (which takes the place of msg) #Currently ignore msg and only return feedback (which takes the place of msg)
......
...@@ -27,6 +27,30 @@ ...@@ -27,6 +27,30 @@
<input name="reload" class="reload" type="button" value="Recheck for Feedback" onclick="document.location.reload(true);" /> <input name="reload" class="reload" type="button" value="Recheck for Feedback" onclick="document.location.reload(true);" />
% endif % endif
<div class="external-grader-message"> <div class="external-grader-message">
${msg|n} ${msg|n}
% if status in ['correct','incorrect']:
<div class="collapsible evaluation-response">
<header>
<a href="#">Respond to Feedback</a>
</header>
<section id="evaluation_${id}" class="evaluation">
<p>How accurate do you find this feedback?</p>
<div class="evaluation-scoring">
<ul class="scoring-list">
<li><input type="radio" name="evaluation-score" id="evaluation-score-5" value="5" /> <label for="evaluation-score-5"> Correct</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-4" value="4" /> <label for="evaluation-score-4"> Partially Correct</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-3" value="3" /> <label for="evaluation-score-3"> No Opinion</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-2" value="2" /> <label for="evaluation-score-2"> Partially Incorrect</label></li>
<li><input type="radio" name="evaluation-score" id="evaluation-score-1" value="1" /> <label for="evaluation-score-1"> Incorrect</label></li>
</ul>
</div>
<p>Additional comments:</p>
<textarea rows="${rows}" cols="${cols}" name="feedback_${id}" class="feedback-on-feedback" id="feedback_${id}"></textarea>
<div class="submit-message-container">
<input name="submit-message" class="submit-message" type="button" value="Submit your message"/>
</div>
</section>
</div>
% endif
</div> </div>
</section> </section>
...@@ -380,6 +380,7 @@ class CapaModule(XModule): ...@@ -380,6 +380,7 @@ class CapaModule(XModule):
'problem_save': self.save_problem, 'problem_save': self.save_problem,
'problem_show': self.get_answer, 'problem_show': self.get_answer,
'score_update': self.update_score, 'score_update': self.update_score,
'message_post' : self.message_post,
} }
if dispatch not in handlers: if dispatch not in handlers:
...@@ -394,6 +395,20 @@ class CapaModule(XModule): ...@@ -394,6 +395,20 @@ class CapaModule(XModule):
}) })
return json.dumps(d, cls=ComplexEncoder) 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): def closed(self):
''' Is the student still allowed to submit answers? ''' ''' Is the student still allowed to submit answers? '''
if self.attempts == self.max_attempts: if self.attempts == self.max_attempts:
......
...@@ -297,6 +297,51 @@ section.problem { ...@@ -297,6 +297,51 @@ section.problem {
float: left; 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 { ...@@ -634,6 +679,10 @@ section.problem {
color: #2C2C2C; color: #2C2C2C;
font-family: monospace; font-family: monospace;
font-size: 1em; font-size: 1em;
padding-top: 10px;
header {
font-size: 1.4em;
}
.shortform { .shortform {
font-weight: bold; font-weight: bold;
......
...@@ -25,6 +25,7 @@ class @Problem ...@@ -25,6 +25,7 @@ class @Problem
@$('section.action input.reset').click @reset @$('section.action input.reset').click @reset
@$('section.action input.show').click @show @$('section.action input.show').click @show
@$('section.action input.save').click @save @$('section.action input.save').click @save
@$('section.evaluation input.submit-message').click @message_post
# Collapsibles # Collapsibles
Collapsible.setCollapsibles(@el) Collapsible.setCollapsibles(@el)
...@@ -197,6 +198,35 @@ class @Problem ...@@ -197,6 +198,35 @@ class @Problem
else else
@gentle_alert response.success @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: => reset: =>
Logger.log 'problem_reset', @answers Logger.log 'problem_reset', @answers
$.postWithPrefix "#{@url}/problem_reset", id: @id, (response) => $.postWithPrefix "#{@url}/problem_reset", id: @id, (response) =>
......
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