Skip to content
base_test_mixins.py 64 KiB
Newer Older
Dong Zhuang's avatar
Dong Zhuang committed
        return response

    def post_grade_by_page_id(self, page_id, grade_data,
                              course_identifier=None, flow_session_id=None,
                              force_login_instructor=True):
        page_ordinal = self.get_page_ordinal_via_page_id(
            page_id, course_identifier, flow_session_id)
Dong Zhuang's avatar
Dong Zhuang committed

        return self.post_grade_by_ordinal(
            page_ordinal, grade_data, course_identifier,
            flow_session_id, force_login_instructor)
    def assertSessionScoreEqual(  # noqa
            self, expect_score, course_identifier=None, flow_session_id=None):
        if flow_session_id is None:
            flow_params = self.get_flow_params(course_identifier, flow_session_id)
            flow_session_id = flow_params["flow_session_id"]
        flow_session = FlowSession.objects.get(id=flow_session_id)
            from decimal import Decimal
            self.assertEqual(flow_session.points, Decimal(str(expect_score)))
            self.assertIsNone(flow_session.points)

    def get_page_submit_history_url_by_ordinal(
            self, page_ordinal, course_identifier=None, flow_session_id=None):
        return self.get_page_view_url_by_ordinal(
            "relate-get_prev_answer_visits_dropdown_content",
            page_ordinal, course_identifier, flow_session_id)

    def get_page_grade_history_url_by_ordinal(
            self, page_ordinal, course_identifier=None, flow_session_id=None):
        return self.get_page_view_url_by_ordinal(
            "relate-get_prev_grades_dropdown_content",
            page_ordinal, course_identifier, flow_session_id)

    def get_page_submit_history_by_ordinal(
            self, page_ordinal, course_identifier=None, flow_session_id=None):
Dong Zhuang's avatar
Dong Zhuang committed
        resp = self.c.get(
            self.get_page_submit_history_url_by_ordinal(
                page_ordinal, course_identifier, flow_session_id),
Dong Zhuang's avatar
Dong Zhuang committed
            HTTP_X_REQUESTED_WITH='XMLHttpRequest')
        return resp

    def get_page_grade_history_by_ordinal(
            self, page_ordinal, course_identifier=None, flow_session_id=None):
Dong Zhuang's avatar
Dong Zhuang committed
        resp = self.c.get(
            self.get_page_grade_history_url_by_ordinal(
                page_ordinal, course_identifier, flow_session_id),
Dong Zhuang's avatar
Dong Zhuang committed
            HTTP_X_REQUESTED_WITH='XMLHttpRequest')
        return resp

    def assertSubmitHistoryItemsCount(  # noqa
            self, page_ordinal, expected_count, course_identifier=None,
            flow_session_id=None):
        resp = self.get_page_submit_history_by_ordinal(
            page_ordinal, course_identifier, flow_session_id)
Dong Zhuang's avatar
Dong Zhuang committed
        import json
        result = json.loads(resp.content.decode())["result"]
        self.assertEqual(len(result), expected_count)

    def assertGradeHistoryItemsCount(  # noqa
            self, page_ordinal, expected_count,
            course_identifier=None,
            flow_session_id=None,
Dong Zhuang's avatar
Dong Zhuang committed
            force_login_instructor=True):

        if course_identifier is None:
            course_identifier = self.get_default_course_identifier()
Dong Zhuang's avatar
Dong Zhuang committed

        if force_login_instructor:
            switch_to = self.get_default_instructor_user(course_identifier)
Dong Zhuang's avatar
Dong Zhuang committed
        else:
            switch_to = self.get_logged_in_user()

        with self.temporarily_switch_to_user(switch_to):
            resp = self.get_page_grade_history_by_ordinal(
                page_ordinal, course_identifier, flow_session_id)
Dong Zhuang's avatar
Dong Zhuang committed

