def __init__(self, vctx, location, page_desc): super(PythonCodeQuestion, self).__init__(vctx, location, page_desc) if vctx is not None and hasattr(page_desc, "data_files"): for data_file in page_desc.data_files: try: if not isinstance(data_file, str): raise ObjectDoesNotExist() from course.content import get_repo_blob get_repo_blob(vctx.repo, data_file, vctx.commit_sha) except ObjectDoesNotExist: raise ValidationError("%s: data file '%s' not found" % (location, data_file)) if not getattr(page_desc, "single_submission", False) and vctx is not None: is_multi_submit = False if hasattr(page_desc, "access_rules"): if hasattr(page_desc.access_rules, "add_permissions"): if (flow_permission.change_answer in page_desc.access_rules.add_permissions): is_multi_submit = True if not is_multi_submit: vctx.add_warning( location, _("code question does not explicitly " "allow multiple submission. Either add " "access_rules/add_permssions/change_answer " "or add 'single_submission: True' to confirm that you intend " "for only a single submission to be allowed. " "While you're at it, consider adding " "access_rules/add_permssions/see_correctness."))
def __init__(self, vctx, location, page_desc): super(PythonCodeQuestion, self).__init__(vctx, location, page_desc) if vctx is not None and hasattr(page_desc, "data_files"): for data_file in page_desc.data_files: try: if not isinstance(data_file, str): raise ObjectDoesNotExist() from course.content import get_repo_blob get_repo_blob(vctx.repo, data_file, vctx.commit_sha) except ObjectDoesNotExist: raise ValidationError("%s: data file '%s' not found" % (location, data_file)) if not getattr(page_desc, "single_submission", False) and vctx is not None: is_multi_submit = False if hasattr(page_desc, "access_rules"): if hasattr(page_desc.access_rules, "add_permissions"): if (flow_permission.change_answer in page_desc.access_rules.add_permissions): is_multi_submit = True if not is_multi_submit: vctx.add_warning(location, _("code question does not explicitly " "allow multiple submission. Either add " "access_rules/add_permssions/change_answer " "or add 'single_submission: True' to confirm that you intend " "for only a single submission to be allowed. " "While you're at it, consider adding " "access_rules/add_permssions/see_correctness."))
def validate_course_content(repo, course_file, events_file, validate_sha, course=None): course_desc = get_yaml_from_repo_safely(repo, course_file, commit_sha=validate_sha) vctx = ValidationContext(repo=repo, commit_sha=validate_sha, course=course) validate_course_desc_struct(vctx, course_file, course_desc) try: from course.content import get_yaml_from_repo events_desc = get_yaml_from_repo(repo, events_file, commit_sha=validate_sha, cached=False) except ObjectDoesNotExist: # That's OK--no calendar info. pass else: validate_calendar_desc_struct(vctx, events_file, events_desc) check_attributes_yml(vctx, repo, "", get_repo_blob(repo, "", validate_sha)) try: flows_tree = get_repo_blob(repo, "flows", validate_sha) except ObjectDoesNotExist: # That's OK--no flows yet. pass else: for entry in flows_tree.items(): if not entry.path.endswith(".yml"): continue from course.constants import FLOW_ID_REGEX flow_id = entry.path[:-4] match = re.match("^" + FLOW_ID_REGEX + "$", flow_id) if match is None: raise ValidationError( string_concat( "%s: ", _("invalid flow name. " "Flow names may only contain (roman) " "letters, numbers, " "dashes and underscores.")) % entry.path) location = "flows/%s" % entry.path flow_desc = get_yaml_from_repo_safely(repo, location, commit_sha=validate_sha) validate_flow_desc(vctx, location, flow_desc) return vctx.warnings
def validate_course_content(repo, course_file, events_file, validate_sha, course=None): course_desc = get_yaml_from_repo_safely(repo, course_file, commit_sha=validate_sha) vctx = ValidationContext( repo=repo, commit_sha=validate_sha, course=course) validate_course_desc_struct(vctx, course_file, course_desc) try: from course.content import get_yaml_from_repo events_desc = get_yaml_from_repo(repo, events_file, commit_sha=validate_sha, cached=False) except ObjectDoesNotExist: # That's OK--no calendar info. pass else: validate_calendar_desc_struct(vctx, events_file, events_desc) check_attributes_yml( vctx, repo, "", get_repo_blob(repo, "", validate_sha)) try: flows_tree = get_repo_blob(repo, "flows", validate_sha) except ObjectDoesNotExist: # That's OK--no flows yet. pass else: for entry in flows_tree.items(): if not entry.path.endswith(".yml"): continue from course.constants import FLOW_ID_REGEX flow_id = entry.path[:-4] match = re.match("^"+FLOW_ID_REGEX+"$", flow_id) if match is None: raise ValidationError( string_concat("%s: ", _("invalid flow name. " "Flow names may only contain (roman) " "letters, numbers, " "dashes and underscores.")) % entry.path) location = "flows/%s" % entry.path flow_desc = get_yaml_from_repo_safely(repo, location, commit_sha=validate_sha) validate_flow_desc(vctx, location, flow_desc) return vctx.warnings
def __init__(self, vctx, location, page_desc): super(PythonCodeQuestion, self).__init__(vctx, location, page_desc) if vctx is not None and hasattr(page_desc, "data_files"): for data_file in page_desc.data_files: try: if not isinstance(data_file, str): raise ObjectDoesNotExist() from course.content import get_repo_blob get_repo_blob(vctx.repo, data_file, vctx.commit_sha) except ObjectDoesNotExist: raise ValidationError("%s: data file '%s' not found" % (location, data_file))
def get_repo_blob_side_effect(repo, full_name, commit_sha, allow_tree=True): # Fake the inline multiple question yaml for specific commit if not (full_name == "questions/multi-question-example.yml" and commit_sha == b"ec41a2de73a99e6022060518cb5c5c162b88cdf5"): return get_repo_blob(repo, full_name, commit_sha, allow_tree) else: class Blob(object): pass blob = Blob() blob.data = INLINE_MULTI_MARKDOWN_FEWER.encode() return blob
def get_repo_blob_side_effect3(repo, full_name, commit_sha, allow_tree=True): if full_name == "media" and allow_tree: raise ObjectDoesNotExist() if full_name == "flows" and allow_tree: tree = Tree() tree.add(b"not_a_flow", stat.S_IFREG, b"not a flow") tree.add(flow1_location.encode(), stat.S_IFREG, b"a flow") return tree if full_name == "staticpages": raise ObjectDoesNotExist() if full_name == "": return Tree() return get_repo_blob(repo, full_name, commit_sha, allow_tree)
def validate_course_content(repo, course_file, events_file, validate_sha, datespec_callback=None): course_desc = get_yaml_from_repo_safely(repo, course_file, commit_sha=validate_sha) ctx = ValidationContext(repo=repo, commit_sha=validate_sha, datespec_callback=datespec_callback) validate_course_desc_struct(ctx, course_file, course_desc) try: from course.content import get_yaml_from_repo events_desc = get_yaml_from_repo(repo, events_file, commit_sha=validate_sha, cached=False) except ObjectDoesNotExist: # That's OK--no calendar info. pass else: validate_calendar_desc_struct(ctx, events_file, events_desc) try: flows_tree = get_repo_blob(repo, "flows", validate_sha) except ObjectDoesNotExist: # That's OK--no flows yet. pass else: for entry in flows_tree.items(): if not entry.path.endswith(".yml"): continue location = "flows/%s" % entry.path flow_desc = get_yaml_from_repo_safely(repo, location, commit_sha=validate_sha) validate_flow_desc(ctx, location, flow_desc) return ctx.warnings
def get_repo_blob_side_effect1(repo, full_name, commit_sha, allow_tree=True): if full_name == "media" and allow_tree: tree = Tree() tree.add(b"media", stat.S_IFDIR, b"some media") return tree if full_name == "flows" and allow_tree: tree = Tree() tree.add(b"not_a_flow", stat.S_IFREG, b"not a flow") tree.add(flow1_location.encode(), stat.S_IFREG, b"a flow") return tree if full_name == "staticpages": tree = Tree() tree.add(b"not_a_page", stat.S_IFREG, b"not a page") tree.add(staticpage1_location.encode(), stat.S_IFREG, b"a static page") tree.add(staticpage2_location.encode(), stat.S_IFREG, b"a static page") return tree if full_name == "": return Tree() return get_repo_blob(repo, full_name, commit_sha, allow_tree)
def validate_course_content(repo, course_file, events_file, validate_sha, datespec_callback=None): course_desc = get_yaml_from_repo_safely(repo, course_file, commit_sha=validate_sha) ctx = ValidationContext( repo=repo, commit_sha=validate_sha, datespec_callback=datespec_callback) validate_course_desc_struct(ctx, course_file, course_desc) try: from course.content import get_yaml_from_repo events_desc = get_yaml_from_repo(repo, events_file, commit_sha=validate_sha, cached=False) except ObjectDoesNotExist: # That's OK--no calendar info. pass else: validate_calendar_desc_struct(ctx, events_file, events_desc) try: flows_tree = get_repo_blob(repo, "flows", validate_sha) except ObjectDoesNotExist: # That's OK--no flows yet. pass else: for entry in flows_tree.items(): if not entry.path.endswith(".yml"): continue location = "flows/%s" % entry.path flow_desc = get_yaml_from_repo_safely(repo, location, commit_sha=validate_sha) validate_flow_desc(ctx, location, flow_desc) return ctx.warnings
def manage_instant_flow_requests(pctx): if pctx.role != participation_role.instructor: raise PermissionDenied("must be instructor to manage instant flow requests") # {{{ find available flow ids from course.content import get_repo_blob flow_ids = [] try: flows_tree = get_repo_blob(pctx.repo, "flows", pctx.course_commit_sha) except ObjectDoesNotExist: # That's OK--no flows yet. pass else: for entry in flows_tree.items(): if entry.path.endswith(".yml"): flow_ids.append(entry.path[:-4]) flow_ids.sort() # }}} request = pctx.request if request.method == "POST": form = InstantFlowRequestForm(flow_ids, request.POST, request.FILES) if "add" in request.POST: op = "add" elif "cancel" in request.POST: op = "cancel" else: raise SuspiciousOperation("invalid operation") now_datetime = get_now_or_fake_time(pctx.request) if form.is_valid(): if op == "add": from datetime import timedelta ifr = InstantFlowRequest() ifr.course = pctx.course ifr.flow_id = form.cleaned_data["flow_id"] ifr.start_time = now_datetime ifr.end_time = ( now_datetime + timedelta( minutes=form.cleaned_data["duration_in_minutes"])) ifr.save() elif op == "cancel": (InstantFlowRequest.objects .filter( course=pctx.course, start_time__lte=now_datetime, end_time__gte=now_datetime, cancelled=False) .order_by("start_time") .update(cancelled=True)) else: raise SuspiciousOperation("invalid operation") else: form = InstantFlowRequestForm(flow_ids) return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": "Manage Instant Flow Requests", })
def validate_course_content(repo, course_file, events_file, validate_sha, course=None): vctx = ValidationContext( repo=repo, commit_sha=validate_sha, course=course) course_desc = get_yaml_from_repo_safely(repo, course_file, commit_sha=validate_sha) validate_staticpage_desc(vctx, course_file, course_desc) try: from course.content import get_yaml_from_repo events_desc = get_yaml_from_repo(repo, events_file, commit_sha=validate_sha, cached=False) except ObjectDoesNotExist: if events_file != "events.yml": vctx.add_warning( _("Events file"), _("Your course repository does not have an events " "file named '%s'.") % events_file) else: # That's OK--no calendar info. pass else: validate_calendar_desc_struct(vctx, events_file, events_desc) if vctx.course is not None: from course.models import ( ParticipationPermission, ParticipationRolePermission) access_kinds = frozenset( ParticipationPermission.objects .filter( participation__course=vctx.course, permission=pperm.access_files_for, ) .values_list("argument", flat=True)) | frozenset( ParticipationRolePermission.objects .filter( role__course=vctx.course, permission=pperm.access_files_for, ) .values_list("argument", flat=True)) else: access_kinds = ["public", "in_exam", "student", "ta", "unenrolled", "instructor"] check_attributes_yml( vctx, repo, "", get_repo_blob(repo, "", validate_sha), access_kinds) try: flows_tree = get_repo_blob(repo, "media", validate_sha) except ObjectDoesNotExist: # That's great--no media directory. pass else: vctx.add_warning( 'media/', _( "Your course repository has a 'media/' directory. " "Linking to media files using 'media:' is discouraged. " "Use the 'repo:' and 'repocur:' linkng schemes instead.")) # {{{ flows try: flows_tree = get_repo_blob(repo, "flows", validate_sha) except ObjectDoesNotExist: # That's OK--no flows yet. pass else: used_grade_identifiers = set() for entry in flows_tree.items(): entry_path = entry.path.decode("utf-8") if not entry_path.endswith(".yml"): continue from course.constants import FLOW_ID_REGEX flow_id = entry_path[:-4] match = re.match("^"+FLOW_ID_REGEX+"$", flow_id) if match is None: raise ValidationError( string_concat("%s: ", _("invalid flow name. " "Flow names may only contain (roman) " "letters, numbers, " "dashes and underscores.")) % entry_path) location = "flows/%s" % entry_path flow_desc = get_yaml_from_repo_safely(repo, location, commit_sha=validate_sha) validate_flow_desc(vctx, location, flow_desc) # {{{ check grade_identifier flow_grade_identifier = None if hasattr(flow_desc, "rules"): flow_grade_identifier = getattr( flow_desc.rules, "grade_identifier", None) if ( flow_grade_identifier is not None and set([flow_grade_identifier]) & used_grade_identifiers): raise ValidationError( string_concat("%s: ", _("flow uses the same grade_identifier " "as another flow")) % location) used_grade_identifiers.add(flow_grade_identifier) if (course is not None and flow_grade_identifier is not None): check_grade_identifier_link( vctx, location, course, flow_id, flow_grade_identifier) # }}} if course is not None: check_for_page_type_changes( vctx, location, course, flow_id, flow_desc) # }}} # {{{ static pages try: pages_tree = get_repo_blob(repo, "staticpages", validate_sha) except ObjectDoesNotExist: # That's OK--no flows yet. pass else: for entry in pages_tree.items(): entry_path = entry.path.decode("utf-8") if not entry_path.endswith(".yml"): continue from course.constants import STATICPAGE_PATH_REGEX page_name = entry_path[:-4] match = re.match("^"+STATICPAGE_PATH_REGEX+"$", page_name) if match is None: raise ValidationError( string_concat("%s: ", _( "invalid page name. " "Page names may only contain " "alphanumeric characters (any language) " "and hyphens." )) % entry_path) location = "staticpages/%s" % entry_path page_desc = get_yaml_from_repo_safely(repo, location, commit_sha=validate_sha) validate_staticpage_desc(vctx, location, page_desc) # }}} return vctx.warnings
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 validate_course_content(repo, course_file, events_file, validate_sha, course=None): course_desc = get_yaml_from_repo_safely(repo, course_file, commit_sha=validate_sha) vctx = ValidationContext(repo=repo, commit_sha=validate_sha, course=course) validate_course_desc_struct(vctx, course_file, course_desc) try: from course.content import get_yaml_from_repo events_desc = get_yaml_from_repo(repo, events_file, commit_sha=validate_sha, cached=False) except ObjectDoesNotExist: if events_file != "events.yml": vctx.add_warning( _("Events file"), _("Your course repository does not have an events " "file named '%s'.") % events_file) else: # That's OK--no calendar info. pass else: validate_calendar_desc_struct(vctx, events_file, events_desc) check_attributes_yml(vctx, repo, "", get_repo_blob(repo, "", validate_sha)) try: flows_tree = get_repo_blob(repo, "media", validate_sha) except ObjectDoesNotExist: # That's great--no media directory. pass else: vctx.add_warning( 'media/', _("Your course repository has a 'media/' directory. " "Linking to media files using 'media:' is discouraged. " "Use the 'repo:' and 'repocur:' linkng schemes instead.")) try: flows_tree = get_repo_blob(repo, "flows", validate_sha) except ObjectDoesNotExist: # That's OK--no flows yet. pass else: used_grade_identifiers = set() for entry in flows_tree.items(): entry_path = entry.path.decode("utf-8") if not entry_path.endswith(".yml"): continue from course.constants import FLOW_ID_REGEX flow_id = entry_path[:-4] match = re.match("^" + FLOW_ID_REGEX + "$", flow_id) if match is None: raise ValidationError( string_concat( "%s: ", _("invalid flow name. " "Flow names may only contain (roman) " "letters, numbers, " "dashes and underscores.")) % entry_path) location = "flows/%s" % entry_path flow_desc = get_yaml_from_repo_safely(repo, location, commit_sha=validate_sha) validate_flow_desc(vctx, location, flow_desc) # {{{ check grade_identifier flow_grade_identifier = None if hasattr(flow_desc, "rules"): flow_grade_identifier = getattr(flow_desc.rules, "grade_identifier", None) if (flow_grade_identifier is not None and set([flow_grade_identifier]) & used_grade_identifiers): raise ValidationError( string_concat( "%s: ", _("flow uses the same grade_identifier " "as another flow")) % location) used_grade_identifiers.add(flow_grade_identifier) if (course is not None and flow_grade_identifier is not None): check_grade_identifier_link(vctx, location, course, flow_id, flow_grade_identifier) # }}} if course is not None: check_for_page_type_changes(vctx, location, course, flow_id, flow_desc) return vctx.warnings
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_python_run_with_retries(run_req, run_timeout=self.page_desc.timeout) 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, 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) 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( 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>"))) # }}} 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 runpy 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, six.text_type): 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.")) 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 validate_course_content(repo, course_file, events_file, validate_sha, course=None): course_desc = get_yaml_from_repo_safely(repo, course_file, commit_sha=validate_sha) vctx = ValidationContext(repo=repo, commit_sha=validate_sha, course=course) validate_course_desc_struct(vctx, course_file, course_desc) try: from course.content import get_yaml_from_repo events_desc = get_yaml_from_repo(repo, events_file, commit_sha=validate_sha, cached=False) except ObjectDoesNotExist: if events_file != "events.yml": vctx.add_warning( _("Events file"), _("Your course repository does not have an events " "file named '%s'.") % events_file ) else: # That's OK--no calendar info. pass else: validate_calendar_desc_struct(vctx, events_file, events_desc) check_attributes_yml(vctx, repo, "", get_repo_blob(repo, "", validate_sha)) try: flows_tree = get_repo_blob(repo, "media", validate_sha) except ObjectDoesNotExist: # That's great--no media directory. pass else: vctx.add_warning( "media/", _( "Your course repository has a 'media/' directory. " "Linking to media files using 'media:' is discouraged. " "Use the 'repo:' and 'repocur:' linkng schemes instead." ), ) try: flows_tree = get_repo_blob(repo, "flows", validate_sha) except ObjectDoesNotExist: # That's OK--no flows yet. pass else: used_grade_identifiers = set() for entry in flows_tree.items(): entry_path = entry.path.decode("utf-8") if not entry_path.endswith(".yml"): continue from course.constants import FLOW_ID_REGEX flow_id = entry_path[:-4] match = re.match("^" + FLOW_ID_REGEX + "$", flow_id) if match is None: raise ValidationError( string_concat( "%s: ", _( "invalid flow name. " "Flow names may only contain (roman) " "letters, numbers, " "dashes and underscores." ), ) % entry_path ) location = "flows/%s" % entry_path flow_desc = get_yaml_from_repo_safely(repo, location, commit_sha=validate_sha) validate_flow_desc(vctx, location, flow_desc) # {{{ check grade_identifier flow_grade_identifier = None if hasattr(flow_desc, "rules"): flow_grade_identifier = getattr(flow_desc.rules, "grade_identifier", None) if flow_grade_identifier is not None and set([flow_grade_identifier]) & used_grade_identifiers: raise ValidationError( string_concat("%s: ", _("flow uses the same grade_identifier " "as another flow")) % location ) used_grade_identifiers.add(flow_grade_identifier) if course is not None and flow_grade_identifier is not None: check_grade_identifier_link(vctx, location, course, flow_id, flow_grade_identifier) # }}} if course is not None: check_for_page_type_changes(vctx, location, course, flow_id, flow_desc) return vctx.warnings