Merge pull request #15416 from edx/robrap/LEARNER-1604-waffle-default
LEARNER-1604: Change default and refactor old unified_course_view flag.
This commit is contained in:
@@ -7,23 +7,19 @@ import functools
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
|
||||
from mock import patch
|
||||
|
||||
from courseware.field_overrides import OverrideFieldData # pylint: disable=import-error
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from courseware.field_overrides import OverrideFieldData # pylint: disable=import-error
|
||||
from mock import patch
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationMixin, CacheIsolationTestCase, FilteredQueryCountMixin
|
||||
from openedx.core.lib.tempdir import mkdtemp_clean
|
||||
|
||||
from xmodule.contentstore.django import _CONTENTSTORE
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore, clear_existing_modulestores, SignalHandler
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
|
||||
from xmodule.modulestore.django import SignalHandler, clear_existing_modulestores, modulestore
|
||||
from xmodule.modulestore.tests.factories import XMODULE_FACTORY_LOCK
|
||||
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationMixin, CacheIsolationTestCase
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
|
||||
|
||||
class StoreConstructors(object):
|
||||
@@ -312,7 +308,7 @@ class ModuleStoreIsolationMixin(CacheIsolationMixin, SignalIsolationMixin):
|
||||
cls.enable_all_signals()
|
||||
|
||||
|
||||
class SharedModuleStoreTestCase(ModuleStoreIsolationMixin, CacheIsolationTestCase):
|
||||
class SharedModuleStoreTestCase(FilteredQueryCountMixin, ModuleStoreIsolationMixin, CacheIsolationTestCase):
|
||||
"""
|
||||
Subclass for any test case that uses a ModuleStore that can be shared
|
||||
between individual tests. This class ensures that the ModuleStore is cleaned
|
||||
@@ -395,7 +391,7 @@ class SharedModuleStoreTestCase(ModuleStoreIsolationMixin, CacheIsolationTestCas
|
||||
super(SharedModuleStoreTestCase, self).setUp()
|
||||
|
||||
|
||||
class ModuleStoreTestCase(ModuleStoreIsolationMixin, TestCase):
|
||||
class ModuleStoreTestCase(FilteredQueryCountMixin, ModuleStoreIsolationMixin, TestCase):
|
||||
"""
|
||||
Subclass for any test case that uses a ModuleStore.
|
||||
Ensures that the ModuleStore is cleaned before/after each test.
|
||||
|
||||
@@ -93,13 +93,10 @@ class CourseTab(object):
|
||||
@property
|
||||
def link_func(self):
|
||||
"""
|
||||
Returns a function that will determine a course URL for this tab.
|
||||
|
||||
The returned function takes two arguments:
|
||||
course (Course) - the course in question.
|
||||
view_name (str) - the name of the view.
|
||||
Returns a function that takes a course and reverse function and will
|
||||
compute the course URL for this tab.
|
||||
"""
|
||||
return self.tab_dict.get('link_func', link_reverse_func(self.view_name))
|
||||
return self.tab_dict.get('link_func', course_reverse_func(self.view_name))
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None):
|
||||
@@ -570,14 +567,46 @@ def key_checker(expected_keys):
|
||||
return check
|
||||
|
||||
|
||||
def link_reverse_func(reverse_name):
|
||||
def course_reverse_func(reverse_name):
|
||||
"""
|
||||
Returns a function that takes in a course and reverse_url_func,
|
||||
and calls the reverse_url_func with the given reverse_name and course's ID.
|
||||
Returns a function that will determine a course URL for the provided
|
||||
reverse_name.
|
||||
|
||||
This is used to generate the url for a CourseTab without having access to Django's reverse function.
|
||||
See documentation for course_reverse_func_from_name_func. This function
|
||||
simply calls course_reverse_func_from_name_func after wrapping reverse_name
|
||||
in a function.
|
||||
"""
|
||||
return lambda course, reverse_url_func: reverse_url_func(reverse_name, args=[course.id.to_deprecated_string()])
|
||||
return course_reverse_func_from_name_func(lambda course: reverse_name)
|
||||
|
||||
|
||||
def course_reverse_func_from_name_func(reverse_name_func):
|
||||
"""
|
||||
Returns a function that will determine a course URL for the provided
|
||||
reverse_name_func.
|
||||
|
||||
Use this when the calculation of the reverse_name is dependent on the
|
||||
course. Otherwise, use the simpler course_reverse_func.
|
||||
|
||||
This can be used to generate the url for a CourseTab without having
|
||||
immediate access to Django's reverse function.
|
||||
|
||||
Arguments:
|
||||
reverse_name_func (function): A function that takes a single argument
|
||||
(Course) and returns the name to be used with the reverse function.
|
||||
|
||||
Returns:
|
||||
A function that takes in two arguments:
|
||||
course (Course): the course in question.
|
||||
reverse_url_func (function): a reverse function for a course URL
|
||||
that uses the course ID in the url.
|
||||
When called, the returned function will return the course URL as
|
||||
determined by calling reverse_url_func with the reverse_name and the
|
||||
course's ID.
|
||||
"""
|
||||
return lambda course, reverse_url_func: reverse_url_func(
|
||||
reverse_name_func(course),
|
||||
args=[course.id.to_deprecated_string()]
|
||||
)
|
||||
|
||||
|
||||
def need_name(dictionary, raise_error=True):
|
||||
|
||||
@@ -30,7 +30,7 @@ class CourseHomePage(CoursePage):
|
||||
self.outline = CourseOutlinePage(browser, self)
|
||||
self.preview = StaffPreviewPage(browser, self)
|
||||
# TODO: TNL-6546: Remove the following
|
||||
self.unified_course_view = False
|
||||
self.course_outline_page = False
|
||||
|
||||
def click_bookmarks_button(self):
|
||||
""" Click on Bookmarks button """
|
||||
@@ -225,9 +225,9 @@ class CourseOutlinePage(PageObject):
|
||||
courseware_page = CoursewarePage(self.browser, self.parent_page.course_id)
|
||||
courseware_page.wait_for_page()
|
||||
|
||||
# TODO: TNL-6546: Remove this if/visit_unified_course_view
|
||||
if self.parent_page.unified_course_view:
|
||||
courseware_page.nav.visit_unified_course_view()
|
||||
# TODO: TNL-6546: Remove this if/visit_course_outline_page
|
||||
if self.parent_page.course_outline_page:
|
||||
courseware_page.nav.visit_course_outline_page()
|
||||
|
||||
self.wait_for(
|
||||
promise_check_func=lambda: courseware_page.nav.is_on_section(section_title, subsection_title),
|
||||
|
||||
@@ -355,7 +355,7 @@ class CourseNavPage(PageObject):
|
||||
super(CourseNavPage, self).__init__(browser)
|
||||
self.parent_page = parent_page
|
||||
# TODO: TNL-6546: Remove the following
|
||||
self.unified_course_view = False
|
||||
self.course_outline_page = False
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.parent_page.is_browser_on_page
|
||||
@@ -579,11 +579,11 @@ class CourseNavPage(PageObject):
|
||||
"""
|
||||
return self.q(css='.chapter-content-container .menu-item.active a').attrs('href')[0]
|
||||
|
||||
# TODO: TNL-6546: Remove all references to self.unified_course_view
|
||||
# TODO: TNL-6546: Remove all references to self.course_outline_page
|
||||
# TODO: TNL-6546: Remove the following function
|
||||
def visit_unified_course_view(self):
|
||||
# use unified_course_view version of the nav
|
||||
self.unified_course_view = True
|
||||
# reload the same page with the unified course view
|
||||
self.browser.get(self.browser.current_url + "&unified_course_view=1")
|
||||
def visit_course_outline_page(self):
|
||||
# use course_outline_page version of the nav
|
||||
self.course_outline_page = True
|
||||
# reload the same page with the course_outline_page flag
|
||||
self.browser.get(self.browser.current_url + "&course_experience.course_outline_page=1")
|
||||
self.wait_for_page()
|
||||
|
||||
@@ -790,9 +790,9 @@ class HighLevelTabTest(UniqueCourseTest):
|
||||
#self.tab_nav.go_to_tab('Course')
|
||||
self.course_home_page.visit()
|
||||
|
||||
# TODO: TNL-6546: Remove unified_course_view.
|
||||
self.course_home_page.unified_course_view = True
|
||||
self.courseware_page.nav.unified_course_view = True
|
||||
# TODO: TNL-6546: Remove course_outline_page.
|
||||
self.course_home_page.course_outline_page = True
|
||||
self.courseware_page.nav.course_outline_page = True
|
||||
|
||||
# Check that the tab lands on the course home page.
|
||||
self.assertTrue(self.course_home_page.is_browser_on_page())
|
||||
|
||||
@@ -68,9 +68,9 @@ class CourseHomeTest(CourseHomeBaseTest):
|
||||
"""
|
||||
self.course_home_page.visit()
|
||||
|
||||
# TODO: TNL-6546: Remove unified_course_view.
|
||||
self.course_home_page.unified_course_view = True
|
||||
self.courseware_page.nav.unified_course_view = True
|
||||
# TODO: TNL-6546: Remove course_outline_page.
|
||||
self.course_home_page.course_outline_page = True
|
||||
self.courseware_page.nav.course_outline_page = True
|
||||
|
||||
# Check that the tab lands on the course home page.
|
||||
self.assertTrue(self.course_home_page.is_browser_on_page())
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,24 +8,24 @@ from datetime import datetime
|
||||
import ddt
|
||||
import mock
|
||||
from ccx_keys.locator import CCXLocator
|
||||
from courseware.field_overrides import OverrideFieldData
|
||||
from courseware.testutils import FieldOverrideTestMixin
|
||||
from courseware.views.views import progress
|
||||
from django.conf import settings
|
||||
from django.core.cache import caches
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from lms.djangoapps.ccx.tests.factories import CcxFactory
|
||||
from nose.plugins.attrib import attr
|
||||
from nose.plugins.skip import SkipTest
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from pytz import UTC
|
||||
from xblock.core import XBlock
|
||||
|
||||
from courseware.field_overrides import OverrideFieldData
|
||||
from courseware.testutils import FieldOverrideTestMixin
|
||||
from courseware.views.views import progress
|
||||
from lms.djangoapps.ccx.tests.factories import CcxFactory
|
||||
from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
||||
from pytz import UTC
|
||||
from request_cache.middleware import RequestCache
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xblock.core import XBlock
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
TEST_DATA_MONGO_MODULESTORE,
|
||||
TEST_DATA_SPLIT_MODULESTORE,
|
||||
@@ -34,6 +34,8 @@ from xmodule.modulestore.tests.django_utils import (
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls, check_sum_of_calls
|
||||
from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin
|
||||
|
||||
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
|
||||
|
||||
|
||||
@attr(shard=3)
|
||||
@mock.patch.dict(
|
||||
@@ -181,7 +183,7 @@ class FieldOverridePerformanceTestCase(FieldOverrideTestMixin, ProceduralCourseT
|
||||
# can actually take affect.
|
||||
OverrideFieldData.provider_classes = None
|
||||
|
||||
with self.assertNumQueries(sql_queries, using='default'):
|
||||
with self.assertNumQueries(sql_queries, using='default', table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with self.assertNumQueries(0, using='student_module_history'):
|
||||
with self.assertMongoCallCount(mongo_reads):
|
||||
with self.assertXBlockInstantiations(1):
|
||||
@@ -235,18 +237,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
|
||||
# # of sql queries to default,
|
||||
# # of mongo queries,
|
||||
# )
|
||||
('no_overrides', 1, True, False): (27, 1),
|
||||
('no_overrides', 2, True, False): (27, 1),
|
||||
('no_overrides', 3, True, False): (27, 1),
|
||||
('ccx', 1, True, False): (27, 1),
|
||||
('ccx', 2, True, False): (27, 1),
|
||||
('ccx', 3, True, False): (27, 1),
|
||||
('no_overrides', 1, False, False): (27, 1),
|
||||
('no_overrides', 2, False, False): (27, 1),
|
||||
('no_overrides', 3, False, False): (27, 1),
|
||||
('ccx', 1, False, False): (27, 1),
|
||||
('ccx', 2, False, False): (27, 1),
|
||||
('ccx', 3, False, False): (27, 1),
|
||||
('no_overrides', 1, True, False): (23, 1),
|
||||
('no_overrides', 2, True, False): (23, 1),
|
||||
('no_overrides', 3, True, False): (23, 1),
|
||||
('ccx', 1, True, False): (23, 1),
|
||||
('ccx', 2, True, False): (23, 1),
|
||||
('ccx', 3, True, False): (23, 1),
|
||||
('no_overrides', 1, False, False): (23, 1),
|
||||
('no_overrides', 2, False, False): (23, 1),
|
||||
('no_overrides', 3, False, False): (23, 1),
|
||||
('ccx', 1, False, False): (23, 1),
|
||||
('ccx', 2, False, False): (23, 1),
|
||||
('ccx', 3, False, False): (23, 1),
|
||||
}
|
||||
|
||||
|
||||
@@ -258,19 +260,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
|
||||
__test__ = True
|
||||
|
||||
TEST_DATA = {
|
||||
('no_overrides', 1, True, False): (27, 3),
|
||||
('no_overrides', 2, True, False): (27, 3),
|
||||
('no_overrides', 3, True, False): (27, 3),
|
||||
('ccx', 1, True, False): (27, 3),
|
||||
('ccx', 2, True, False): (27, 3),
|
||||
('ccx', 3, True, False): (27, 3),
|
||||
('ccx', 1, True, True): (28, 3),
|
||||
('ccx', 2, True, True): (28, 3),
|
||||
('ccx', 3, True, True): (28, 3),
|
||||
('no_overrides', 1, False, False): (27, 3),
|
||||
('no_overrides', 2, False, False): (27, 3),
|
||||
('no_overrides', 3, False, False): (27, 3),
|
||||
('ccx', 1, False, False): (27, 3),
|
||||
('ccx', 2, False, False): (27, 3),
|
||||
('ccx', 3, False, False): (27, 3),
|
||||
('no_overrides', 1, True, False): (23, 3),
|
||||
('no_overrides', 2, True, False): (23, 3),
|
||||
('no_overrides', 3, True, False): (23, 3),
|
||||
('ccx', 1, True, False): (23, 3),
|
||||
('ccx', 2, True, False): (23, 3),
|
||||
('ccx', 3, True, False): (23, 3),
|
||||
('ccx', 1, True, True): (24, 3),
|
||||
('ccx', 2, True, True): (24, 3),
|
||||
('ccx', 3, True, True): (24, 3),
|
||||
('no_overrides', 1, False, False): (23, 3),
|
||||
('no_overrides', 2, False, False): (23, 3),
|
||||
('no_overrides', 3, False, False): (23, 3),
|
||||
('ccx', 1, False, False): (23, 3),
|
||||
('ccx', 2, False, False): (23, 3),
|
||||
('ccx', 3, False, False): (23, 3),
|
||||
}
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
This module is essentially a broker to xmodule/tabs.py -- it was originally introduced to
|
||||
perform some LMS-specific tab display gymnastics for the Entrance Exams feature
|
||||
"""
|
||||
from courseware.access import has_access
|
||||
from courseware.entrance_exams import user_can_skip_entrance_exam
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_noop
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.entrance_exams import user_can_skip_entrance_exam
|
||||
from openedx.core.lib.course_tabs import CourseTabPluginManager
|
||||
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, default_course_url_name
|
||||
from request_cache.middleware import RequestCache
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.tabs import CourseTab, CourseTabList, key_checker, link_reverse_func
|
||||
from xmodule.tabs import CourseTab, CourseTabList, course_reverse_func_from_name_func, key_checker
|
||||
|
||||
|
||||
class EnrolledTab(CourseTab):
|
||||
@@ -41,11 +39,11 @@ class CoursewareTab(EnrolledTab):
|
||||
@property
|
||||
def link_func(self):
|
||||
"""
|
||||
Returns a function that computes the URL for this tab.
|
||||
Returns a function that takes a course and reverse function and will
|
||||
compute the course URL for this tab.
|
||||
"""
|
||||
request = RequestCache.get_current_request()
|
||||
url_name = default_course_url_name(request)
|
||||
return link_reverse_func(url_name)
|
||||
reverse_name_func = lambda course: default_course_url_name(course.id)
|
||||
return course_reverse_func_from_name_func(reverse_name_func)
|
||||
|
||||
|
||||
class CourseInfoTab(CourseTab):
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
"""
|
||||
Tests use cases related to LMS Entrance Exam behavior, such as gated content access (TOC)
|
||||
"""
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.client import RequestFactory
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from mock import Mock, patch
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from courseware.entrance_exams import (
|
||||
course_has_entrance_exam,
|
||||
@@ -18,7 +12,14 @@ from courseware.model_data import FieldDataCache
|
||||
from courseware.module_render import get_module, handle_xblock_callback, toc_for_course
|
||||
from courseware.tests.factories import InstructorFactory, StaffFactory, UserFactory
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.client import RequestFactory
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from mock import Mock, patch
|
||||
from nose.plugins.attrib import attr
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import AnonymousUserFactory, CourseEnrollmentFactory
|
||||
from util.milestones_helpers import (
|
||||
@@ -353,6 +354,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
|
||||
self.assertNotIn('To access course materials, you must score', resp.content)
|
||||
self.assertNotIn('You have passed the entrance exam.', resp.content)
|
||||
|
||||
# TODO: LEARNER-71: Do we need to adjust or remove this test?
|
||||
@override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
|
||||
def test_entrance_exam_passed_message_and_course_content(self):
|
||||
"""
|
||||
Unit Test: exam passing message and rest of the course section should be present
|
||||
|
||||
@@ -3,14 +3,15 @@ This test file will run through some LMS test scenarios regarding access and nav
|
||||
"""
|
||||
import time
|
||||
|
||||
from courseware.tests.factories import GlobalStaffFactory
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
from mock import patch
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from courseware.tests.factories import GlobalStaffFactory
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
@@ -95,6 +96,8 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
raise AssertionError("assertTabInactive failed: " + tabname + " active")
|
||||
return
|
||||
|
||||
# TODO: LEARNER-71: Do we need to adjust or remove this test?
|
||||
@override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
|
||||
def test_chrome_settings(self):
|
||||
'''
|
||||
Test settings for disabling and modifying navigation chrome in the courseware:
|
||||
@@ -223,6 +226,8 @@ class TestNavigation(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
resp = self.client.get(url)
|
||||
self.assertRedirects(resp, section_url)
|
||||
|
||||
# TODO: LEARNER-71: Do we need to adjust or remove this test?
|
||||
@override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
|
||||
def test_incomplete_course(self):
|
||||
email = self.staff_user.email
|
||||
password = "test"
|
||||
|
||||
@@ -10,26 +10,8 @@ from HTMLParser import HTMLParser
|
||||
from urllib import quote, urlencode
|
||||
from uuid import uuid4
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404, HttpResponseBadRequest
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client, RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from freezegun import freeze_time
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from mock import MagicMock, PropertyMock, create_autospec, patch
|
||||
from nose.plugins.attrib import attr
|
||||
from opaque_keys.edx.locations import Location, SlashSeparatedCourseKey
|
||||
from pytz import UTC
|
||||
from waffle.testutils import override_flag
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Scope, String
|
||||
from xblock.fragment import Fragment
|
||||
|
||||
import courseware.views.views as views
|
||||
import ddt
|
||||
import shoppingcart
|
||||
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from certificates import api as certs_api
|
||||
@@ -45,9 +27,21 @@ from courseware.tests.factories import GlobalStaffFactory, StudentModuleFactory
|
||||
from courseware.testutils import RenderXBlockTestMixin
|
||||
from courseware.url_helpers import get_redirect_url
|
||||
from courseware.user_state_client import DjangoXBlockUserStateClient
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404, HttpResponseBadRequest
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client, RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from freezegun import freeze_time
|
||||
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
|
||||
from lms.djangoapps.grades.config.waffle import waffle as grades_waffle
|
||||
from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from mock import MagicMock, PropertyMock, create_autospec, patch
|
||||
from nose.plugins.attrib import attr
|
||||
from opaque_keys.edx.locations import Location, SlashSeparatedCourseKey
|
||||
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory
|
||||
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory, ProgramFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
@@ -55,14 +49,20 @@ from openedx.core.djangoapps.crawlers.models import CrawlersConfig
|
||||
from openedx.core.djangoapps.credit.api import set_credit_requirements
|
||||
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
from openedx.core.lib.gating import api as gating_api
|
||||
from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG
|
||||
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
|
||||
from pytz import UTC
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory
|
||||
from util.tests.test_date_utils import fake_pgettext, fake_ugettext
|
||||
from util.url import reload_django_url_config
|
||||
from util.views import ensure_valid_course_key
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import Scope, String
|
||||
from xblock.fragment import Fragment
|
||||
from xmodule.graders import ShowCorrectness
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -73,6 +73,8 @@ from xmodule.modulestore.tests.django_utils import (
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
|
||||
|
||||
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class TestJumpTo(ModuleStoreTestCase):
|
||||
@@ -209,8 +211,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
|
||||
NUM_PROBLEMS = 20
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.mongo, 10, 150),
|
||||
(ModuleStoreEnum.Type.split, 4, 150),
|
||||
(ModuleStoreEnum.Type.mongo, 10, 143),
|
||||
(ModuleStoreEnum.Type.split, 4, 143),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
|
||||
@@ -228,7 +230,7 @@ class IndexQueryTestCase(ModuleStoreTestCase):
|
||||
self.client.login(username=self.user.username, password=password)
|
||||
CourseEnrollment.enroll(self.user, course.id)
|
||||
|
||||
with self.assertNumQueries(expected_mysql_query_count):
|
||||
with self.assertNumQueries(expected_mysql_query_count, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(expected_mongo_query_count):
|
||||
url = reverse(
|
||||
'courseware_section',
|
||||
@@ -519,8 +521,6 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
mock_user.is_authenticated.return_value = False
|
||||
self.assertEqual(views.user_groups(mock_user), [])
|
||||
|
||||
# TODO: TNL-6546: Remove decorator for unified_course_view
|
||||
@override_flag('unified_course_view', active=True)
|
||||
def test_get_redirect_url(self):
|
||||
# test the course location
|
||||
self.assertEqual(
|
||||
@@ -964,6 +964,7 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# TODO: TNL-6387: Remove test
|
||||
@override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
|
||||
def test_accordion(self):
|
||||
"""
|
||||
This needs a response_context, which is not included in the render_accordion's main method
|
||||
@@ -1068,7 +1069,7 @@ class BaseDueDateTests(ModuleStoreTestCase):
|
||||
|
||||
self.time_with_tz = "2013-09-18 11:30:00+00:00"
|
||||
|
||||
def test_backwards_compatability(self):
|
||||
def test_backwards_compatibility(self):
|
||||
# The test course being used has show_timezone = False in the policy file
|
||||
# (and no due_date_display_format set). This is to test our backwards compatibility--
|
||||
# in course_module's init method, the date_display_format will be set accordingly to
|
||||
@@ -1116,6 +1117,7 @@ class TestProgressDueDate(BaseDueDateTests):
|
||||
return self.client.get(reverse('progress', args=[unicode(course.id)]))
|
||||
|
||||
|
||||
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
|
||||
class TestAccordionDueDate(BaseDueDateTests):
|
||||
"""
|
||||
Test that the accordion page displays due dates correctly
|
||||
@@ -1129,6 +1131,31 @@ class TestAccordionDueDate(BaseDueDateTests):
|
||||
follow=True
|
||||
)
|
||||
|
||||
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
|
||||
@override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
|
||||
def test_backwards_compatibility(self):
|
||||
super(TestAccordionDueDate, self).test_backwards_compatibility()
|
||||
|
||||
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
|
||||
@override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
|
||||
def test_defaults(self):
|
||||
super(TestAccordionDueDate, self).test_defaults()
|
||||
|
||||
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
|
||||
@override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
|
||||
def test_format_date(self):
|
||||
super(TestAccordionDueDate, self).test_format_date()
|
||||
|
||||
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
|
||||
@override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
|
||||
def test_format_invalid(self):
|
||||
super(TestAccordionDueDate, self).test_format_invalid()
|
||||
|
||||
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
|
||||
@override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
|
||||
def test_format_none(self):
|
||||
super(TestAccordionDueDate, self).test_format_none()
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class StartDateTests(ModuleStoreTestCase):
|
||||
@@ -1248,7 +1275,6 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
"""
|
||||
Tests that verify that the progress page works correctly.
|
||||
"""
|
||||
|
||||
@ddt.data('"><script>alert(1)</script>', '<script>alert(1)</script>', '</script><script>alert(1)</script>')
|
||||
def test_progress_page_xss_prevent(self, malicious_code):
|
||||
"""
|
||||
@@ -1438,23 +1464,27 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
|
||||
SelfPacedConfiguration(enabled=self_paced_enabled).save()
|
||||
self.setup_course(self_paced=self_paced)
|
||||
with self.assertNumQueries(44), check_mongo_calls(1):
|
||||
with self.assertNumQueries(40, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(1):
|
||||
self._get_progress_page()
|
||||
|
||||
@ddt.data(
|
||||
(False, 44, 30),
|
||||
(True, 37, 26)
|
||||
(False, 40, 26),
|
||||
(True, 33, 22)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_progress_queries(self, enable_waffle, initial, subsequent):
|
||||
self.setup_course()
|
||||
with grades_waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=enable_waffle):
|
||||
with self.assertNumQueries(initial), check_mongo_calls(1):
|
||||
with self.assertNumQueries(
|
||||
initial, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST
|
||||
), check_mongo_calls(1):
|
||||
self._get_progress_page()
|
||||
|
||||
# subsequent accesses to the progress page require fewer queries.
|
||||
for _ in range(2):
|
||||
with self.assertNumQueries(subsequent), check_mongo_calls(1):
|
||||
with self.assertNumQueries(
|
||||
subsequent, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST
|
||||
), check_mongo_calls(1):
|
||||
self._get_progress_page()
|
||||
|
||||
@patch(
|
||||
|
||||
@@ -31,10 +31,9 @@ from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
||||
from openedx.core.djangoapps.monitoring_utils import set_custom_metrics_for_course_key
|
||||
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
|
||||
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
|
||||
from openedx.features.course_experience import UNIFIED_COURSE_VIEW_FLAG, default_course_url_name
|
||||
from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG, default_course_url_name
|
||||
from openedx.features.course_experience.views.course_sock import CourseSockFragmentView
|
||||
from openedx.features.enterprise_support.api import data_sharing_consent_required
|
||||
from request_cache.middleware import RequestCache
|
||||
from shoppingcart.models import CourseRegistrationCode
|
||||
from student.views import is_course_blocked
|
||||
from util.views import ensure_valid_course_key
|
||||
@@ -328,7 +327,7 @@ class CoursewareIndex(View):
|
||||
Returns and creates the rendering context for the courseware.
|
||||
Also returns the table of contents for the courseware.
|
||||
"""
|
||||
course_url_name = default_course_url_name(request)
|
||||
course_url_name = default_course_url_name(self.course.id)
|
||||
course_url = reverse(course_url_name, kwargs={'course_id': unicode(self.course.id)})
|
||||
courseware_context = {
|
||||
'csrf': csrf(self.request)['csrf_token'],
|
||||
@@ -348,7 +347,7 @@ class CoursewareIndex(View):
|
||||
'disable_optimizely': not WaffleSwitchNamespace('RET').is_enabled('enable_optimizely_in_courseware'),
|
||||
'section_title': None,
|
||||
'sequence_title': None,
|
||||
'disable_accordion': waffle.flag_is_active(request, UNIFIED_COURSE_VIEW_FLAG),
|
||||
'disable_accordion': COURSE_OUTLINE_PAGE_FLAG.is_enabled(self.course.id),
|
||||
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
|
||||
'upgrade_link': check_and_get_upgrade_link(request, self.effective_user, self.course.id),
|
||||
'upgrade_price': get_cosmetic_verified_display_price(self.course),
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.utils.translation import ugettext as _
|
||||
from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled
|
||||
from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.features.course_experience import course_home_page_title, UNIFIED_COURSE_VIEW_FLAG
|
||||
from openedx.features.course_experience import course_home_page_title, COURSE_OUTLINE_PAGE_FLAG
|
||||
%>
|
||||
<%
|
||||
include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and (course.enable_proctored_exams or course.enable_timed_exams)
|
||||
@@ -159,7 +159,7 @@ ${HTML(fragment.foot_html())}
|
||||
<nav aria-label="${_('Course')}" class="sr-is-focusable" tabindex="-1">
|
||||
<div class="has-breadcrumbs">
|
||||
<div class="breadcrumbs">
|
||||
% if waffle.flag_is_active(request, UNIFIED_COURSE_VIEW_FLAG):
|
||||
% if COURSE_OUTLINE_PAGE_FLAG.is_enabled(course.id):
|
||||
<span class="nav-item nav-item-course">
|
||||
<a href="${course_url}">${course_home_page_title(course)}</a>
|
||||
</span>
|
||||
|
||||
@@ -10,8 +10,10 @@ namespace. For example:
|
||||
|
||||
WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='course_experience')
|
||||
|
||||
HIDE_SEARCH_FLAG = WaffleFlag(WAFFLE_FLAG_NAMESPACE, 'hide_search')
|
||||
# Use CourseWaffleFlag when you are in the context of a course.
|
||||
UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'unified_course_tab')
|
||||
# Use WaffleFlag when outside the context of a course.
|
||||
HIDE_SEARCH_FLAG = WaffleFlag(WAFFLE_FLAG_NAMESPACE, 'hide_search')
|
||||
|
||||
You can check these flags in code using the following:
|
||||
|
||||
@@ -43,14 +45,14 @@ To test WaffleSwitchNamespace, use the provided context managers. For example:
|
||||
...
|
||||
|
||||
"""
|
||||
import logging
|
||||
from abc import ABCMeta
|
||||
from contextlib import contextmanager
|
||||
import logging
|
||||
from waffle.testutils import override_switch as waffle_override_switch
|
||||
from waffle import flag_is_active, switch_is_active
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from request_cache import get_request, get_cache as get_request_cache
|
||||
from request_cache import get_cache as get_request_cache, get_request
|
||||
from waffle import flag_is_active, switch_is_active
|
||||
from waffle.models import Flag
|
||||
from waffle.testutils import override_switch as waffle_override_switch
|
||||
|
||||
from .models import WaffleFlagCourseOverrideModel
|
||||
|
||||
@@ -64,7 +66,6 @@ class WaffleNamespace(object):
|
||||
An instance of this class represents a single namespace
|
||||
(e.g. "course_experience"), and can be used to work with a set of
|
||||
flags or switches that will all share this namespace.
|
||||
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@@ -92,7 +93,6 @@ class WaffleNamespace(object):
|
||||
|
||||
Arguments:
|
||||
setting_name (String): The name of the flag or switch.
|
||||
|
||||
"""
|
||||
return u'{}.{}'.format(self.name, setting_name)
|
||||
|
||||
@@ -110,7 +110,6 @@ class WaffleSwitchNamespace(WaffleNamespace):
|
||||
|
||||
All namespaced switch values are stored in a single request cache containing
|
||||
all switches for all namespaces.
|
||||
|
||||
"""
|
||||
def is_enabled(self, switch_name):
|
||||
"""
|
||||
@@ -174,7 +173,6 @@ class WaffleFlagNamespace(WaffleNamespace):
|
||||
|
||||
All namespaced flag values are stored in a single request cache containing
|
||||
all flags for all namespaces.
|
||||
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@@ -185,7 +183,7 @@ class WaffleFlagNamespace(WaffleNamespace):
|
||||
"""
|
||||
return self._get_request_cache().setdefault('flags', {})
|
||||
|
||||
def is_flag_active(self, flag_name, check_before_waffle_callback=None):
|
||||
def is_flag_active(self, flag_name, check_before_waffle_callback=None, flag_undefined_default=None):
|
||||
"""
|
||||
Returns and caches whether the provided flag is active.
|
||||
|
||||
@@ -202,7 +200,8 @@ class WaffleFlagNamespace(WaffleNamespace):
|
||||
check_before_waffle_callback(namespaced_flag_name) returns True
|
||||
or False, it is cached and returned. If it returns None, then
|
||||
waffle is used.
|
||||
|
||||
flag_undefined_default (Boolean): A default value to be returned if
|
||||
the waffle flag is to be checked, but doesn't exist.
|
||||
"""
|
||||
# validate arguments
|
||||
namespaced_flag_name = self._namespaced_name(flag_name)
|
||||
@@ -213,7 +212,16 @@ class WaffleFlagNamespace(WaffleNamespace):
|
||||
value = check_before_waffle_callback(namespaced_flag_name)
|
||||
|
||||
if value is None:
|
||||
value = flag_is_active(get_request(), namespaced_flag_name)
|
||||
|
||||
if flag_undefined_default is not None:
|
||||
# determine if the flag is undefined in waffle
|
||||
try:
|
||||
Flag.objects.get(name=namespaced_flag_name)
|
||||
except Flag.DoesNotExist:
|
||||
value = flag_undefined_default
|
||||
|
||||
if value is None:
|
||||
value = flag_is_active(get_request(), namespaced_flag_name)
|
||||
|
||||
self._cached_flags[namespaced_flag_name] = value
|
||||
return value
|
||||
@@ -224,7 +232,7 @@ class WaffleFlag(object):
|
||||
Represents a single waffle flag, using a cached waffle namespace.
|
||||
"""
|
||||
|
||||
def __init__(self, waffle_namespace, flag_name):
|
||||
def __init__(self, waffle_namespace, flag_name, flag_undefined_default=None):
|
||||
"""
|
||||
Initializes the waffle flag instance.
|
||||
|
||||
@@ -232,16 +240,21 @@ class WaffleFlag(object):
|
||||
waffle_namespace (WaffleFlagNamespace): Provides a cached namespace
|
||||
for this flag.
|
||||
flag_name (String): The name of the flag (without namespacing).
|
||||
|
||||
flag_undefined_default (Boolean): A default value to be returned if
|
||||
the waffle flag is to be checked, but doesn't exist.
|
||||
"""
|
||||
self.waffle_namespace = waffle_namespace
|
||||
self.flag_name = flag_name
|
||||
self.flag_undefined_default = flag_undefined_default
|
||||
|
||||
def is_enabled(self):
|
||||
"""
|
||||
Returns whether or not the flag is enabled.
|
||||
"""
|
||||
return self.waffle_namespace.is_flag_active(self.flag_name)
|
||||
return self.waffle_namespace.is_flag_active(
|
||||
self.flag_name,
|
||||
flag_undefined_default=self.flag_undefined_default
|
||||
)
|
||||
|
||||
|
||||
class CourseWaffleFlag(WaffleFlag):
|
||||
@@ -249,7 +262,6 @@ class CourseWaffleFlag(WaffleFlag):
|
||||
Represents a single waffle flag that can be forced on/off for a course.
|
||||
|
||||
Uses a cached waffle namespace.
|
||||
|
||||
"""
|
||||
|
||||
def _get_course_override_callback(self, course_id):
|
||||
@@ -259,7 +271,6 @@ class CourseWaffleFlag(WaffleFlag):
|
||||
Arguments:
|
||||
course_id (CourseKey): The course to check for override before
|
||||
checking waffle.
|
||||
|
||||
"""
|
||||
def course_override_callback(namespaced_flag_name):
|
||||
"""
|
||||
@@ -269,7 +280,6 @@ class CourseWaffleFlag(WaffleFlag):
|
||||
Arguments:
|
||||
namespaced_flag_name (String): A namespaced version of the flag
|
||||
to check.
|
||||
|
||||
"""
|
||||
force_override = WaffleFlagCourseOverrideModel.override_value(namespaced_flag_name, course_id)
|
||||
|
||||
@@ -287,12 +297,12 @@ class CourseWaffleFlag(WaffleFlag):
|
||||
Arguments:
|
||||
course_id (CourseKey): The course to check for override before
|
||||
checking waffle.
|
||||
|
||||
"""
|
||||
# validate arguments
|
||||
assert issubclass(type(course_id), CourseKey), "The course_id '{}' must be a CourseKey.".format(str(course_id))
|
||||
|
||||
return self.waffle_namespace.is_flag_active(
|
||||
self.flag_name,
|
||||
check_before_waffle_callback=self._get_course_override_callback(course_id)
|
||||
check_before_waffle_callback=self._get_course_override_callback(course_id),
|
||||
flag_undefined_default=self.flag_undefined_default
|
||||
)
|
||||
|
||||
@@ -5,9 +5,8 @@ import ddt
|
||||
from django.test import TestCase
|
||||
from mock import patch
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
from request_cache.middleware import RequestCache
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
from .. import CourseWaffleFlag, WaffleFlagNamespace
|
||||
from ..models import WaffleFlagCourseOverrideModel
|
||||
@@ -50,3 +49,34 @@ class TestCourseWaffleFlag(TestCase):
|
||||
self.NAMESPACED_FLAG_NAME,
|
||||
self.TEST_COURSE_KEY
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
{'flag_undefined_default': None, 'result': False},
|
||||
{'flag_undefined_default': False, 'result': False},
|
||||
{'flag_undefined_default': True, 'result': True},
|
||||
)
|
||||
def test_undefined_waffle_flag(self, data):
|
||||
"""
|
||||
Test flag with various defaults provided for undefined waffle flags.
|
||||
"""
|
||||
RequestCache.clear_request_cache()
|
||||
|
||||
test_course_flag = CourseWaffleFlag(
|
||||
self.TEST_NAMESPACE,
|
||||
self.FLAG_NAME,
|
||||
flag_undefined_default=data['flag_undefined_default']
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
WaffleFlagCourseOverrideModel,
|
||||
'override_value',
|
||||
return_value=WaffleFlagCourseOverrideModel.ALL_CHOICES.unset
|
||||
):
|
||||
# check twice to test that the result is properly cached
|
||||
self.assertEqual(test_course_flag.is_enabled(self.TEST_COURSE_KEY), data['result'])
|
||||
self.assertEqual(test_course_flag.is_enabled(self.TEST_COURSE_KEY), data['result'])
|
||||
# result is cached, so override check should happen once
|
||||
WaffleFlagCourseOverrideModel.override_value.assert_called_once_with(
|
||||
self.NAMESPACED_FLAG_NAME,
|
||||
self.TEST_COURSE_KEY
|
||||
)
|
||||
|
||||
@@ -6,6 +6,12 @@ from functools import wraps
|
||||
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
# Can be used with FilteredQueryCountMixin.assertNumQueries() to blacklist
|
||||
# waffle tables. For example:
|
||||
# QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
|
||||
# with self.assertNumQueries(6, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
WAFFLE_TABLES = ['waffle_utils_waffleflagcourseoverridemodel', 'waffle_flag', 'waffle_switch', 'waffle_sample']
|
||||
|
||||
|
||||
def override_waffle_flag(flag, active):
|
||||
"""
|
||||
|
||||
@@ -9,6 +9,7 @@ Utility classes for testing django applications.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import re
|
||||
from unittest import skipUnless
|
||||
|
||||
import crum
|
||||
@@ -17,9 +18,10 @@ from django.conf import settings
|
||||
from django.contrib import sites
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.cache import caches
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
from django.test import RequestFactory, TestCase, override_settings
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from nose.plugins import Plugin
|
||||
|
||||
from request_cache.middleware import RequestCache
|
||||
|
||||
|
||||
@@ -145,6 +147,82 @@ class CacheIsolationTestCase(CacheIsolationMixin, TestCase):
|
||||
self.addCleanup(self.clear_caches)
|
||||
|
||||
|
||||
class _AssertNumQueriesContext(CaptureQueriesContext):
|
||||
"""
|
||||
This is a copy of Django's internal class of the same name, with the
|
||||
addition of being able to provide a table_blacklist used to filter queries
|
||||
before comparing the count.
|
||||
"""
|
||||
def __init__(self, test_case, num, connection, table_blacklist=None):
|
||||
"""
|
||||
Same as Django's _AssertNumQueriesContext __init__, with the addition of
|
||||
the following argument:
|
||||
table_blacklist (List): A list of table names to filter out of the
|
||||
set of queries that get counted.
|
||||
"""
|
||||
self.test_case = test_case
|
||||
self.num = num
|
||||
self.table_blacklist = table_blacklist
|
||||
super(_AssertNumQueriesContext, self).__init__(connection)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
def is_unfiltered_query(query):
|
||||
"""
|
||||
Returns True if the query does not contain a blacklisted table, and
|
||||
False otherwise.
|
||||
|
||||
Note: This is a simple naive implementation that makes no attempt
|
||||
to parse the query.
|
||||
"""
|
||||
if self.table_blacklist:
|
||||
for table in self.table_blacklist:
|
||||
# SQL contains the following format for columns:
|
||||
# "table_name"."column_name". The regex ensures there is no
|
||||
# "." before the name to avoid matching columns.
|
||||
if re.search(r'[^.]"{}"'.format(table), query['sql']):
|
||||
return False
|
||||
return True
|
||||
|
||||
super(_AssertNumQueriesContext, self).__exit__(exc_type, exc_value, traceback)
|
||||
if exc_type is not None:
|
||||
return
|
||||
filtered_queries = [query for query in self.captured_queries if is_unfiltered_query(query)]
|
||||
executed = len(filtered_queries)
|
||||
self.test_case.assertEqual(
|
||||
executed, self.num,
|
||||
"%d queries executed, %d expected\nCaptured queries were:\n%s" % (
|
||||
executed, self.num,
|
||||
'\n'.join(
|
||||
query['sql'] for query in filtered_queries
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class FilteredQueryCountMixin(object):
|
||||
"""
|
||||
Mixin to add to any subclass of Django's TestCase that replaces
|
||||
assertNumQueries with one that accepts a blacklist of tables to filter out
|
||||
of the count.
|
||||
"""
|
||||
def assertNumQueries(self, num, func=None, table_blacklist=None, *args, **kwargs):
|
||||
"""
|
||||
Used to replace Django's assertNumQueries with the same capability, with
|
||||
the addition of the following argument:
|
||||
table_blacklist (List): A list of table names to filter out of the
|
||||
set of queries that get counted.
|
||||
"""
|
||||
using = kwargs.pop("using", DEFAULT_DB_ALIAS)
|
||||
conn = connections[using]
|
||||
|
||||
context = _AssertNumQueriesContext(self, num, conn, table_blacklist=table_blacklist)
|
||||
if func is None:
|
||||
return context
|
||||
|
||||
with context:
|
||||
func(*args, **kwargs)
|
||||
|
||||
|
||||
class NoseDatabaseIsolation(Plugin):
|
||||
"""
|
||||
nosetest plugin that resets django databases before any tests begin.
|
||||
|
||||
@@ -38,7 +38,7 @@ class CourseBookmarksView(View):
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
course_url_name = default_course_url_name(request)
|
||||
course_url_name = default_course_url_name(course.id)
|
||||
course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)})
|
||||
|
||||
# Render the bookmarks list as a fragment
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
"""
|
||||
Unified course experience settings and helper methods.
|
||||
"""
|
||||
import waffle
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace
|
||||
from request_cache.middleware import RequestCache
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlag, WaffleFlagNamespace
|
||||
|
||||
# Waffle flag to enable the full screen course content view along with a unified
|
||||
# course home page.
|
||||
# NOTE: This is the only legacy flag that does not use the namespace.
|
||||
UNIFIED_COURSE_VIEW_FLAG = 'unified_course_view'
|
||||
|
||||
# Namespace for course experience waffle flags.
|
||||
WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='course_experience')
|
||||
|
||||
# Waffle flag to enable the separate course outline page and full width content.
|
||||
COURSE_OUTLINE_PAGE_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'course_outline_page', flag_undefined_default=True)
|
||||
|
||||
# Waffle flag to enable a single unified "Course" tab.
|
||||
UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'unified_course_tab')
|
||||
|
||||
@@ -33,11 +29,14 @@ def course_home_page_title(course): # pylint: disable=unused-argument
|
||||
return _('Course')
|
||||
|
||||
|
||||
def default_course_url_name(request=None):
|
||||
def default_course_url_name(course_id):
|
||||
"""
|
||||
Returns the default course URL name for the current user.
|
||||
|
||||
Arguments:
|
||||
course_id (CourseKey): The course id of the current course.
|
||||
"""
|
||||
if waffle.flag_is_active(request or RequestCache.get_current_request(), UNIFIED_COURSE_VIEW_FLAG):
|
||||
if COURSE_OUTLINE_PAGE_FLAG.is_enabled(course_id):
|
||||
return 'openedx.course_experience.course_home'
|
||||
else:
|
||||
return 'courseware'
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"""
|
||||
Tests for the course home page.
|
||||
"""
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
|
||||
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
@@ -19,6 +17,8 @@ TEST_WELCOME_MESSAGE = '<h2>Welcome!</h2>'
|
||||
TEST_UPDATE_MESSAGE = '<h2>Test Update!</h2>'
|
||||
TEST_COURSE_UPDATES_TOOL = '/course/updates">'
|
||||
|
||||
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
|
||||
|
||||
|
||||
def course_home_url(course):
|
||||
"""
|
||||
@@ -105,7 +105,7 @@ class TestCourseHomePage(SharedModuleStoreTestCase):
|
||||
course_home_url(self.course)
|
||||
|
||||
# Fetch the view and verify the query counts
|
||||
with self.assertNumQueries(46):
|
||||
with self.assertNumQueries(39, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_home_url(self.course)
|
||||
self.client.get(url)
|
||||
|
||||
@@ -4,15 +4,14 @@ Tests for course verification sock
|
||||
|
||||
import datetime
|
||||
import ddt
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from courseware.views.views import get_course_prices
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from .test_course_home import course_home_url
|
||||
|
||||
TEST_PASSWORD = 'test'
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""
|
||||
Tests for the course updates page.
|
||||
"""
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from courseware.courses import get_course_info_usage_key
|
||||
from django.core.urlresolvers import reverse
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
||||
from openedx.features.course_experience.views.course_updates import CourseUpdatesFragmentView
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from openedx.features.course_experience.views.course_updates import CourseUpdatesFragmentView
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
@@ -15,6 +15,8 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, chec
|
||||
|
||||
TEST_PASSWORD = 'test'
|
||||
|
||||
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
|
||||
|
||||
|
||||
def course_updates_url(course):
|
||||
"""
|
||||
@@ -125,7 +127,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
|
||||
course_updates_url(self.course)
|
||||
|
||||
# Fetch the view and verify that the query counts haven't changed
|
||||
with self.assertNumQueries(36):
|
||||
with self.assertNumQueries(32, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_updates_url(self.course)
|
||||
self.client.get(url)
|
||||
|
||||
@@ -46,7 +46,7 @@ class CourseReviewsFragmentView(EdxFragmentView):
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
course_url_name = default_course_url_name(request)
|
||||
course_url_name = default_course_url_name(course.id)
|
||||
course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)})
|
||||
|
||||
# Create the fragment
|
||||
|
||||
@@ -50,7 +50,7 @@ class CourseUpdatesFragmentView(EdxFragmentView):
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
course_url_name = default_course_url_name(request)
|
||||
course_url_name = default_course_url_name(course.id)
|
||||
course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)})
|
||||
|
||||
ordered_updates = self.get_ordered_updates(request, course)
|
||||
|
||||
@@ -49,7 +49,7 @@ class CourseSearchFragmentView(EdxFragmentView):
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_overview_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
course_url_name = default_course_url_name(request)
|
||||
course_url_name = default_course_url_name(course.id)
|
||||
course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)})
|
||||
|
||||
# Render the course home fragment
|
||||
|
||||
Reference in New Issue
Block a user