Dong Zhuang's avatar
Dong Zhuang committed
        import json
        result = json.loads(resp.content.decode())["result"]
        self.assertEqual(len(result), expected_count)

    def get_update_course_url(self, course_identifier=None):
        if course_identifier is None:
            course_identifier = self.get_default_course_identifier()
        return reverse("relate-update_course", args=[course_identifier])
Dong Zhuang's avatar
Dong Zhuang committed

    def post_update_course_content(self, commit_sha,
                                   fetch_update=False,
                                   prevent_discarding_revisions=True,
                                   force_login_instructor=True,
                                   course=None,
                                   ):
        # course instead of course_identifier because we need to do
        # refresh_from_db
        course = course or self.get_default_course()
Dong Zhuang's avatar
Dong Zhuang committed

        try:
            commit_sha = commit_sha.decode()
        except Exception:
            pass

        data = {"new_sha": [commit_sha]}
Dong Zhuang's avatar
Dong Zhuang committed

        if not prevent_discarding_revisions:
            data["prevent_discarding_revisions"] = ["on"]

        if not fetch_update:
            data["update"] = ["Update"]
        else:
            data["fetch_update"] = ["Fetch and update"]

        force_login_user = None
        if force_login_instructor:
            force_login_user = self.get_default_instructor_user(course.identifier)
Dong Zhuang's avatar
Dong Zhuang committed

        with self.temporarily_switch_to_user(force_login_user):
                self.get_update_course_url(course.identifier), data)
            course.refresh_from_db()
Dong Zhuang's avatar
Dong Zhuang committed

        return response

    def get_page_data_by_page_id(
            self, page_id, course_identifier=None, flow_session_id=None):
        flow_params = self.get_flow_params(course_identifier, flow_session_id)
        return FlowPageData.objects.get(
            flow_session_id=flow_params["flow_session_id"], page_id=page_id)

    def get_page_visits(self, course_identifier=None,
                        flow_session_id=None, page_ordinal=None, page_id=None,
                        **kwargs):
        query_kwargs = {}
        if kwargs.get("answer_visit", False):
            query_kwargs.update({"answer__isnull": False})
        flow_params = self.get_flow_params(course_identifier, flow_session_id)
        query_kwargs.update({"flow_session_id": flow_params["flow_session_id"]})
        if page_ordinal is not None:
            query_kwargs.update({"page_data__page_ordinal": page_ordinal})
        elif page_id is not None:
            query_kwargs.update({"page_data__page_id": page_id})
        return FlowPageVisit.objects.filter(**query_kwargs)

    def get_last_answer_visit(self, course_identifier=None,
                              flow_session_id=None, page_ordinal=None,
                              page_id=None, assert_not_none=True):
        result_qset = self.get_page_visits(course_identifier,
                                           flow_session_id, page_ordinal, page_id,
                                           answer_visit=True).order_by('-pk')[:1]
        if result_qset:
            result = result_qset[0]
        else:
            result = None
        if assert_not_none:
            self.assertIsNotNone(result, "The query returns None")
        return result

    def download_all_submissions_url(self, flow_id, course_identifier):
        params = {"course_identifier": course_identifier,
                  "flow_id": flow_id}
        return reverse("relate-download_all_submissions", kwargs=params)

    def get_download_all_submissions(self, flow_id, course_identifier=None):
        if course_identifier is None:
            course_identifier = self.get_default_course_identifier()

        return self.c.get(
            self.download_all_submissions_url(flow_id, course_identifier))

    def post_download_all_submissions_by_group_page_id(
            self, group_page_id, flow_id, course_identifier=None, **kwargs):
        """
        :param group_page_id: format: group_id/page_id
        :param flow_id:
        :param course_identifier:
        :param kwargs: for updating the default post_data
        :return: response
        """
        if course_identifier is None:
            course_identifier = self.get_default_course_identifier()

        data = {'restrict_to_rules_tag': '<<<ALL>>>',
                'which_attempt': 'last',
                'extra_file': '', 'download': 'Download',
                'page_id': group_page_id,
                'non_in_progress_only': 'on'}

        data.update(kwargs)

        return self.c.post(
            self.download_all_submissions_url(flow_id, course_identifier),
            data=data
        )

    def get_flow_page_analytics(self, flow_id, group_id, page_id,
                                course_identifier=None):
        if course_identifier is None:
            course_identifier = self.get_default_course_identifier()

        params = {"course_identifier": course_identifier,
                  "flow_id": flow_id,
                  "group_id": group_id,
                  "page_id": page_id}

        return self.c.get(reverse("relate-page_analytics", kwargs=params))


