Example #1
0
    def __init__(self,
                 repo,
                 course,
                 flow_id,
                 ordinal,
                 participation,
                 flow_session,
                 request=None):
        FlowContext.__init__(self,
                             repo,
                             course,
                             flow_id,
                             participation,
                             flow_session=flow_session)

        from course.content import adjust_flow_session_page_data
        adjust_flow_session_page_data(repo, flow_session, course.identifier,
                                      self.flow_desc)

        if ordinal >= flow_session.page_count:
            raise PageOrdinalOutOfRange()

        from course.models import FlowPageData
        page_data = self.page_data = get_object_or_404(
            FlowPageData, flow_session=flow_session, ordinal=ordinal)

        from course.content import get_flow_page_desc
        try:
            self.page_desc = get_flow_page_desc(flow_session, self.flow_desc,
                                                page_data.group_id,
                                                page_data.page_id)
        except ObjectDoesNotExist:
            self.page_desc = None
            self.page = None
            self.page_context = None
        else:
            self.page = instantiate_flow_page_with_ctx(self, page_data)

            page_uri = None
            if request is not None:
                from django.core.urlresolvers import reverse
                page_uri = request.build_absolute_uri(
                    reverse("relate-view_flow_page",
                            args=(course.identifier, flow_session.id,
                                  ordinal)))

            from course.page import PageContext
            self.page_context = PageContext(course=self.course,
                                            repo=self.repo,
                                            commit_sha=self.course_commit_sha,
                                            flow_session=flow_session,
                                            page_uri=page_uri)

        self._prev_answer_visit = False
Example #2
0
    def __init__(self, repo, course, flow_id, ordinal, participation,
                 flow_session):
        FlowContext.__init__(self,
                             repo,
                             course,
                             flow_id,
                             participation,
                             flow_session=flow_session)

        from course.content import adjust_flow_session_page_data
        adjust_flow_session_page_data(repo, flow_session, course.identifier,
                                      self.flow_desc, self.course_commit_sha)

        if ordinal >= flow_session.page_count:
            raise PageOrdinalOutOfRange()

        from course.models import FlowPageData
        page_data = self.page_data = get_object_or_404(
            FlowPageData, flow_session=flow_session, ordinal=ordinal)

        from course.content import get_flow_page_desc
        try:
            self.page_desc = get_flow_page_desc(flow_session, self.flow_desc,
                                                page_data.group_id,
                                                page_data.page_id)
        except ObjectDoesNotExist:
            self.page_desc = None
            self.page = None
            self.page_context = None
        else:
            self.page = instantiate_flow_page_with_ctx(self, page_data)

            from course.page import PageContext
            self.page_context = PageContext(course=self.course,
                                            repo=self.repo,
                                            commit_sha=self.course_commit_sha,
                                            flow_session=flow_session)

        self._prev_answer_visit = False
