Exemplo n.º 1
0
    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."))
Exemplo n.º 2
0
    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."))
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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
Exemplo n.º 5
0
    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))
Exemplo n.º 6
0
    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))
Exemplo n.º 7
0
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
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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)
Exemplo n.º 10
0
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)
Exemplo n.º 11
0
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
Exemplo n.º 12
0
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)
Exemplo n.º 13
0
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)
Exemplo n.º 14
0
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
Exemplo n.º 15
0
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",
    })
Exemplo n.º 16
0
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
Exemplo n.º 17
0
    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))
Exemplo n.º 18
0
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
Exemplo n.º 19
0
    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)
        )
Exemplo n.º 20
0
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
Exemplo n.º 21
0
    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))
Exemplo n.º 22
0
    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))
Exemplo n.º 23
0
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