def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=ugettext("No answer provided.")) permutation = page_data["permutation"] choice = answer_data["choice"] unpermed_idx_set = set([permutation[idx] for idx in choice]) correct_idx_set = set(self.unpermuted_correct_indices()) if unpermed_idx_set == correct_idx_set: correctness = 1 else: if getattr(self.page_desc, "allow_partial_credit", False): correctness = ((len(self.page_desc.choices) - len( unpermed_idx_set.symmetric_difference(correct_idx_set))) / len(self.page_desc.choices)) elif getattr(self.page_desc, "allow_partial_credit_subset_only", False): if unpermed_idx_set < correct_idx_set: correctness = (len(unpermed_idx_set) / len(correct_idx_set)) else: correctness = 0 else: correctness = 0 return AnswerFeedback(correctness=correctness)
def test_validate_point_count_called(self): import random with mock.patch("course.page.base.validate_point_count") \ as mock_validate_point_count, \ mock.patch("course.page.base.get_auto_feedback") \ as mock_get_auto_feedback: mock_validate_point_count.side_effect = lambda x: x mock_get_auto_feedback.side_effect = lambda x: x for i in range(10): correctness = random.uniform(0, 15) feedback = "some feedback" AnswerFeedback(correctness, feedback) mock_validate_point_count.assert_called_once_with(correctness) # because feedback is not None self.assertEqual(mock_get_auto_feedback.call_count, 0) mock_validate_point_count.reset_mock() for i in range(10): correctness = random.uniform(0, 15) AnswerFeedback(correctness) # because get_auto_feedback is mocked, the call_count of # mock_validate_point_count is only once mock_validate_point_count.assert_called_once_with(correctness) mock_validate_point_count.reset_mock() # because feedback is None self.assertEqual(mock_get_auto_feedback.call_count, 1) mock_get_auto_feedback.reset_mock() AnswerFeedback(correctness=None) mock_validate_point_count.assert_called_once_with(None)
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=ugettext("No answer provided.")) answer_dict = answer_data["answer"] total_weight = 0 for idx, name in enumerate(self.embedded_name_list): total_weight += self.answer_instance_list[idx].weight if total_weight > 0: achieved_weight = 0 for answer_instance in self.answer_instance_list: if answer_dict[answer_instance.name] is not None: achieved_weight += answer_instance.get_weight( answer_dict[answer_instance.name]) correctness = achieved_weight / total_weight # for case when all questions have no weight assigned else: n_corr = 0 for answer_instance in self.answer_instance_list: if answer_dict[answer_instance.name] is not None: n_corr += answer_instance.get_correctness( answer_dict[answer_instance.name]) correctness = n_corr / len(self.answer_instance_list) return AnswerFeedback(correctness=correctness)
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback="No answer provided.") answer = answer_data["answer"] correctness, correct_answer_text = max( (matcher.grade(answer), matcher.correct_answer_text()) for matcher in self.matchers) return AnswerFeedback(correctness=correctness)
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=ugettext("No answer provided.")) permutation = page_data["permutation"] choice = answer_data["choice"] disregard_idx_set = set(self.unpermuted_disregard_indices()) always_correct_idx_set = set(self.unpermuted_always_correct_indices()) unpermed_idx_set = ( set([permutation[idx] for idx in choice]) - disregard_idx_set - always_correct_idx_set) correct_idx_set = ( set(self.unpermuted_correct_indices()) - disregard_idx_set - always_correct_idx_set) num_choices = len(self.page_desc.choices) - len(disregard_idx_set) if self.credit_mode == "exact": if unpermed_idx_set == correct_idx_set: correctness = 1 else: correctness = 0 elif self.credit_mode == "proportional": correctness = ( ( num_choices - len(unpermed_idx_set .symmetric_difference(correct_idx_set))) / num_choices) elif self.credit_mode == "proportional_correct": correctness = ( ( len(unpermed_idx_set & correct_idx_set) + len(always_correct_idx_set)) / ( len(correct_idx_set) + len(always_correct_idx_set))) if not (unpermed_idx_set <= correct_idx_set): correctness = 0 return AnswerFeedback(correctness=correctness)
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback="No answer provided.") if grade_data is not None and not grade_data["released"]: grade_data = None code_feedback = PythonCodeQuestion.grade(self, page_context, page_data, answer_data, grade_data) correctness = None percentage = None if (code_feedback is not None and code_feedback.correctness is not None and grade_data is not None and grade_data["grade_percent"] is not None): correctness = ( code_feedback.correctness * (self.page_desc.value - self.page_desc.human_feedback_value) + grade_data["grade_percent"] / 100 * self.page_desc.human_feedback_value) / self.page_desc.value percentage = correctness * 100 elif (self.page_desc.human_feedback_value == self.page_desc.value and grade_data is not None and grade_data["grade_percent"] is not None): correctness = grade_data["grade_percent"] / 100 percentage = correctness * 100 human_feedback_percentage = None human_feedback_text = None if grade_data is not None: if grade_data["feedback_text"] is not None: human_feedback_text = markup_to_html( page_context, grade_data["feedback_text"]) human_feedback_percentage = grade_data["grade_percent"] from django.template.loader import render_to_string feedback = render_to_string( "course/feedback-code-with-human.html", { "percentage": percentage, "code_feedback": code_feedback, "human_feedback_text": human_feedback_text, "human_feedback_percentage": human_feedback_percentage, }) return AnswerFeedback(correctness=correctness, feedback=feedback, bulk_feedback=code_feedback.bulk_feedback)
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=ugettext("No answer provided.")) permutation = page_data["permutation"] choice = answer_data["choice"] if permutation[choice] in self.unpermuted_correct_indices(): correctness = 1 else: correctness = 0 return AnswerFeedback(correctness=correctness)
def test_from_json(self): json = { "correctness": 0.5, "feedback": "what ever" } af = AnswerFeedback.from_json(json, None) self.assertEqual(af.correctness, 0.5) self.assertEqual(af.feedback, "what ever") self.assertEqual(af.bulk_feedback, None)
def test_from_json(self): json = { "correctness": 0.5, "feedback": "what ever" } af = AnswerFeedback.from_json(json, None) self.assertEqual(af.correctness, 0.5) self.assertEqual(af.feedback, "what ever") self.assertEqual(af.bulk_feedback, None)
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=ugettext("No answer provided.")) answer = answer_data["answer"] correctnesses_and_answers = [] for matcher in self.matchers: try: matcher.validate(answer) except forms.ValidationError: continue correctnesses_and_answers.append( (matcher.grade(answer), matcher.correct_answer_text())) correctness, correct_answer_text = max(correctnesses_and_answers) return AnswerFeedback(correctness=correctness)
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=ugettext("No answer provided.")) answer = answer_data["answer"] correctness = 0 for matcher in self.matchers: try: matcher.validate(answer) except forms.ValidationError: continue matcher_correctness = matcher.grade(answer) if (matcher_correctness is not None and matcher_correctness >= correctness): correctness = matcher_correctness return AnswerFeedback(correctness=correctness)
def test_from_json_none(self): af = AnswerFeedback.from_json(None, None) self.assertIsNone(af)
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=_("No answer provided.")) user_code = answer_data["answer"] # {{{ request run run_req = {"compile_only": False, "user_code": user_code} def transfer_attr(name): if hasattr(self.page_desc, name): run_req[name] = getattr(self.page_desc, name) transfer_attr("setup_code") transfer_attr("names_for_user") transfer_attr("names_from_user") if hasattr(self.page_desc, "test_code"): run_req["test_code"] = self.get_test_code() if hasattr(self.page_desc, "data_files"): run_req["data_files"] = {} from course.content import get_repo_blob for data_file in self.page_desc.data_files: from base64 import b64encode run_req["data_files"][data_file] = \ b64encode( get_repo_blob( page_context.repo, data_file, page_context.commit_sha).data).decode() try: response_dict = request_python_run_with_retries( run_req, run_timeout=self.page_desc.timeout) except: from traceback import format_exc response_dict = { "result": "uncaught_error", "message": "Error connecting to container", "traceback": "".join(format_exc()), } # }}} feedback_bits = [] # {{{ send email if the grading code broke if response_dict["result"] in [ "uncaught_error", "setup_compile_error", "setup_error", "test_compile_error", "test_error" ]: error_msg_parts = ["RESULT: %s" % response_dict["result"]] for key, val in sorted(response_dict.items()): if (key not in ["result", "figures"] and val and isinstance(val, six.string_types)): error_msg_parts.append( "-------------------------------------") error_msg_parts.append(key) error_msg_parts.append( "-------------------------------------") error_msg_parts.append(val) error_msg_parts.append("-------------------------------------") error_msg_parts.append("user code") error_msg_parts.append("-------------------------------------") error_msg_parts.append(user_code) error_msg_parts.append("-------------------------------------") error_msg = "\n".join(error_msg_parts) with translation.override(settings.RELATE_ADMIN_EMAIL_LOCALE): from django.template.loader import render_to_string message = render_to_string( "course/broken-code-question-email.txt", { "page_id": self.page_desc.id, "course": page_context.course, "error_message": error_msg, }) if (not page_context.in_sandbox and not is_nuisance_failure(response_dict)): try: from django.core.mail import send_mail send_mail( "".join( ["[%s] ", _("code question execution failed")]) % page_context.course.identifier, message, settings.ROBOT_EMAIL_FROM, recipient_list=[page_context.course.notify_email]) except Exception: from traceback import format_exc feedback_bits.append( six.text_type( string_concat( "<p>", _("Both the grading code and the attempt to " "notify course staff about the issue failed. " "Please contact the course or site staff and " "inform them of this issue, mentioning this " "entire error message:"), "</p>", "<p>", _("Sending an email to the course staff about the " "following failure failed with " "the following error message:"), "<pre>", "".join(format_exc()), "</pre>", _("The original failure message follows:"), "</p>"))) # }}} from relate.utils import dict_to_struct response = dict_to_struct(response_dict) bulk_feedback_bits = [] if hasattr(response, "points"): correctness = response.points feedback_bits.append("<p><b>%s</b></p>" % get_auto_feedback(correctness)) else: correctness = None if response.result == "success": pass elif response.result in [ "uncaught_error", "setup_compile_error", "setup_error", "test_compile_error", "test_error" ]: feedback_bits.append("".join([ "<p>", _("The grading code failed. Sorry about that. " "The staff has been informed, and if this problem is " "due to an issue with the grading code, " "it will be fixed as soon as possible. " "In the meantime, you'll see a traceback " "below that may help you figure out what went wrong."), "</p>" ])) elif response.result == "timeout": feedback_bits.append("".join([ "<p>", _("Your code took too long to execute. The problem " "specifies that your code may take at most %s seconds " "to run. " "It took longer than that and was aborted."), "</p>" ]) % self.page_desc.timeout) correctness = 0 elif response.result == "user_compile_error": feedback_bits.append("".join([ "<p>", _("Your code failed to compile. An error message is " "below."), "</p>" ])) correctness = 0 elif response.result == "user_error": feedback_bits.append("".join([ "<p>", _("Your code failed with an exception. " "A traceback is below."), "</p>" ])) correctness = 0 else: raise RuntimeError("invalid runpy result: %s" % response.result) if hasattr(response, "feedback") and response.feedback: feedback_bits.append("".join([ "<p>", _("Here is some feedback on your code"), ":" "<ul>%s</ul></p>" ]) % "".join("<li>%s</li>" % escape(fb_item) for fb_item in response.feedback)) if hasattr(response, "traceback") and response.traceback: feedback_bits.append("".join([ "<p>", _("This is the exception traceback"), ":" "<pre>%s</pre></p>" ]) % escape(response.traceback)) if hasattr(response, "exec_host") and response.exec_host != "localhost": import socket try: exec_host_name, dummy, dummy = socket.gethostbyaddr( response.exec_host) except socket.error: exec_host_name = response.exec_host feedback_bits.append("".join( ["<p>", _("Your code ran on %s.") % exec_host_name, "</p>"])) if hasattr(response, "stdout") and response.stdout: bulk_feedback_bits.append("".join([ "<p>", _("Your code printed the following output"), ":" "<pre>%s</pre></p>" ]) % escape(response.stdout)) if hasattr(response, "stderr") and response.stderr: bulk_feedback_bits.append("".join([ "<p>", _("Your code printed the following error messages"), ":" "<pre>%s</pre></p>" ]) % escape(response.stderr)) if hasattr(response, "figures") and response.figures: fig_lines = [ "".join([ "<p>", _("Your code produced the following plots"), ":</p>" ]), '<dl class="result-figure-list">', ] for nr, mime_type, b64data in response.figures: fig_lines.extend([ "".join(["<dt>", _("Figure"), "%d<dt>"]) % nr, '<dd><img alt="Figure %d" src="data:%s;base64,%s"></dd>' % (nr, mime_type, b64data) ]) fig_lines.append("</dl>") bulk_feedback_bits.extend(fig_lines) return AnswerFeedback(correctness=correctness, feedback="\n".join(feedback_bits), bulk_feedback="\n".join(bulk_feedback_bits))
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=_("No answer provided.")) user_code = answer_data["answer"] # {{{ request run run_req = {"compile_only": False, "user_code": user_code} def transfer_attr(name): if hasattr(self.page_desc, name): run_req[name] = getattr(self.page_desc, name) transfer_attr("setup_code") transfer_attr("names_for_user") transfer_attr("names_from_user") run_req["test_code"] = self.get_test_code() if hasattr(self.page_desc, "data_files"): run_req["data_files"] = {} from course.content import get_repo_blob for data_file in self.page_desc.data_files: from base64 import b64encode run_req["data_files"][data_file] = \ b64encode( get_repo_blob( page_context.repo, data_file, page_context.commit_sha).data).decode() try: response_dict = request_run_with_retries( run_req, run_timeout=self.page_desc.timeout, image=self.container_image) except Exception: from traceback import format_exc response_dict = { "result": "uncaught_error", "message": "Error connecting to container", "traceback": "".join(format_exc()), } # }}} feedback_bits = [] correctness = None if "points" in response_dict: correctness = response_dict["points"] try: feedback_bits.append("<p><b>%s</b></p>" % _(get_auto_feedback(correctness))) except Exception as e: correctness = None response_dict["result"] = "setup_error" response_dict["message"] = ("%s: %s" % (type(e).__name__, str(e))) # {{{ send email if the grading code broke if response_dict["result"] in [ "uncaught_error", "setup_compile_error", "setup_error", "test_compile_error", "test_error" ]: error_msg_parts = ["RESULT: %s" % response_dict["result"]] for key, val in sorted(response_dict.items()): if (key not in ["result", "figures"] and val and isinstance(val, str)): error_msg_parts.append( "-------------------------------------") error_msg_parts.append(key) error_msg_parts.append( "-------------------------------------") error_msg_parts.append(val) error_msg_parts.append("-------------------------------------") error_msg_parts.append("user code") error_msg_parts.append("-------------------------------------") error_msg_parts.append(user_code) error_msg_parts.append("-------------------------------------") error_msg = "\n".join(error_msg_parts) from relate.utils import local_now, format_datetime_local from course.utils import LanguageOverride with LanguageOverride(page_context.course): from relate.utils import render_email_template message = render_email_template( "course/broken-code-question-email.txt", { "site": getattr(settings, "RELATE_BASE_URL"), "page_id": self.page_desc.id, "course": page_context.course, "error_message": error_msg, "review_uri": page_context.page_uri, "time": format_datetime_local(local_now()) }) if (not page_context.in_sandbox and not is_nuisance_failure(response_dict)): try: from django.core.mail import EmailMessage msg = EmailMessage( "".join([ "[%s:%s] ", _("code question execution failed") ]) % (page_context.course.identifier, page_context.flow_session.flow_id if page_context.flow_session is not None else _("<unknown flow>")), message, settings.ROBOT_EMAIL_FROM, [page_context.course.notify_email]) from relate.utils import get_outbound_mail_connection msg.connection = get_outbound_mail_connection("robot") msg.send() except Exception: from traceback import format_exc feedback_bits.append( str( string_concat( "<p>", _("Both the grading code and the attempt to " "notify course staff about the issue failed. " "Please contact the course or site staff and " "inform them of this issue, mentioning this " "entire error message:"), "</p>", "<p>", _("Sending an email to the course staff about the " "following failure failed with " "the following error message:"), "<pre>", "".join(format_exc()), "</pre>", _("The original failure message follows:"), "</p>"))) # }}} if hasattr(self.page_desc, "correct_code"): def normalize_code(s): return (s.replace(" ", "").replace("\r", "").replace( "\n", "").replace("\t", "")) if (normalize_code(user_code) == normalize_code( self.page_desc.correct_code)): feedback_bits.append( "<p><b>%s</b></p>" % _("It looks like you submitted code that is identical to " "the reference solution. This is not allowed.")) from relate.utils import dict_to_struct response = dict_to_struct(response_dict) bulk_feedback_bits = [] if response.result == "success": pass elif response.result in [ "uncaught_error", "setup_compile_error", "setup_error", "test_compile_error", "test_error" ]: feedback_bits.append("".join([ "<p>", _("The grading code failed. Sorry about that. " "The staff has been informed, and if this problem is " "due to an issue with the grading code, " "it will be fixed as soon as possible. " "In the meantime, you'll see a traceback " "below that may help you figure out what went wrong."), "</p>" ])) elif response.result == "timeout": feedback_bits.append("".join([ "<p>", _("Your code took too long to execute. The problem " "specifies that your code may take at most %s seconds " "to run. " "It took longer than that and was aborted."), "</p>" ]) % self.page_desc.timeout) correctness = 0 elif response.result == "user_compile_error": feedback_bits.append("".join([ "<p>", _("Your code failed to compile. An error message is " "below."), "</p>" ])) correctness = 0 elif response.result == "user_error": feedback_bits.append("".join([ "<p>", _("Your code failed with an exception. " "A traceback is below."), "</p>" ])) correctness = 0 else: raise RuntimeError("invalid run result: %s" % response.result) if hasattr(response, "feedback") and response.feedback: def sanitize(s): import bleach return bleach.clean(s, tags=["p", "pre"]) feedback_bits.append("".join([ "<p>", _("Here is some feedback on your code"), ":" "<ul>%s</ul></p>" ]) % "".join("<li>%s</li>" % sanitize(fb_item) for fb_item in response.feedback)) if hasattr(response, "traceback") and response.traceback: feedback_bits.append("".join([ "<p>", _("This is the exception traceback"), ":" "<pre>%s</pre></p>" ]) % escape(response.traceback)) if hasattr(response, "exec_host") and response.exec_host != "localhost": import socket try: exec_host_name, dummy, dummy = socket.gethostbyaddr( response.exec_host) except socket.error: exec_host_name = response.exec_host feedback_bits.append("".join( ["<p>", _("Your code ran on %s.") % exec_host_name, "</p>"])) if hasattr(response, "stdout") and response.stdout: bulk_feedback_bits.append("".join([ "<p>", _("Your code printed the following output"), ":" "<pre>%s</pre></p>" ]) % escape(response.stdout)) if hasattr(response, "stderr") and response.stderr: bulk_feedback_bits.append("".join([ "<p>", _("Your code printed the following error messages"), ":" "<pre>%s</pre></p>" ]) % escape(response.stderr)) if hasattr(response, "figures") and response.figures: fig_lines = [ "".join([ "<p>", _("Your code produced the following plots"), ":</p>" ]), '<dl class="result-figure-list">', ] for nr, mime_type, b64data in response.figures: if mime_type in ["image/jpeg", "image/png"]: fig_lines.extend([ "".join(["<dt>", _("Figure"), "%d<dt>"]) % nr, '<dd><img alt="Figure %d" src="data:%s;base64,%s"></dd>' % (nr, mime_type, b64data) ]) fig_lines.append("</dl>") bulk_feedback_bits.extend(fig_lines) # {{{ html output / santization if hasattr(response, "html") and response.html: def is_allowed_data_uri(allowed_mimetypes, uri): import re m = re.match(r"^data:([-a-z0-9]+/[-a-z0-9]+);base64,", uri) if not m: return False mimetype = m.group(1) return mimetype in allowed_mimetypes def sanitize(s): import bleach def filter_audio_attributes(tag, name, value): if name in ["controls"]: return True else: return False def filter_source_attributes(tag, name, value): if name in ["type"]: return True elif name == "src": if is_allowed_data_uri([ "audio/wav", ], value): return bleach.sanitizer.VALUE_SAFE else: return False else: return False def filter_img_attributes(tag, name, value): if name in ["alt", "title"]: return True elif name == "src": return is_allowed_data_uri([ "image/png", "image/jpeg", ], value) else: return False if not isinstance(s, str): return _("(Non-string in 'HTML' output filtered out)") return bleach.clean(s, tags=bleach.ALLOWED_TAGS + ["audio", "video", "source"], attributes={ "audio": filter_audio_attributes, "source": filter_source_attributes, "img": filter_img_attributes, }) bulk_feedback_bits.extend( sanitize(snippet) for snippet in response.html) # }}} return AnswerFeedback(correctness=correctness, feedback="\n".join(feedback_bits), bulk_feedback="\n".join(bulk_feedback_bits))
def grade(self, page_context, page_data, answer_data, grade_data): if answer_data is None: return AnswerFeedback(correctness=0, feedback=_("No answer provided.")) if grade_data is not None and not grade_data["released"]: grade_data = None code_feedback = PythonCodeQuestion.grade(self, page_context, page_data, answer_data, grade_data) human_points = self.human_feedback_point_value(page_context, page_data) code_points = self.page_desc.value - human_points correctness = None percentage = None if (code_feedback is not None and code_feedback.correctness is not None and grade_data is not None and grade_data["grade_percent"] is not None): code_feedback_percentage = 100 - self.human_feedback_percentage percentage = ( code_feedback.correctness * code_feedback_percentage + grade_data["grade_percent"] / 100 * self.human_feedback_percentage) correctness = percentage / 100 elif (self.human_feedback_percentage == 100 and grade_data is not None and grade_data["grade_percent"] is not None): correctness = grade_data["grade_percent"] / 100 percentage = correctness * 100 elif (self.human_feedback_percentage == 0 and code_feedback.correctness is not None): correctness = code_feedback.correctness percentage = correctness * 100 human_feedback_text = None human_feedback_points = None if grade_data is not None: assert grade_data["feedback_text"] is not None if grade_data["feedback_text"].strip(): human_feedback_text = markup_to_html( page_context, grade_data["feedback_text"]) human_graded_percentage = grade_data["grade_percent"] if human_graded_percentage is not None: human_feedback_points = (human_graded_percentage / 100. * human_points) code_feedback_points = None if (code_feedback is not None and code_feedback.correctness is not None): code_feedback_points = code_feedback.correctness * code_points from django.template.loader import render_to_string feedback = render_to_string( "course/feedback-code-with-human.html", { "percentage": percentage, "code_feedback": code_feedback, "code_feedback_points": code_feedback_points, "code_points": code_points, "human_feedback_text": human_feedback_text, "human_feedback_points": human_feedback_points, "human_points": human_points, }) return AnswerFeedback(correctness=correctness, feedback=feedback, bulk_feedback=code_feedback.bulk_feedback)
def test_correctness_negative(self): correctness = -0.1 with self.assertRaises(InvalidFeedbackPointsError): AnswerFeedback(correctness)
def test_correctness_exceed_max_extra_credit_factor(self): correctness = MAX_EXTRA_CREDIT_FACTOR + 0.1 with self.assertRaises(InvalidFeedbackPointsError): AnswerFeedback(correctness)
def test_from_json_none(self): af = AnswerFeedback.from_json(None, None) self.assertIsNone(af)
def test_correctness_can_be_none(self): af = AnswerFeedback(None) self.assertIsNone(af.correctness)