Example #3
0
def page_analytics(pctx, flow_id, group_id, page_id):
    if pctx.role not in [
            participation_role.teaching_assistant,
            participation_role.instructor,
            participation_role.observer,
            ]:
        raise PermissionDenied(_("must be at least TA to view analytics"))

    flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id,
            pctx.course_commit_sha)

    restrict_to_first_attempt = int(
            bool(pctx.request.GET.get("restrict_to_first_attempt") == "1"))

    is_multiple_submit = is_flow_multiple_submit(flow_desc)

    page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id)

    visits = (FlowPageVisit.objects
            .filter(
                flow_session__course=pctx.course,
                flow_session__flow_id=flow_id,
                page_data__group_id=group_id,
                page_data__page_id=page_id,
                is_submitted_answer=True,
                ))

    if connection.features.can_distinct_on_fields:
        if restrict_to_first_attempt:
            visits = (visits
                    .distinct("flow_session__participation__id")
                    .order_by("flow_session__participation__id", "visit_time"))
        elif is_multiple_submit:
            visits = (visits
                    .distinct("page_data__id")
                    .order_by("page_data__id", "-visit_time"))

    visits = (visits
            .select_related("flow_session")
            .select_related("page_data"))

    normalized_answer_and_correctness_to_count = {}

    title = None
    body = None
    total_count = 0
    graded_count = 0

    for visit in visits:
        page = page_cache.get_page(group_id, page_id, pctx.course_commit_sha)

        from course.page import PageContext
        grading_page_context = PageContext(
                course=pctx.course,
                repo=pctx.repo,
                commit_sha=pctx.course_commit_sha,
                flow_session=visit.flow_session)

        title = page.title(grading_page_context, visit.page_data.data)
        body = page.body(grading_page_context, visit.page_data.data)
        normalized_answer = page.normalized_answer(
                grading_page_context, visit.page_data.data, visit.answer)

        answer_feedback = visit.get_most_recent_feedback()

        if answer_feedback is not None:
            key = (normalized_answer, answer_feedback.correctness)
            normalized_answer_and_correctness_to_count[key] = \
                    normalized_answer_and_correctness_to_count.get(key, 0) + 1
            graded_count += 1
        else:
            key = (normalized_answer, None)
            normalized_answer_and_correctness_to_count[key] = \
                    normalized_answer_and_correctness_to_count.get(key, 0) + 1

        total_count += 1

    answer_stats = []
    for (normalized_answer, correctness), count in \
            normalized_answer_and_correctness_to_count.iteritems():
        answer_stats.append(
                AnswerStats(
                    normalized_answer=normalized_answer,
                    correctness=correctness,
                    count=count,
                    percentage=safe_div(100 * count, total_count)))

    answer_stats = sorted(
            answer_stats,
            key=lambda astats: astats.percentage,
            reverse=True)

    return render_course_page(pctx, "course/analytics-page.html", {
        "flow_identifier": flow_id,
        "group_id": group_id,
        "page_id": page_id,
        "title": title,
        "body": body,
        "answer_stats_list": answer_stats,
        "restrict_to_first_attempt": restrict_to_first_attempt,
        })
Example #4
0
def make_page_answer_stats_list(pctx, flow_id, restrict_to_first_attempt):
    flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id,
            pctx.course_commit_sha)

    is_multiple_submit = is_flow_multiple_submit(flow_desc)

    page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id)

    page_info_list = []
    for group_desc in flow_desc.groups:
        for page_desc in group_desc.pages:
            points = 0
            graded_count = 0

            answer_count = 0
            total_count = 0

            visits = (FlowPageVisit.objects
                    .filter(
                        flow_session__course=pctx.course,
                        flow_session__flow_id=flow_id,
                        page_data__group_id=group_desc.id,
                        page_data__page_id=page_desc.id,
                        is_submitted_answer=True,
                        ))

            if connection.features.can_distinct_on_fields:
                if restrict_to_first_attempt:
                    visits = (visits
                            .distinct("flow_session__participation__id")
                            .order_by("flow_session__participation__id",
                                "visit_time"))
                elif is_multiple_submit:
                    visits = (visits
                            .distinct("page_data__id")
                            .order_by("page_data__id", "-visit_time"))

            visits = (visits
                    .select_related("flow_session")
                    .select_related("page_data"))

            answer_expected = False

            title = None
            for visit in visits:
                page = page_cache.get_page(group_desc.id, page_desc.id,
                        pctx.course_commit_sha)

                answer_expected = answer_expected or page.expects_answer()

                from course.page import PageContext
                grading_page_context = PageContext(
                        course=pctx.course,
                        repo=pctx.repo,
                        commit_sha=pctx.course_commit_sha,
                        flow_session=visit.flow_session)

                title = page.title(grading_page_context, visit.page_data.data)

                answer_feedback = visit.get_most_recent_feedback()

                if visit.answer is not None:
                    answer_count += 1
                total_count += 1

                if (answer_feedback is not None
                        and answer_feedback.correctness is not None):
                    if visit.answer is None:
                        assert answer_feedback.correctness == 0
                    else:
                        points += answer_feedback.correctness

                    graded_count += 1

            if not answer_expected:
                continue

            page_info_list.append(
                    PageAnswerStats(
                        group_id=group_desc.id,
                        page_id=page_desc.id,
                        title=title,
                        average_correctness=safe_div(points, graded_count),
                        average_emptiness=safe_div(
                            graded_count - answer_count, graded_count),
                        answer_count=answer_count,
                        total_count=total_count,
                        url=reverse(
                            "relate-page_analytics",
                            args=(
                                pctx.course_identifier,
                                flow_id,
                                group_desc.id,
                                page_desc.id,
                                ))))

    return page_info_list