class SingleCourseTestMixin(CoursesTestMixinBase):
    courses_setup_list = SINGLE_COURSE_SETUP_LIST

    @classmethod
    def setUpTestData(cls):  # noqa
        super(SingleCourseTestMixin, cls).setUpTestData()
        assert len(cls.course_qset) == 1
        cls.course = cls.course_qset.first()
        cls.instructor_participation = Participation.objects.filter(
            course=cls.course,
            roles__identifier="instructor",
            status=participation_status.active
        ).first()
        assert cls.instructor_participation

        cls.student_participation = Participation.objects.filter(
            course=cls.course,
            roles__identifier="student",
            status=participation_status.active
        ).first()
        assert cls.student_participation

        cls.ta_participation = Participation.objects.filter(
            course=cls.course,
            roles__identifier="ta",
            status=participation_status.active
        ).first()
        assert cls.ta_participation
        cls.c.logout()
        cls.course_page_url = cls.get_course_page_url()

    def setUp(self):  # noqa
        super(SingleCourseTestMixin, self).setUp()

        # reload objects created during setUpTestData in case they were modified in
        # tests. Ref: https://goo.gl/AuzJRC#django.test.TestCase.setUpTestData
        self.course.refresh_from_db()
        self.instructor_participation.refresh_from_db()
        self.student_participation.refresh_from_db()
        self.ta_participation.refresh_from_db()

    @classmethod
    def get_default_course(cls):
        return cls.course

    @classmethod
    def get_default_course_identifier(cls):
        return cls.get_default_course().identifier
    def copy_course_dict_and_set_attrs_for_post(self, attrs_dict={}):
        from course.models import Course
        kwargs = Course.objects.first().__dict__
        kwargs.update(attrs_dict)

        import six
        for k, v in six.iteritems(kwargs):
            if v is None:
                kwargs[k] = ""
        return kwargs


class TwoCourseTestMixin(CoursesTestMixinBase):
    courses_setup_list = []

    @classmethod
    def setUpTestData(cls):  # noqa
        super(TwoCourseTestMixin, cls).setUpTestData()
        assert len(cls.course_qset) == 2, (
            "'courses_setup_list' should contain two courses")
        cls.course1 = cls.course_qset.first()
        cls.course1_instructor_participation = Participation.objects.filter(
            course=cls.course1,
            roles__identifier="instructor",
            status=participation_status.active
        ).first()
        assert cls.course1_instructor_participation

        cls.course1_student_participation = Participation.objects.filter(
            course=cls.course1,
            roles__identifier="student",
            status=participation_status.active
        ).first()
        assert cls.course1_student_participation

        cls.course1_ta_participation = Participation.objects.filter(
            course=cls.course1,
            roles__identifier="ta",
            status=participation_status.active
        ).first()
        assert cls.course1_ta_participation
        cls.course1_page_url = cls.get_course_page_url(cls.course1.identifier)

        cls.course2 = cls.course_qset.last()
        cls.course2_instructor_participation = Participation.objects.filter(
            course=cls.course2,
            roles__identifier="instructor",
            status=participation_status.active
        ).first()
        assert cls.course2_instructor_participation

        cls.course2_student_participation = Participation.objects.filter(
            course=cls.course2,
            roles__identifier="student",
            status=participation_status.active
        ).first()
        assert cls.course2_student_participation

        cls.course2_ta_participation = Participation.objects.filter(
            course=cls.course2,
            roles__identifier="ta",
            status=participation_status.active
        ).first()
        assert cls.course2_ta_participation
        cls.course2_page_url = cls.get_course_page_url(cls.course2.identifier)

        cls.c.logout()

    def setUp(self):  # noqa
        super(TwoCourseTestMixin, self).setUp()
        # reload objects created during setUpTestData in case they were modified in
        # tests. Ref: https://goo.gl/AuzJRC#django.test.TestCase.setUpTestData
        self.course1.refresh_from_db()
        self.course1_instructor_participation.refresh_from_db()
        self.course1_student_participation.refresh_from_db()
        self.course1_ta_participation.refresh_from_db()

        self.course2.refresh_from_db()
        self.course2_instructor_participation.refresh_from_db()
        self.course2_student_participation.refresh_from_db()
        self.course2_ta_participation.refresh_from_db()


