Skip to content
test_flow.py 205 KiB
Newer Older
Dong Zhuang's avatar
Dong Zhuang committed
__copyright__ = "Copyright (C) 2018 Dong Zhuang"

__license__ = """
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""

Dong Zhuang's avatar
Dong Zhuang committed
import itertools
Andreas Klöckner's avatar
Andreas Klöckner committed
import unittest
Dong Zhuang's avatar
Dong Zhuang committed

Josh Asplund's avatar
Josh Asplund committed
import pytest
Dong Zhuang's avatar
Dong Zhuang committed
from django import http
from django.contrib.auth.models import AnonymousUser
Andreas Klöckner's avatar
Andreas Klöckner committed
from django.contrib.sessions.middleware import SessionMiddleware
from django.core import mail
Dong Zhuang's avatar
Dong Zhuang committed
from django.core.exceptions import PermissionDenied
Andreas Klöckner's avatar
Andreas Klöckner committed
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
Dong Zhuang's avatar
Dong Zhuang committed
from django.utils.timezone import now, timedelta

Andreas Klöckner's avatar
Andreas Klöckner committed
from course import constants, flow, models
from course.constants import (
    flow_permission as fperm,
    grade_aggregation_strategy as g_strategy,
Andreas Klöckner's avatar
Andreas Klöckner committed
)
from course.utils import FlowSessionGradingRule, FlowSessionStartRule
from relate.utils import StyledForm, dict_to_struct
from tests import factories
Dong Zhuang's avatar
Dong Zhuang committed
from tests.base_test_mixins import (
    CoursesTestMixinBase,
    HackRepoMixin,
    SingleCourseQuizPageTestMixin,
Andreas Klöckner's avatar
Andreas Klöckner committed
    SingleCourseTestMixin,
)
Dong Zhuang's avatar
Dong Zhuang committed
from tests.constants import QUIZ_FLOW_ID
Josh Asplund's avatar
Josh Asplund committed
from tests.utils import mock
Dong Zhuang's avatar
Dong Zhuang committed
def get_flow_permissions_list(excluded=None):
    if not isinstance(excluded, list):
        excluded = [excluded]
    all_flow_permissions = dict(constants.FLOW_PERMISSION_CHOICES).keys()
    return [fp for fp in all_flow_permissions if fp not in excluded]
Dong Zhuang's avatar
Dong Zhuang committed
#{{{ test flow.adjust_flow_session_page_data

def flow_page_data_save_side_effect(self, *args, **kwargs):
    if self.page_id == "half1":
        raise RuntimeError("this error should not have been raised!")


Dong Zhuang's avatar
Dong Zhuang committed
class AdjustFlowSessionPageDataTest(
Dong Zhuang's avatar
Dong Zhuang committed
        SingleCourseQuizPageTestMixin, HackRepoMixin, TestCase):
Dong Zhuang's avatar
Dong Zhuang committed
    # test flow.adjust_flow_session_page_data

Dong Zhuang's avatar
Dong Zhuang committed
    initial_commit_sha = "my_fake_commit_sha_1"
Dong Zhuang's avatar
Dong Zhuang committed
    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()
        client = Client()
        client.force_login(cls.student_participation.user)
        cls.start_flow(client, flow_id=cls.flow_id)
Dong Zhuang's avatar
Dong Zhuang committed
    def test_remove_rename_and_revive(self):
Dong Zhuang's avatar
Dong Zhuang committed
        # {{{ 1st round: do a visit
        resp = self.client.get(self.get_page_url_by_ordinal(0))
Dong Zhuang's avatar
Dong Zhuang committed
        self.assertEqual(resp.status_code, 200)

        fpds_1st = models.FlowPageData.objects.all()
        fpd_ids_1st = list(fpds_1st.values_list("page_id", flat=True))
        welcome_page_title_1st = fpds_1st.get(page_id="welcome").title
        # }}}

        # {{{ 2nd round: change sha
        self.course.active_git_commit_sha = "my_fake_commit_sha_2"
        self.course.save()

        resp = self.client.get(self.get_page_url_by_ordinal(0))
Dong Zhuang's avatar
Dong Zhuang committed
        self.assertEqual(resp.status_code, 200)

        fpds_2nd = models.FlowPageData.objects.all()
        welcome_page_title_2nd = fpds_2nd.get(page_id="welcome").title
        fpd_ids_2nd = list(fpds_2nd.values_list("page_id", flat=True))

        # the page (with page_id "welcome") has changed title
        self.assertNotEqual(welcome_page_title_1st, welcome_page_title_2nd)

        # these two pages have removed page_ordinal
        # (those in group2 are not considered)
        page_ids_removed_in_2nd = {"half1", "lsq2"}
        self.assertTrue(
            page_ids_removed_in_2nd
            < set(fpds_2nd.filter(
Dong Zhuang's avatar
Dong Zhuang committed
                    page_ordinal=None).values_list("page_id", flat=True)))

        page_ids_introduced_in_2nd = {"half1_id_renamed", "half_again2"}
        self.assertNotIn(page_ids_introduced_in_2nd, fpd_ids_1st)
        self.assertTrue(page_ids_introduced_in_2nd < set(fpd_ids_2nd))

        self.assertTrue(set(fpd_ids_2nd) > set(fpd_ids_1st))
        # }}}

        # {{{ 3rd round: revive back
        self.course.active_git_commit_sha = "my_fake_commit_sha_1"
        self.course.save()

        resp = self.client.get(self.get_page_url_by_ordinal(0))
Dong Zhuang's avatar
Dong Zhuang committed
        self.assertEqual(resp.status_code, 200)

        fpds_3rd = models.FlowPageData.objects.all()
        fpd_ids_3rd = list(fpds_3rd.values_list("page_id", flat=True))
        welcome_page_title_3rd = fpds_2nd.get(page_id="welcome").title
        self.assertEqual(welcome_page_title_1st, welcome_page_title_3rd)

        # no page_data instances are removed
        self.assertSetEqual(set(fpd_ids_2nd), set(fpd_ids_3rd))
        self.assertSetEqual(
            page_ids_introduced_in_2nd,
            set(fpds_3rd.filter(
                    page_ordinal=None).values_list("page_id", flat=True)))
Dong Zhuang's avatar
Dong Zhuang committed
        for page_id in page_ids_removed_in_2nd:
            self.assertIsNotNone(
                models.FlowPageData.objects.get(page_id=page_id).page_ordinal)
Dong Zhuang's avatar
Dong Zhuang committed
            # }}}
    # disabled by AK 2020-05-03: why is it valid to set a flow page's ordinal
    # to None while it is in use?
    def no_test_remove_page_with_non_ordinal(self):
        resp = self.client.get(self.get_page_url_by_ordinal(0))
Dong Zhuang's avatar
Dong Zhuang committed
        self.assertEqual(resp.status_code, 200)

        # change this page's ordinal to None before change the commit_sha,
        # so that no save is needed when update course, for this page
        fpd = models.FlowPageData.objects.get(page_id="half1")
        fpd.page_ordinal = None
        fpd.save()

        with mock.patch(
                "course.models.FlowPageData.save",
                autospec=True) as mock_fpd_save:
            mock_fpd_save.side_effect = flow_page_data_save_side_effect

            self.course.active_git_commit_sha = "my_fake_commit_sha_2"
            self.course.save()

            resp = self.client.get(self.get_page_url_by_ordinal(0))
Dong Zhuang's avatar
Dong Zhuang committed
            self.assertEqual(resp.status_code, 200)

Dong Zhuang's avatar
Dong Zhuang committed
# }}}

Dong Zhuang's avatar
Dong Zhuang committed

class GradePageVisitTest(SingleCourseQuizPageTestMixin, TestCase):
    # patching tests for flow.grade_page_visits
    def test_not_is_submitted_answer(self):
        visit = mock.MagicMock()
        visit_grade_model = mock.MagicMock()
        visit.is_submitted_answer = False

        expected_error_msg = "cannot grade ungraded answer"
        with self.assertRaises(RuntimeError) as cm:
            flow.grade_page_visit(visit, visit_grade_model)
        self.assertIn(expected_error_msg, str(cm.exception))

        with self.assertRaises(RuntimeError) as cm:
            flow.grade_page_visit(visit, visit_grade_model, {"key": "value"})
        self.assertIn(expected_error_msg, str(cm.exception))

        with self.assertRaises(RuntimeError) as cm:
            flow.grade_page_visit(visit, visit_grade_model, {"key": "value"}, False)
        self.assertIn(expected_error_msg, str(cm.exception))

    def test_page_answer_not_gradable(self):
        with self.temporarily_switch_to_user(self.student_participation.user):
            self.start_flow(self.flow_id)
            fpvgs = models.FlowPageVisitGrade.objects.all()
            self.assertEqual(fpvgs.count(), 0)

            page_id = "age_group"

            self.submit_page_answer_by_page_id_and_test(
                page_id, do_grading=True, expected_grades=0)

            fpvgs = models.FlowPageVisitGrade.objects.filter(
Loading
Loading full blame...