Example #5
0
def view_page_sandbox(pctx):
    # type: (CoursePageContext) -> http.HttpResponse

    if not pctx.has_permission(pperm.use_page_sandbox):
        raise PermissionDenied()

    from course.validation import ValidationError
    from relate.utils import dict_to_struct, Struct
    import yaml

    PAGE_SESSION_KEY = make_sandbox_session_key(  # noqa
        PAGE_SESSION_KEY_PREFIX, pctx.course.identifier)
    ANSWER_DATA_SESSION_KEY = make_sandbox_session_key(  # noqa
        ANSWER_DATA_SESSION_KEY_PREFIX, pctx.course.identifier)
    PAGE_DATA_SESSION_KEY = make_sandbox_session_key(  # noqa
        PAGE_DATA_SESSION_KEY_PREFIX, pctx.course.identifier)

    request = pctx.request
    page_source = pctx.request.session.get(PAGE_SESSION_KEY)

    page_errors = None
    page_warnings = None

    is_clear_post = (request.method == "POST" and "clear" in request.POST)
    is_clear_response_post = (request.method == "POST"
                              and "clear_response" in request.POST)
    is_preview_post = (request.method == "POST" and "preview" in request.POST)

    def make_form(data=None):
        # type: (Optional[Text]) -> PageSandboxForm
        return PageSandboxForm(page_source, "yaml", request.user.editor_mode,
                               ugettext("Enter YAML markup for a flow page."),
                               data)

    if is_preview_post:
        edit_form = make_form(pctx.request.POST)
        new_page_source = None

        if edit_form.is_valid():
            try:
                from pytools.py_codegen import remove_common_indentation
                new_page_source = remove_common_indentation(
                    edit_form.cleaned_data["content"],
                    require_leading_newline=False)
                from course.content import expand_yaml_macros
                new_page_source = expand_yaml_macros(pctx.repo,
                                                     pctx.course_commit_sha,
                                                     new_page_source)

                yaml_data = yaml.load(new_page_source)  # type: ignore
                page_desc = dict_to_struct(yaml_data)

                if not isinstance(page_desc, Struct):
                    raise ValidationError(
                        "Provided page source code is not "
                        "a dictionary. Do you need to remove a leading "
                        "list marker ('-') or some stray indentation?")

                from course.validation import validate_flow_page, ValidationContext
                vctx = ValidationContext(repo=pctx.repo,
                                         commit_sha=pctx.course_commit_sha)

                validate_flow_page(vctx, "sandbox", page_desc)

                page_warnings = vctx.warnings

            except Exception:
                import sys
                tp, e, _ = sys.exc_info()

                page_errors = (ugettext("Page failed to load/validate") +
                               ": " + "%(err_type)s: %(err_str)s" % {
                                   "err_type": tp.__name__,
                                   "err_str": e
                               })  # type: ignore

            else:
                # Yay, it did validate.
                request.session[
                    PAGE_SESSION_KEY] = page_source = new_page_source

            del new_page_source

        edit_form = make_form(pctx.request.POST)

    elif is_clear_post:
        page_source = None
        pctx.request.session[PAGE_DATA_SESSION_KEY] = None
        pctx.request.session[ANSWER_DATA_SESSION_KEY] = None
        del pctx.request.session[PAGE_DATA_SESSION_KEY]
        del pctx.request.session[ANSWER_DATA_SESSION_KEY]
        edit_form = make_form()

    elif is_clear_response_post:
        page_source = None
        pctx.request.session[PAGE_DATA_SESSION_KEY] = None
        pctx.request.session[ANSWER_DATA_SESSION_KEY] = None
        del pctx.request.session[PAGE_DATA_SESSION_KEY]
        del pctx.request.session[ANSWER_DATA_SESSION_KEY]
        edit_form = make_form(pctx.request.POST)

    else:
        edit_form = make_form()

    have_valid_page = page_source is not None
    if have_valid_page:
        yaml_data = yaml.load(page_source)  # type: ignore
        page_desc = cast(FlowPageDesc, dict_to_struct(yaml_data))

        from course.content import instantiate_flow_page
        try:
            page = instantiate_flow_page("sandbox", pctx.repo, page_desc,
                                         pctx.course_commit_sha)
        except Exception:
            import sys
            tp, e, _ = sys.exc_info()

            page_errors = (ugettext("Page failed to load/validate") + ": " +
                           "%(err_type)s: %(err_str)s" % {
                               "err_type": tp.__name__,
                               "err_str": e
                           })  # type: ignore
            have_valid_page = False

    if have_valid_page:
        page_desc = cast(FlowPageDesc, page_desc)

        # Try to recover page_data, answer_data
        page_data = get_sandbox_data_for_page(pctx, page_desc,
                                              PAGE_DATA_SESSION_KEY)

        answer_data = get_sandbox_data_for_page(pctx, page_desc,
                                                ANSWER_DATA_SESSION_KEY)

        from course.models import FlowSession
        from course.page import PageContext
        page_context = PageContext(
            course=pctx.course,
            repo=pctx.repo,
            commit_sha=pctx.course_commit_sha,

            # This helps code pages retrieve the editor pref.
            flow_session=FlowSession(course=pctx.course,
                                     participation=pctx.participation),
            in_sandbox=True)

        if page_data is None:
            page_data = page.initialize_page_data(page_context)
            pctx.request.session[PAGE_DATA_SESSION_KEY] = (page_desc.type,
                                                           page_desc.id,
                                                           page_data)

        title = page.title(page_context, page_data)
        body = page.body(page_context, page_data)

        feedback = None
        page_form_html = None

        if page.expects_answer():
            from course.page.base import PageBehavior
            page_behavior = PageBehavior(show_correctness=True,
                                         show_answer=True,
                                         may_change_answer=True)

            if request.method == "POST" and not is_preview_post:
                page_form = page.process_form_post(page_context, page_data,
                                                   request.POST, request.FILES,
                                                   page_behavior)

                if page_form.is_valid():

                    answer_data = page.answer_data(page_context, page_data,
                                                   page_form, request.FILES)

                    feedback = page.grade(page_context,
                                          page_data,
                                          answer_data,
                                          grade_data=None)

                    pctx.request.session[ANSWER_DATA_SESSION_KEY] = (
                        page_desc.type, page_desc.id, answer_data)

            else:
                try:
                    page_form = page.make_form(page_context, page_data,
                                               answer_data, page_behavior)

                except Exception:
                    import sys
                    tp, e, _ = sys.exc_info()

                    page_errors = (
                        ugettext("Page failed to load/validate "
                                 "(change page ID to clear faults)") + ": " +
                        "%(err_type)s: %(err_str)s" % {
                            "err_type": tp.__name__,
                            "err_str": e
                        })  # type: ignore  # noqa: E501
                    have_valid_page = False

                    page_form = None

            if page_form is not None:
                page_form.helper.add_input(
                    Submit("submit", ugettext("Submit answer"), accesskey="g"))
                page_form_html = page.form_to_html(pctx.request, page_context,
                                                   page_form, answer_data)

        correct_answer = page.correct_answer(page_context,
                                             page_data,
                                             answer_data,
                                             grade_data=None)

        return render_course_page(
            pctx,
            "course/sandbox-page.html",
            {
                "edit_form": edit_form,
                "page_errors": page_errors,
                "page_warnings": page_warnings,
                "form": edit_form,  # to placate form.media
                "have_valid_page": True,
                "title": title,
                "body": body,
                "page_form_html": page_form_html,
                "feedback": feedback,
                "correct_answer": correct_answer,
            })

    else:

        return render_course_page(
            pctx,
            "course/sandbox-page.html",
            {
                "edit_form": edit_form,
                "form": edit_form,  # to placate form.media
                "have_valid_page": False,
                "page_errors": page_errors,
                "page_warnings": page_warnings,
            })