class SingleCoursePageTestMixin(SingleCourseTestMixin):
    # This serves as cache
    _default_session_id = None

    @property
    def flow_id(self):
        raise NotImplementedError

    @classmethod
    def update_default_flow_session_id(cls, course_identifier):
        cls._default_session_id = cls.default_flow_params["flow_session_id"]

    def get_default_flow_session_id(self, course_identifier):
        if self._default_session_id is not None:
            return self._default_session_id
        self._default_session_id = self.get_latest_session_id(course_identifier)
        return self._default_session_id


class TwoCoursePageTestMixin(TwoCourseTestMixin):
    _course1_default_session_id = None
    _course2_default_session_id = None

    @property
    def flow_id(self):
        raise NotImplementedError

    def get_default_flow_session_id(self, course_identifier):
        if course_identifier == self.course1.identifier:
            if self._course1_default_session_id is not None:
                return self._course1_default_session_id
            self._course1_default_session_id = (
                self.get_last_session_id(course_identifier))
            return self._course1_default_session_id
        if course_identifier == self.course2.identifier:
            if self._course2_default_session_id is not None:
                return self._course2_default_session_id
            self._course2_default_session_id = (
                self.get_last_session_id(course_identifier))
            return self._course2_default_session_id

    @classmethod
    def update_default_flow_session_id(cls, course_identifier):
        new_session_id = cls.default_flow_params["flow_session_id"]
        if course_identifier == cls.course1.identifier:
            cls._course1_default_session_id = new_session_id
        elif course_identifier == cls.course2.identifier:
            cls._course2_default_session_id = new_session_id


class FallBackStorageMessageTestMixin(object):
    # In case other message storage are used, the following is the default
    # storage used by django and RELATE. Tests which concerns the message
    # should not include this mixin.
    storage = 'django.contrib.messages.storage.fallback.FallbackStorage'

    def setUp(self):  # noqa
Dong Zhuang's avatar
Dong Zhuang committed
        super(FallBackStorageMessageTestMixin, self).setUp()
        self.msg_settings_override = override_settings(MESSAGE_STORAGE=self.storage)
        self.msg_settings_override.enable()
        self.addCleanup(self.msg_settings_override.disable)

    def get_listed_storage_from_response(self, response):
Dong Zhuang's avatar
Dong Zhuang committed
        return list(self.get_response_context_value_by_name(response, 'messages'))

    def clear_message_response_storage(self, response):
        # this should only be used for debug, because we are using private method
        # which might change
Dong Zhuang's avatar
Dong Zhuang committed
        try:
            storage = self.get_response_context_value_by_name(response, 'messages')
        except AssertionError:
            # message doesn't exist in response context
            return
        if hasattr(storage, '_loaded_data'):
            storage._loaded_data = []
        elif hasattr(storage, '_loaded_message'):
            storage._loaded_messages = []

        if hasattr(storage, '_queued_messages'):
            storage._queued_messages = []

        self.assertEqual(len(storage), 0)

    def assertResponseMessagesCount(self, response, expected_count):  # noqa
        storage = self.get_listed_storage_from_response(response)
        self.assertEqual(len(storage), expected_count)

    def assertResponseMessagesEqual(self, response, expected_messages):  # noqa
        storage = self.get_listed_storage_from_response(response)