Example #6
0
def view_page_sandbox(pctx):
    from relate.utils import dict_to_struct
    import yaml

    PAGE_SESSION_KEY = "cf_validated_sandbox_page:" + pctx.course.identifier
    ANSWER_DATA_SESSION_KEY = "cf_page_sandbox_answer_data:" + pctx.course.identifier

    request = pctx.request
    page_source = pctx.request.session.get(PAGE_SESSION_KEY)

    page_errors = None

    is_preview_post = (request.method == "POST" and "preview" in request.POST)

    def make_form(data=None):
        return SandboxForm(
                page_source, "yaml", vim_mode,
                "Enter YAML markup for a flow page.",
                data)

    vim_mode = pctx.request.session.get(CF_SANDBOX_VIM_MODE, False)

    if is_preview_post:
        edit_form = make_form(pctx.request.POST)

        if edit_form.is_valid():
            pctx.request.session[CF_SANDBOX_VIM_MODE] = \
                    vim_mode = edit_form.cleaned_data["vim_mode"]

            try:
                new_page_source = edit_form.cleaned_data["content"]
                page_desc = dict_to_struct(yaml.load(new_page_source))

                from course.validation import validate_flow_page, ValidationContext
                vctx = ValidationContext(
                        repo=pctx.repo,
                        commit_sha=pctx.course_commit_sha)

                validate_flow_page(vctx, "sandbox", page_desc)

            except:
                import sys
                tp, e, _ = sys.exc_info()

                page_errors = (
                        "Page failed to load/validate: "
                        "%s: %s" % (tp.__name__, e))

            else:
                # Yay, it did validate.
                request.session[PAGE_SESSION_KEY] = page_source = new_page_source

            del new_page_source

        edit_form = make_form(pctx.request.POST)

    else:
        edit_form = make_form()

    have_valid_page = page_source is not None
    if have_valid_page:
        page_desc = dict_to_struct(yaml.load(page_source))

        from course.content import instantiate_flow_page
        page = instantiate_flow_page("sandbox", pctx.repo, page_desc,
                pctx.course_commit_sha)

        page_data = page.make_page_data()

        from course.page import PageContext
        page_context = PageContext(
                course=pctx.course,
                repo=pctx.repo,
                commit_sha=pctx.course_commit_sha,
                flow_session=None)

        title = page.title(page_context, page_data)
        body = page.body(page_context, page_data)

        # {{{ try to recover answer_data

        answer_data = None

        stored_answer_data_tuple = \
                pctx.request.session.get(ANSWER_DATA_SESSION_KEY)

        # Session storage uses JSON and may turn tuples into lists.
        if (isinstance(stored_answer_data_tuple, (list, tuple))
                and len(stored_answer_data_tuple) == 2):
            stored_answer_data_page_id, stored_answer_data = \
                    stored_answer_data_tuple

            if stored_answer_data_page_id == page_desc.id:
                answer_data = stored_answer_data

        # }}}

        feedback = None
        page_form_html = None

        if page.expects_answer():
            if request.method == "POST" and not is_preview_post:
                page_form = page.post_form(page_context, page_data,
                        request.POST, request.FILES)

                if page_form.is_valid():

                    answer_data = page.answer_data(page_context, page_data,
                            page_form, request.FILES)

                    feedback = page.grade(page_context, page_data, answer_data,
                            grade_data=None)

                    pctx.request.session[ANSWER_DATA_SESSION_KEY] = (
                            page_desc.id, answer_data)

            else:
                page_form = page.make_form(page_context, page_data,
                        answer_data, answer_is_final=False)

            if page_form is not None:
                page_form.helper.add_input(
                        Submit("submit", "Submit answer", accesskey="g",
                            css_class="col-lg-offset-2"))
                page_form_html = page.form_to_html(
                        pctx.request, page_context, page_form, answer_data)

        correct_answer = page.correct_answer(
                page_context, page_data, answer_data,
                grade_data=None)

        return render_course_page(pctx, "course/sandbox-page.html", {
            "edit_form": edit_form,
            "page_errors": page_errors,
            "form": edit_form,  # to placate form.media
            "have_valid_page": True,
            "title": title,
            "body": body,
            "page_form_html": page_form_html,
            "feedback": feedback,
            "correct_answer": correct_answer,
        })

    else:

        return render_course_page(pctx, "course/sandbox-page.html", {
            "edit_form": edit_form,
            "form": edit_form,  # to placate form.media
            "have_valid_page": False,
            "page_errors": page_errors,
        })