Dong Zhuang's avatar
Dong Zhuang committed
        if not isinstance(expected_messages, list):
            expected_messages = [expected_messages]
        self.assertEqual(len([m for m in storage]), len(expected_messages))
        self.assertEqual([m.message for m in storage], expected_messages)

Dong Zhuang's avatar
Dong Zhuang committed
    def assertResponseMessagesEqualRegex(self, response, expected_message_regexs):  # noqa
        storage = self.get_listed_storage_from_response(response)
        if not isinstance(expected_message_regexs, list):
            expected_message_regexs = [expected_message_regexs]
        self.assertEqual(len([m for m in storage]), len(expected_message_regexs))
        messages = [m.message for m in storage]
        for idx, m in enumerate(messages):
            six.assertRegex(self, m, expected_message_regexs[idx])

Dong Zhuang's avatar
Dong Zhuang committed
    def assertResponseMessagesContains(self, response, expected_messages):  # noqa
        storage = self.get_listed_storage_from_response(response)
        if isinstance(expected_messages, str):
            expected_messages = [expected_messages]
        messages = [m.message for m in storage]
        for em in expected_messages:
            self.assertIn(em, messages)

    def assertResponseMessageLevelsEqual(self, response, expected_levels):  # noqa
        storage = self.get_listed_storage_from_response(response)
        self.assertEqual([m.level for m in storage], expected_levels)

    def debug_print_response_messages(self, response):
        """
        For debugging :class:`django.contrib.messages` objects in post response
        :param response: response
        """
        try:
            storage = self.get_listed_storage_from_response(response)
            print("\n-----------message start (%i total)-------------"
                  % len(storage))
            for m in storage:
                print(m.message)
            print("-----------message end-------------\n")
        except KeyError:
            print("\n-------no message----------")
Dong Zhuang's avatar
Dong Zhuang committed


class SubprocessRunpyContainerMixin(object):
Dong Zhuang's avatar
Dong Zhuang committed
    """
    This mixin is used to fake a runpy container, only needed when
    the TestCase include test(s) for code questions
    """
    @classmethod
    def setUpClass(cls):  # noqa
        if six.PY2:
            from unittest import SkipTest
            raise SkipTest("In process fake container is configured for "
                           "PY3 only, since currently runpy docker only "
                           "provide PY3 envrionment")

        super(SubprocessRunpyContainerMixin, cls).setUpClass()
Dong Zhuang's avatar
Dong Zhuang committed

        python_executable = os.getenv("PY_EXE")

        if not python_executable:
            python_executable = sys.executable

        import subprocess
        args = [python_executable,
                os.path.abspath(
                    os.path.join(
                        os.path.dirname(__file__), os.pardir,
                        "docker-image-run-py", "runpy")),
                ]
        cls.faked_container_process = subprocess.Popen(
            args,
            stdout=subprocess.DEVNULL,

            # because runpy prints to stderr
            stderr=subprocess.DEVNULL
        )

    def setUp(self):
        super(SubprocessRunpyContainerMixin, self).setUp()
        self.faked_container_patch = mock.patch(
            "course.page.code.SPAWN_CONTAINERS_FOR_RUNPY", False)
        self.faked_container_patch.start()
        self.addCleanup(self.faked_container_patch.stop)
Dong Zhuang's avatar
Dong Zhuang committed

    @classmethod
    def tearDownClass(cls):  # noqa
        super(SubprocessRunpyContainerMixin, cls).tearDownClass()

        from course.page.code import SPAWN_CONTAINERS_FOR_RUNPY
        # Make sure SPAWN_CONTAINERS_FOR_RUNPY is reset to True
        assert SPAWN_CONTAINERS_FOR_RUNPY
        if sys.platform.startswith("win"):
            # Without these lines, tests on Appveyor hanged when all tests
            # finished.
            # However, On nix platforms, these lines resulted in test
            # failure when there were more than one TestCases which were using
            # this mixin. So we don't kill the subprocess, and it won't bring
            # bad side effects to remainder tests.
            cls.faked_container_process.kill()


def improperly_configured_cache_patch():
    # can be used as context manager or decorator
    if six.PY3:
        built_in_import_path = "builtins.__import__"
        import builtins  # noqa
    else:
        built_in_import_path = "__builtin__.__import__"
        import __builtin__ as builtins  # noqa
    built_in_import = builtins.__import__

    def my_disable_cache_import(name, globals=None, locals=None, fromlist=(),
                                level=0):
        if name == "django.core.cache":
            raise ImproperlyConfigured()
        return built_in_import(name, globals, locals, fromlist, level)

    return mock.patch(built_in_import_path, side_effect=my_disable_cache_import)

# {{{ admin

ADMIN_TWO_COURSE_SETUP_LIST = deepcopy(TWO_COURSE_SETUP_LIST)
# switch roles
ADMIN_TWO_COURSE_SETUP_LIST[1]["participations"][0]["role_identifier"] = "ta"
ADMIN_TWO_COURSE_SETUP_LIST[1]["participations"][1]["role_identifier"] = "instructor"  # noqa


class AdminTestMixin(TwoCourseTestMixin):
    courses_setup_list = ADMIN_TWO_COURSE_SETUP_LIST
    none_participation_user_create_kwarg_list = (
        NONE_PARTICIPATION_USER_CREATE_KWARG_LIST)

    @classmethod
    def setUpTestData(cls):  # noqa
        super(AdminTestMixin, cls).setUpTestData()  # noqa

        # create 2 participation (with new user) for course1
        from tests.factories import ParticipationFactory

        cls.course1_student_participation2 = (
            ParticipationFactory.create(course=cls.course1))
        cls.course1_student_participation3 = (
            ParticipationFactory.create(course=cls.course1))
        cls.instructor1 = cls.course1_instructor_participation.user
        cls.instructor2 = cls.course2_instructor_participation.user
        assert cls.instructor1 != cls.instructor2

        # grant all admin permissions to instructors
        from django.contrib.auth.models import Permission

        for user in [cls.instructor1, cls.instructor2]:
            user.is_staff = True
            user.save()
            for perm in Permission.objects.all():
                user.user_permissions.add(perm)

    @classmethod
    def get_admin_change_list_view_url(cls, app_name, model_name):
        return reverse("admin:%s_%s_changelist" % (app_name, model_name))

    @classmethod
    def get_admin_change_view_url(cls, app_name, model_name, args=None):
        if args is None:
            args = []
        return reverse("admin:%s_%s_change" % (app_name, model_name), args=args)

    def get_admin_form_fields(self, response):
        """
        Return a list of AdminFields for the AdminForm in the response.
        """
        admin_form = response.context['adminform']
        fieldsets = list(admin_form)

        field_lines = []
        for fieldset in fieldsets:
            field_lines += list(fieldset)

        fields = []
        for field_line in field_lines:
            fields += list(field_line)

        return fields

    def get_admin_form_fields_names(self, response):
        return [f.field.name for f in self.get_admin_form_fields(response)]

    def get_changelist(self, request, model, modeladmin):
        from django.contrib.admin.views.main import ChangeList
        return ChangeList(
            request, model, modeladmin.list_display,
            modeladmin.list_display_links, modeladmin.list_filter,
            modeladmin.date_hierarchy, modeladmin.search_fields,
            modeladmin.list_select_related, modeladmin.list_per_page,
            modeladmin.list_max_show_all, modeladmin.list_editable, modeladmin,
        )

    def get_filterspec_list(self, request, changelist=None, model=None,
                            modeladmin=None):
        if changelist is None:
            assert request and model and modeladmin
            changelist = self.get_changelist(request, model, modeladmin)

        filterspecs = changelist.get_filters(request)[0]
        filterspec_list = []
        for filterspec in filterspecs:
            choices = tuple(c['display'] for c in filterspec.choices(changelist))
            filterspec_list.append(choices)

        return filterspec_list

# }}}