Example #7
0
def download_all_submissions(pctx, flow_id):
    if pctx.role not in [
            participation_role.teaching_assistant,
            participation_role.instructor,
            participation_role.observer,
    ]:
        raise PermissionDenied(
            _("must be at least TA to download submissions"))

    from course.content import get_flow_desc
    flow_desc = get_flow_desc(pctx.repo, pctx.course, flow_id,
                              pctx.course_commit_sha)

    # {{{ find access rules tag

    if hasattr(flow_desc, "rules"):
        access_rules_tags = getattr(flow_desc.rules, "tags", [])
    else:
        access_rules_tags = []

    ALL_SESSION_TAG = string_concat("<<<", _("ALL"), ">>>")  # noqa
    session_tag_choices = [(tag, tag) for tag in access_rules_tags] + [
        (ALL_SESSION_TAG, string_concat("(", _("ALL"), ")"))
    ]

    # }}}

    page_ids = [
        "%s/%s" % (group_desc.id, page_desc.id)
        for group_desc in flow_desc.groups for page_desc in group_desc.pages
    ]

    request = pctx.request
    if request.method == "POST":
        form = DownloadAllSubmissionsForm(page_ids, session_tag_choices,
                                          request.POST)

        if form.is_valid():
            which_attempt = form.cleaned_data["which_attempt"]

            slash_index = form.cleaned_data["page_id"].index("/")
            group_id = form.cleaned_data["page_id"][:slash_index]
            page_id = form.cleaned_data["page_id"][slash_index + 1:]

            from course.utils import PageInstanceCache
            page_cache = PageInstanceCache(pctx.repo, pctx.course, flow_id)

            visits = (
                FlowPageVisit.objects.filter(
                    flow_session__course=pctx.course,
                    flow_session__flow_id=flow_id,
                    page_data__group_id=group_id,
                    page_data__page_id=page_id,
                    is_submitted_answer=True,
                ).select_related("flow_session").select_related(
                    "flow_session__participation__user").select_related(
                        "page_data")

                # We overwrite earlier submissions with later ones
                # in a dictionary below.
                .order_by("visit_time"))

            if form.cleaned_data["non_in_progress_only"]:
                visits = visits.filter(flow_session__in_progress=False)

            if form.cleaned_data["restrict_to_rules_tag"] != ALL_SESSION_TAG:
                visits = visits.filter(flow_session__access_rules_tag=(
                    form.cleaned_data["restrict_to_rules_tag"]))

            submissions = {}

            for visit in visits:
                page = page_cache.get_page(group_id, page_id,
                                           pctx.course_commit_sha)

                from course.page import PageContext
                grading_page_context = PageContext(
                    course=pctx.course,
                    repo=pctx.repo,
                    commit_sha=pctx.course_commit_sha,
                    flow_session=visit.flow_session)

                bytes_answer = page.normalized_bytes_answer(
                    grading_page_context, visit.page_data.data, visit.answer)

                if which_attempt in ["first", "last"]:
                    key = (visit.flow_session.participation.user.username, )
                elif which_attempt == "all":
                    key = (visit.flow_session.participation.user.username,
                           str(visit.flow_session.id))
                else:
                    raise NotImplementedError()

                if bytes_answer is not None:
                    if (which_attempt == "first" and key in submissions):
                        # Already there, disregard further ones
                        continue

                    submissions[key] = bytes_answer

            from six import BytesIO
            from zipfile import ZipFile
            bio = BytesIO()
            with ZipFile(bio, "w") as subm_zip:
                for key, (extension, bytes_answer) in \
                        six.iteritems(submissions):
                    subm_zip.writestr("-".join(key) + extension, bytes_answer)

                extra_file = request.FILES.get("extra_file")
                if extra_file is not None:
                    subm_zip.writestr(extra_file.name, extra_file.read())

            response = http.HttpResponse(bio.getvalue(),
                                         content_type="application/zip")
            response['Content-Disposition'] = (
                'attachment; filename="submissions_%s_%s_%s_%s_%s.zip"' %
                (pctx.course.identifier, flow_id, group_id, page_id,
                 now().date().strftime("%Y-%m-%d")))
            return response

    else:
        form = DownloadAllSubmissionsForm(page_ids, session_tag_choices)

    return render_course_page(
        pctx, "course/generic-course-form.html", {
            "form": form,
            "form_description": _("Download All Submissions in Zip file")
        })
Example #8
0
def view_page_sandbox(pctx):
    if pctx.role not in [
            participation_role.instructor,
            participation_role.teaching_assistant]:
        raise PermissionDenied(
                ugettext("must be instructor or TA to access sandbox"))

    from course.validation import ValidationError
    from relate.utils import dict_to_struct, Struct
    import yaml

    PAGE_SESSION_KEY = (  # noqa
            "cf_validated_sandbox_page:" + pctx.course.identifier)
    ANSWER_DATA_SESSION_KEY = (  # noqa
        "cf_page_sandbox_answer_data:" + pctx.course.identifier)

    request = pctx.request
    page_source = pctx.request.session.get(PAGE_SESSION_KEY)

    page_errors = None
    page_warnings = None

    is_preview_post = (request.method == "POST" and "preview" in request.POST)

    from course.models import get_user_status
    ustatus = get_user_status(request.user)

    def make_form(data=None):
        return SandboxForm(
                page_source, "yaml", ustatus.editor_mode,
                ugettext("Enter YAML markup for a flow page."),
                data)

    if is_preview_post:
        edit_form = make_form(pctx.request.POST)

        if edit_form.is_valid():
            try:
                new_page_source = edit_form.cleaned_data["content"]
                page_desc = dict_to_struct(yaml.load(new_page_source))

                if not isinstance(page_desc, Struct):
                    raise ValidationError("Provided page source code is not "
                            "a dictionary. Do you need to remove a leading "
                            "list marker ('-') or some stray indentation?")

                from course.validation import validate_flow_page, ValidationContext
                vctx = ValidationContext(
                        repo=pctx.repo,
                        commit_sha=pctx.course_commit_sha)

                validate_flow_page(vctx, "sandbox", page_desc)

                page_warnings = vctx.warnings

            except:
                import sys
                tp, e, _ = sys.exc_info()

                page_errors = (
                        ugettext("Page failed to load/validate")
                        + ": "
                        + "%(err_type)s: %(err_str)s" % {
                            "err_type": tp.__name__, "err_str": e})

            else:
                # Yay, it did validate.
                request.session[PAGE_SESSION_KEY] = page_source = new_page_source

            del new_page_source

        edit_form = make_form(pctx.request.POST)

    else:
        edit_form = make_form()

    have_valid_page = page_source is not None
    if have_valid_page:
        page_desc = dict_to_struct(yaml.load(page_source))

        from course.content import instantiate_flow_page
        try:
            page = instantiate_flow_page("sandbox", pctx.repo, page_desc,
                    pctx.course_commit_sha)
        except:
            import sys
            tp, e, _ = sys.exc_info()

            page_errors = (
                    ugettext("Page failed to load/validate")
                    + ": "
                    + "%(err_type)s: %(err_str)s" % {
                        "err_type": tp.__name__, "err_str": e})
            have_valid_page = False

    if have_valid_page:
        page_data = page.make_page_data()

        from course.models import FlowSession
        from course.page import PageContext
        page_context = PageContext(
                course=pctx.course,
                repo=pctx.repo,
                commit_sha=pctx.course_commit_sha,

                # This helps code pages retrieve the editor pref.
                flow_session=FlowSession(
                    course=pctx.course,
                    participation=pctx.participation),

                in_sandbox=True)

        title = page.title(page_context, page_data)
        body = page.body(page_context, page_data)

        # {{{ try to recover answer_data

        answer_data = None

        stored_answer_data_tuple = \
                pctx.request.session.get(ANSWER_DATA_SESSION_KEY)

        # Session storage uses JSON and may turn tuples into lists.
        if (isinstance(stored_answer_data_tuple, (list, tuple))
                and len(stored_answer_data_tuple) == 3):
            stored_answer_data_page_type, stored_answer_data_page_id, stored_answer_data = \
                    stored_answer_data_tuple

            if (
                    stored_answer_data_page_type == page_desc.type
                    and
                    stored_answer_data_page_id == page_desc.id):
                answer_data = stored_answer_data

        # }}}

        feedback = None
        page_form_html = None

        if page.expects_answer():
            from course.page.base import PageBehavior
            page_behavior = PageBehavior(
                    show_correctness=True,
                    show_answer=True,
                    may_change_answer=True)

            if request.method == "POST" and not is_preview_post:
                page_form = page.process_form_post(page_context, page_data,
                        request.POST, request.FILES,
                        page_behavior)

                if page_form.is_valid():

                    answer_data = page.answer_data(page_context, page_data,
                            page_form, request.FILES)

                    feedback = page.grade(page_context, page_data, answer_data,
                            grade_data=None)

                    pctx.request.session[ANSWER_DATA_SESSION_KEY] = (
                            page_desc.type, page_desc.id, answer_data)

            else:
                page_form = page.make_form(page_context, page_data,
                        answer_data, page_behavior)

            if page_form is not None:
                page_form.helper.add_input(
                        Submit("submit",
                            ugettext("Submit answer"),
                            accesskey="g"))
                page_form_html = page.form_to_html(
                        pctx.request, page_context, page_form, answer_data)

        correct_answer = page.correct_answer(
                page_context, page_data, answer_data,
                grade_data=None)

        return render_course_page(pctx, "course/sandbox-page.html", {
            "edit_form": edit_form,
            "page_errors": page_errors,
            "page_warnings": page_warnings,
            "form": edit_form,  # to placate form.media
            "have_valid_page": True,
            "title": title,
            "body": body,
            "page_form_html": page_form_html,
            "feedback": feedback,
            "correct_answer": correct_answer,
        })

    else:

        return render_course_page(pctx, "course/sandbox-page.html", {
            "edit_form": edit_form,
            "form": edit_form,  # to placate form.media
            "have_valid_page": False,
            "page_errors": page_errors,
            "page_warnings": page_warnings,
        })