Commit eaab2cf4 by Robert Raposa

Add course overrides of waffle flags.

parent 867fac31
...@@ -1010,6 +1010,9 @@ INSTALLED_APPS = ( ...@@ -1010,6 +1010,9 @@ INSTALLED_APPS = (
# Customized celery tasks, including persisting failed tasks so they can # Customized celery tasks, including persisting failed tasks so they can
# be retried # be retried
'celery_utils', 'celery_utils',
# Waffle related utilities
'openedx.core.djangoapps.waffle_utils',
) )
......
...@@ -2267,7 +2267,7 @@ def auto_auth(request): ...@@ -2267,7 +2267,7 @@ def auto_auth(request):
elif course_id: elif course_id:
# Redirect to the course homepage (in LMS) or outline page (in Studio) # Redirect to the course homepage (in LMS) or outline page (in Studio)
try: try:
redirect_url = reverse(course_home_url_name(request), kwargs={'course_id': course_id}) redirect_url = reverse(course_home_url_name(course_key), kwargs={'course_id': course_id})
except NoReverseMatch: except NoReverseMatch:
redirect_url = reverse('course_handler', kwargs={'course_key_string': course_id}) redirect_url = reverse('course_handler', kwargs={'course_key_string': course_id})
else: else:
......
...@@ -231,18 +231,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): ...@@ -231,18 +231,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
# # of sql queries to default, # # of sql queries to default,
# # of mongo queries, # # of mongo queries,
# ) # )
('no_overrides', 1, True, False): (24, 1), ('no_overrides', 1, True, False): (25, 1),
('no_overrides', 2, True, False): (24, 1), ('no_overrides', 2, True, False): (25, 1),
('no_overrides', 3, True, False): (24, 1), ('no_overrides', 3, True, False): (25, 1),
('ccx', 1, True, False): (24, 1), ('ccx', 1, True, False): (25, 1),
('ccx', 2, True, False): (24, 1), ('ccx', 2, True, False): (25, 1),
('ccx', 3, True, False): (24, 1), ('ccx', 3, True, False): (25, 1),
('no_overrides', 1, False, False): (24, 1), ('no_overrides', 1, False, False): (25, 1),
('no_overrides', 2, False, False): (24, 1), ('no_overrides', 2, False, False): (25, 1),
('no_overrides', 3, False, False): (24, 1), ('no_overrides', 3, False, False): (25, 1),
('ccx', 1, False, False): (24, 1), ('ccx', 1, False, False): (25, 1),
('ccx', 2, False, False): (24, 1), ('ccx', 2, False, False): (25, 1),
('ccx', 3, False, False): (24, 1), ('ccx', 3, False, False): (25, 1),
} }
...@@ -254,19 +254,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): ...@@ -254,19 +254,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True __test__ = True
TEST_DATA = { TEST_DATA = {
('no_overrides', 1, True, False): (24, 3), ('no_overrides', 1, True, False): (25, 3),
('no_overrides', 2, True, False): (24, 3), ('no_overrides', 2, True, False): (25, 3),
('no_overrides', 3, True, False): (24, 3), ('no_overrides', 3, True, False): (25, 3),
('ccx', 1, True, False): (24, 3), ('ccx', 1, True, False): (25, 3),
('ccx', 2, True, False): (24, 3), ('ccx', 2, True, False): (25, 3),
('ccx', 3, True, False): (24, 3), ('ccx', 3, True, False): (25, 3),
('ccx', 1, True, True): (25, 3), ('ccx', 1, True, True): (26, 3),
('ccx', 2, True, True): (25, 3), ('ccx', 2, True, True): (26, 3),
('ccx', 3, True, True): (25, 3), ('ccx', 3, True, True): (26, 3),
('no_overrides', 1, False, False): (24, 3), ('no_overrides', 1, False, False): (25, 3),
('no_overrides', 2, False, False): (24, 3), ('no_overrides', 2, False, False): (25, 3),
('no_overrides', 3, False, False): (24, 3), ('no_overrides', 3, False, False): (25, 3),
('ccx', 1, False, False): (24, 3), ('ccx', 1, False, False): (25, 3),
('ccx', 2, False, False): (24, 3), ('ccx', 2, False, False): (25, 3),
('ccx', 3, False, False): (24, 3), ('ccx', 3, False, False): (25, 3),
} }
...@@ -2,15 +2,14 @@ ...@@ -2,15 +2,14 @@
This module is essentially a broker to xmodule/tabs.py -- it was originally introduced to 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 perform some LMS-specific tab display gymnastics for the Entrance Exams feature
""" """
import waffle
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _, ugettext_noop from django.utils.translation import ugettext as _, ugettext_noop
from courseware.access import has_access from courseware.access import has_access
from courseware.entrance_exams import user_can_skip_entrance_exam from courseware.entrance_exams import user_can_skip_entrance_exam
from openedx.core.lib.course_tabs import CourseTabPluginManager from openedx.core.lib.course_tabs import CourseTabPluginManager
from openedx.features.course_experience import default_course_url_name, UNIFIED_COURSE_EXPERIENCE_FLAG from openedx.features.course_experience import default_course_url_name
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
from request_cache.middleware import RequestCache from request_cache.middleware import RequestCache
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.tabs import CourseTab, CourseTabList, key_checker, link_reverse_func from xmodule.tabs import CourseTab, CourseTabList, key_checker, link_reverse_func
...@@ -66,8 +65,7 @@ class CourseInfoTab(CourseTab): ...@@ -66,8 +65,7 @@ class CourseInfoTab(CourseTab):
""" """
The "Home" tab is not shown for the new unified course experience. The "Home" tab is not shown for the new unified course experience.
""" """
request = RequestCache.get_current_request() return not UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id)
return not waffle.flag_is_active(request, UNIFIED_COURSE_EXPERIENCE_FLAG)
class SyllabusTab(EnrolledTab): class SyllabusTab(EnrolledTab):
......
...@@ -7,8 +7,6 @@ from django.core.urlresolvers import reverse ...@@ -7,8 +7,6 @@ from django.core.urlresolvers import reverse
from freezegun import freeze_time from freezegun import freeze_time
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from pytz import utc from pytz import utc
from waffle.testutils import override_flag
from openedx.features.course_experience import UNIFIED_COURSE_EXPERIENCE_FLAG
from commerce.models import CommerceConfiguration from commerce.models import CommerceConfiguration
from course_modes.tests.factories import CourseModeFactory from course_modes.tests.factories import CourseModeFactory
...@@ -24,6 +22,8 @@ from courseware.date_summary import ( ...@@ -24,6 +22,8 @@ from courseware.date_summary import (
) )
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
from student.tests.factories import CourseEnrollmentFactory, UserFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory
from lms.djangoapps.verify_student.models import VerificationDeadline from lms.djangoapps.verify_student.models import VerificationDeadline
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
...@@ -194,7 +194,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -194,7 +194,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
'info', 'info',
'openedx.course_experience.course_home', 'openedx.course_experience.course_home',
) )
@override_flag(UNIFIED_COURSE_EXPERIENCE_FLAG, active=True) @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
def test_todays_date_no_timezone(self, url_name): def test_todays_date_no_timezone(self, url_name):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
self.setup_course_and_user() self.setup_course_and_user()
...@@ -218,7 +218,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -218,7 +218,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
'info', 'info',
'openedx.course_experience.course_home', 'openedx.course_experience.course_home',
) )
@override_flag(UNIFIED_COURSE_EXPERIENCE_FLAG, active=True) @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
def test_todays_date_timezone(self, url_name): def test_todays_date_timezone(self, url_name):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
self.setup_course_and_user() self.setup_course_and_user()
...@@ -249,7 +249,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -249,7 +249,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
'info', 'info',
'openedx.course_experience.course_home', 'openedx.course_experience.course_home',
) )
@override_flag(UNIFIED_COURSE_EXPERIENCE_FLAG, active=True) @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
def test_start_date_render(self, url_name): def test_start_date_render(self, url_name):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
self.setup_course_and_user() self.setup_course_and_user()
...@@ -267,7 +267,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -267,7 +267,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
'info', 'info',
'openedx.course_experience.course_home', 'openedx.course_experience.course_home',
) )
@override_flag(UNIFIED_COURSE_EXPERIENCE_FLAG, active=True) @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
def test_start_date_render_time_zone(self, url_name): def test_start_date_render_time_zone(self, url_name):
with freeze_time('2015-01-02'): with freeze_time('2015-01-02'):
self.setup_course_and_user() self.setup_course_and_user()
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
Test cases for tabs. Test cases for tabs.
""" """
from waffle.testutils import override_flag
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404 from django.http import Http404
from mock import MagicMock, Mock, patch from mock import MagicMock, Mock, patch
...@@ -18,7 +16,8 @@ from courseware.tests.helpers import LoginEnrollmentTestCase ...@@ -18,7 +16,8 @@ from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.factories import InstructorFactory, StaffFactory from courseware.tests.factories import InstructorFactory, StaffFactory
from courseware.views.views import get_static_tab_fragment, StaticCourseTabView from courseware.views.views import get_static_tab_fragment, StaticCourseTabView
from openedx.core.djangolib.testing.utils import get_mock_request from openedx.core.djangolib.testing.utils import get_mock_request
from openedx.features.course_experience import UNIFIED_COURSE_EXPERIENCE_FLAG from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from util.milestones_helpers import ( from util.milestones_helpers import (
...@@ -776,12 +775,13 @@ class CourseInfoTabTestCase(TabTestCase): ...@@ -776,12 +775,13 @@ class CourseInfoTabTestCase(TabTestCase):
self.user = self.create_mock_user() self.user = self.create_mock_user()
self.request = get_mock_request(self.user) self.request = get_mock_request(self.user)
@override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=False)
def test_default_tab(self): def test_default_tab(self):
# Verify that the course info tab is the first tab # Verify that the course info tab is the first tab
tabs = get_course_tab_list(self.request, self.course) tabs = get_course_tab_list(self.request, self.course)
self.assertEqual(tabs[0].type, 'course_info') self.assertEqual(tabs[0].type, 'course_info')
@override_flag(UNIFIED_COURSE_EXPERIENCE_FLAG, active=True) @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
def test_default_tab_for_new_course_experience(self): def test_default_tab_for_new_course_experience(self):
# Verify that the unified course experience hides the course info tab # Verify that the unified course experience hides the course info tab
tabs = get_course_tab_list(self.request, self.course) tabs = get_course_tab_list(self.request, self.course)
......
...@@ -210,8 +210,8 @@ class IndexQueryTestCase(ModuleStoreTestCase): ...@@ -210,8 +210,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS = 20 NUM_PROBLEMS = 20
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 10, 142), (ModuleStoreEnum.Type.mongo, 10, 143),
(ModuleStoreEnum.Type.split, 4, 142), (ModuleStoreEnum.Type.split, 4, 143),
) )
@ddt.unpack @ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
...@@ -1420,12 +1420,12 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1420,12 +1420,12 @@ class ProgressPageTests(ProgressPageBaseTests):
"""Test that query counts remain the same for self-paced and instructor-paced courses.""" """Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration(enabled=self_paced_enabled).save() SelfPacedConfiguration(enabled=self_paced_enabled).save()
self.setup_course(self_paced=self_paced) self.setup_course(self_paced=self_paced)
with self.assertNumQueries(40), check_mongo_calls(1): with self.assertNumQueries(41), check_mongo_calls(1):
self._get_progress_page() self._get_progress_page()
@ddt.data( @ddt.data(
(False, 40, 26), (False, 41, 27),
(True, 33, 22) (True, 34, 23)
) )
@ddt.unpack @ddt.unpack
def test_progress_queries(self, enable_waffle, initial, subsequent): def test_progress_queries(self, enable_waffle, initial, subsequent):
......
...@@ -86,9 +86,9 @@ from openedx.core.djangoapps.programs.utils import ProgramMarketingDataExtender ...@@ -86,9 +86,9 @@ from openedx.core.djangoapps.programs.utils import ProgramMarketingDataExtender
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.features.course_experience import ( from openedx.features.course_experience import (
UNIFIED_COURSE_EXPERIENCE_FLAG, UNIFIED_COURSE_TAB_FLAG,
UNIFIED_COURSE_VIEW_FLAG, UNIFIED_COURSE_VIEW_FLAG,
course_home_url_name course_home_url_name,
) )
from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView
from openedx.features.enterprise_support.api import data_sharing_consent_required from openedx.features.enterprise_support.api import data_sharing_consent_required
...@@ -263,11 +263,12 @@ def course_info(request, course_id): ...@@ -263,11 +263,12 @@ def course_info(request, course_id):
return url return url
return None return None
course_key = CourseKey.from_string(course_id)
# If the unified course experience is enabled, redirect to the "Course" tab # If the unified course experience is enabled, redirect to the "Course" tab
if waffle.flag_is_active(request, UNIFIED_COURSE_EXPERIENCE_FLAG): if UNIFIED_COURSE_TAB_FLAG.is_enabled(course_key):
return redirect(reverse(course_home_url_name(request), args=[course_id])) return redirect(reverse(course_home_url_name(course_key), args=[course_id]))
course_key = CourseKey.from_string(course_id)
with modulestore().bulk_operations(course_key): with modulestore().bulk_operations(course_key):
course = get_course_by_id(course_key, depth=2) course = get_course_by_id(course_key, depth=2)
access_response = has_access(request.user, 'load', course, course_key) access_response = has_access(request.user, 'load', course, course_key)
...@@ -669,7 +670,7 @@ def course_about(request, course_id): ...@@ -669,7 +670,7 @@ def course_about(request, course_id):
modes = CourseMode.modes_for_course_dict(course_key) modes = CourseMode.modes_for_course_dict(course_key)
if configuration_helpers.get_value('ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False)): if configuration_helpers.get_value('ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False)):
return redirect(reverse(course_home_url_name(request), args=[unicode(course.id)])) return redirect(reverse(course_home_url_name(course.id), args=[unicode(course.id)]))
registered = registered_for_course(course, request.user) registered = registered_for_course(course, request.user)
...@@ -677,7 +678,7 @@ def course_about(request, course_id): ...@@ -677,7 +678,7 @@ def course_about(request, course_id):
studio_url = get_studio_url(course, 'settings/details') studio_url = get_studio_url(course, 'settings/details')
if has_access(request.user, 'load', course): if has_access(request.user, 'load', course):
course_target = reverse(course_home_url_name(request), args=[course.id.to_deprecated_string()]) course_target = reverse(course_home_url_name(course.id), args=[course.id.to_deprecated_string()])
else: else:
course_target = reverse('about_course', args=[course.id.to_deprecated_string()]) course_target = reverse('about_course', args=[course.id.to_deprecated_string()])
...@@ -1241,7 +1242,7 @@ def course_survey(request, course_id): ...@@ -1241,7 +1242,7 @@ def course_survey(request, course_id):
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key) course = get_course_with_access(request.user, 'load', course_key)
redirect_url = reverse(course_home_url_name(request), args=[course_id]) redirect_url = reverse(course_home_url_name(course.id), args=[course_id])
# if there is no Survey associated with this course, # if there is no Survey associated with this course,
# then redirect to the course instead # then redirect to the course instead
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
This module contains various configuration settings via This module contains various configuration settings via
waffle switches for the Grades app. waffle switches for the Grades app.
""" """
from openedx.core.djangolib.waffle_utils import WaffleSwitchPlus from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
# Namespace # Namespace
...@@ -18,4 +18,4 @@ def waffle(): ...@@ -18,4 +18,4 @@ def waffle():
""" """
Returns the namespaced, cached, audited Waffle class for Grades. Returns the namespaced, cached, audited Waffle class for Grades.
""" """
return WaffleSwitchPlus(namespace=WAFFLE_NAMESPACE, log_prefix=u'Grades: ') return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Grades: ')
...@@ -2211,6 +2211,9 @@ INSTALLED_APPS = ( ...@@ -2211,6 +2211,9 @@ INSTALLED_APPS = (
# Unusual migrations # Unusual migrations
'database_fixups', 'database_fixups',
# Waffle related utilities
'openedx.core.djangoapps.waffle_utils',
# Features # Features
'openedx.features.course_bookmarks', 'openedx.features.course_bookmarks',
'openedx.features.course_experience', 'openedx.features.course_experience',
......
...@@ -56,7 +56,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_ ...@@ -56,7 +56,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
% endif % endif
<div class="course-container"> <div class="course-container">
<article class="course${mode_class}"> <article class="course${mode_class}">
<% course_target = reverse(course_home_url_name(), args=[unicode(course_overview.id)]) %> <% course_target = reverse(course_home_url_name(course_overview.id), args=[unicode(course_overview.id)]) %>
<section class="details" aria-labelledby="details-heading-${course_overview.number}"> <section class="details" aria-labelledby="details-heading-${course_overview.number}">
<h2 class="hd hd-2 sr" id="details-heading-${course_overview.number}">${_('Course details')}</h2> <h2 class="hd hd-2 sr" id="details-heading-${course_overview.number}">${_('Course details')}</h2>
<div class="wrapper-course-image" aria-hidden="true"> <div class="wrapper-course-image" aria-hidden="true">
......
...@@ -75,7 +75,7 @@ from openedx.features.course_experience import course_home_url_name ...@@ -75,7 +75,7 @@ from openedx.features.course_experience import course_home_url_name
</div> </div>
% if not reg_code_already_redeemed: % if not reg_code_already_redeemed:
%if redemption_success: %if redemption_success:
<% course_url = reverse(course_home_url_name(), args=[course.id.to_deprecated_string()]) %> <% course_url = reverse(course_home_url_name(course.id), args=[course.id.to_deprecated_string()]) %>
<a href="${course_url}" class="link-button course-link-bg-color">${_("View Course")} <span class="icon fa fa-caret-right" aria-hidden="true"></span></a> <a href="${course_url}" class="link-button course-link-bg-color">${_("View Course")} <span class="icon fa fa-caret-right" aria-hidden="true"></span></a>
%elif not registered_for_course: %elif not registered_for_course:
<form method="post"> <form method="post">
......
...@@ -55,6 +55,6 @@ class HelpModalTests(ModuleStoreTestCase): ...@@ -55,6 +55,6 @@ class HelpModalTests(ModuleStoreTestCase):
Simple test to make sure that you don't get a 500 error when the modal Simple test to make sure that you don't get a 500 error when the modal
is enabled. is enabled.
""" """
url = reverse(course_home_url_name(), args=[self.course.id.to_deprecated_string()]) url = reverse(course_home_url_name(self.course.id), args=[self.course.id.to_deprecated_string()])
resp = self.client.get(url) resp = self.client.get(url)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
This module contains various configuration settings via This module contains various configuration settings via
waffle switches for the Block Structure framework. waffle switches for the Block Structure framework.
""" """
from openedx.core.djangolib.waffle_utils import WaffleSwitchPlus from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
from request_cache.middleware import request_cached from request_cache.middleware import request_cached
from .models import BlockStructureConfiguration from .models import BlockStructureConfiguration
...@@ -22,7 +22,7 @@ def waffle(): ...@@ -22,7 +22,7 @@ def waffle():
""" """
Returns the namespaced and cached Waffle class for BlockStructures. Returns the namespaced and cached Waffle class for BlockStructures.
""" """
return WaffleSwitchPlus(namespace=WAFFLE_NAMESPACE, log_prefix=u'BlockStructure: ') return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'BlockStructure: ')
@request_cached @request_cached
......
...@@ -19,7 +19,7 @@ except ImportError: ...@@ -19,7 +19,7 @@ except ImportError:
import psutil import psutil
import request_cache import request_cache
from openedx.core.djangolib.waffle_utils import WaffleSwitchPlus from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
REQUEST_CACHE_KEY = 'monitoring_custom_metrics' REQUEST_CACHE_KEY = 'monitoring_custom_metrics'
...@@ -163,4 +163,4 @@ class MonitoringMemoryMiddleware(object): ...@@ -163,4 +163,4 @@ class MonitoringMemoryMiddleware(object):
""" """
Returns whether this middleware is enabled. Returns whether this middleware is enabled.
""" """
return WaffleSwitchPlus(namespace=WAFFLE_NAMESPACE).is_enabled(u'enable_memory_middleware') return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE).is_enabled(u'enable_memory_middleware')
"""
Django admin page for waffle utils models
"""
from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin, KeyedConfigurationModelAdmin
from .forms import WaffleFlagCourseOverrideAdminForm
from .models import WaffleFlagCourseOverrideModel
class WaffleFlagCourseOverrideAdmin(KeyedConfigurationModelAdmin):
"""
Admin for course override of waffle flags.
Includes search by course_id and waffle_flag.
"""
form = WaffleFlagCourseOverrideAdminForm
search_fields = ['waffle_flag', 'course_id']
fieldsets = (
(None, {
'fields': ('waffle_flag', 'course_id', 'override_choice', 'enabled'),
'description': 'Enter a valid course id and an existing waffle flag. The waffle flag name is not validated.'
}),
)
admin.site.register(WaffleFlagCourseOverrideModel, WaffleFlagCourseOverrideAdmin)
"""
Defines a form for providing validation of subsection grade templates.
"""
from django import forms
from openedx.core.lib.courses import clean_course_id
from .models import WaffleFlagCourseOverrideModel
class WaffleFlagCourseOverrideAdminForm(forms.ModelForm):
"""
Input form for course override of waffle flags, allowing us to verify data.
"""
class Meta(object):
model = WaffleFlagCourseOverrideModel
fields = '__all__'
def clean_course_id(self):
"""
Validate the course id
"""
return clean_course_id(self)
def clean_waffle_flag(self):
"""
Validate the waffle flag is an existing flag.
"""
cleaned_flag = self.cleaned_data['waffle_flag']
if not cleaned_flag:
msg = u'Waffle flag must be supplied.'
raise forms.ValidationError(msg)
return cleaned_flag.strip()
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import openedx.core.djangoapps.xmodule_django.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='WaffleFlagCourseOverrideModel',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('waffle_flag', models.CharField(max_length=255, db_index=True)),
('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255, db_index=True)),
('override_choice', models.CharField(default=b'on', max_length=3, choices=[(b'on', 'Force On'), (b'off', 'Force Off')])),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
options={
'verbose_name': 'Waffle flag course override',
'verbose_name_plural': 'Waffle flag course overrides',
},
),
]
"""
Models for configuring waffle utils.
"""
from django.db.models import CharField
from django.utils.translation import ugettext_lazy as _
from model_utils import Choices
from config_models.models import ConfigurationModel
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
from request_cache.middleware import request_cached
class WaffleFlagCourseOverrideModel(ConfigurationModel):
"""
Used to force a waffle flag on or off for a course.
"""
OVERRIDE_CHOICES = Choices(('on', _('Force On')), ('off', _('Force Off')))
ALL_CHOICES = OVERRIDE_CHOICES + Choices('unset')
KEY_FIELDS = ('waffle_flag', 'course_id')
# The course that these features are attached to.
waffle_flag = CharField(max_length=255, db_index=True)
course_id = CourseKeyField(max_length=255, db_index=True)
override_choice = CharField(choices=OVERRIDE_CHOICES, default=OVERRIDE_CHOICES.on, max_length=3)
@classmethod
@request_cached
def override_value(cls, waffle_flag, course_id):
"""
Returns whether the waffle flag was overridden (on or off) for the
course, or is unset.
Arguments:
waffle_flag (String): The name of the flag.
course_id (CourseKey): The course id for which the flag may have
been overridden.
If the current config is not set or disabled for this waffle flag and
course id, returns ALL_CHOICES.unset.
Otherwise, returns ALL_CHOICES.on or ALL_CHOICES.off as configured for
the override_choice.
"""
if not course_id or not waffle_flag:
return cls.ALL_CHOICES.unset
effective = cls.objects.filter(waffle_flag=waffle_flag, course_id=course_id).order_by('-change_date').first()
if effective and effective.enabled:
return effective.override_choice
return cls.ALL_CHOICES.unset
class Meta(object):
app_label = "waffle_utils"
verbose_name = 'Waffle flag course override'
verbose_name_plural = 'Waffle flag course overrides'
def __unicode__(self):
enabled_label = "Enabled" if self.enabled else "Not Enabled"
# pylint: disable=no-member
return u"Course '{}': Persistent Grades {}".format(self.course_id.to_deprecated_string(), enabled_label)
"""
Tests for waffle utils features.
"""
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 .. import CourseWaffleFlag, WaffleFlagNamespace
from ..models import WaffleFlagCourseOverrideModel
@ddt.ddt
class TestCourseWaffleFlag(TestCase):
"""
Tests the CourseWaffleFlag.
"""
NAMESPACE_NAME = "test_namespace"
FLAG_NAME = "test_flag"
NAMESPACED_FLAG_NAME = NAMESPACE_NAME + "." + FLAG_NAME
TEST_COURSE_KEY = CourseKey.from_string("edX/DemoX/Demo_Course")
TEST_NAMESPACE = WaffleFlagNamespace(NAMESPACE_NAME)
TEST_COURSE_FLAG = CourseWaffleFlag(TEST_NAMESPACE, FLAG_NAME)
@ddt.data(
{'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.on, 'waffle_enabled': False, 'result': True},
{'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.off, 'waffle_enabled': True, 'result': False},
{'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.unset, 'waffle_enabled': True, 'result': True},
{'course_override': WaffleFlagCourseOverrideModel.ALL_CHOICES.unset, 'waffle_enabled': False, 'result': False},
)
def test_course_waffle_flag(self, data):
"""
Tests various combinations of a flag being set in waffle and overridden
for a course.
"""
RequestCache.clear_request_cache()
with patch.object(WaffleFlagCourseOverrideModel, 'override_value', return_value=data['course_override']):
with override_flag(self.NAMESPACED_FLAG_NAME, active=data['waffle_enabled']):
# check twice to test that the result is properly cached
self.assertEqual(self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_KEY), data['result'])
self.assertEqual(self.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
)
"""
Tests for waffle utils models.
"""
from ddt import data, ddt, unpack
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
from request_cache.middleware import RequestCache
from ..models import WaffleFlagCourseOverrideModel
@ddt
class WaffleFlagCourseOverrideTests(TestCase):
"""
Tests for the waffle flag course override model.
"""
WAFFLE_TEST_NAME = "waffle_test_course_override"
TEST_COURSE_KEY = CourseKey.from_string("edX/DemoX/Demo_Course")
OVERRIDE_CHOICES = WaffleFlagCourseOverrideModel.ALL_CHOICES
# Data format: ( is_enabled, override_choice, expected_result )
@data((True, OVERRIDE_CHOICES.on, OVERRIDE_CHOICES.on),
(True, OVERRIDE_CHOICES.off, OVERRIDE_CHOICES.off),
(False, OVERRIDE_CHOICES.on, OVERRIDE_CHOICES.unset))
@unpack
def test_setting_override(self, is_enabled, override_choice, expected_result):
RequestCache.clear_request_cache()
self.set_waffle_course_override(override_choice, is_enabled)
override_value = WaffleFlagCourseOverrideModel.override_value(
self.WAFFLE_TEST_NAME, self.TEST_COURSE_KEY
)
self.assertEqual(override_value, expected_result)
def test_setting_override_multiple_times(self):
RequestCache.clear_request_cache()
self.set_waffle_course_override(self.OVERRIDE_CHOICES.on)
self.set_waffle_course_override(self.OVERRIDE_CHOICES.off)
override_value = WaffleFlagCourseOverrideModel.override_value(
self.WAFFLE_TEST_NAME, self.TEST_COURSE_KEY
)
self.assertEqual(override_value, self.OVERRIDE_CHOICES.off)
def set_waffle_course_override(self, override_choice, is_enabled=True):
WaffleFlagCourseOverrideModel.objects.create(
waffle_flag=self.WAFFLE_TEST_NAME,
override_choice=override_choice,
enabled=is_enabled,
course_id=self.TEST_COURSE_KEY
)
"""
Tests for waffle utils test utilities.
"""
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
from request_cache.middleware import RequestCache
from .. import CourseWaffleFlag, WaffleFlagNamespace
from ..testutils import override_waffle_flag
class OverrideWaffleFlagTests(TestCase):
"""
Tests for the override_waffle_flag decorator.
"""
NAMESPACE_NAME = "test_namespace"
FLAG_NAME = "test_flag"
NAMESPACED_FLAG_NAME = NAMESPACE_NAME + "." + FLAG_NAME
TEST_COURSE_KEY = CourseKey.from_string("edX/DemoX/Demo_Course")
TEST_NAMESPACE = WaffleFlagNamespace(NAMESPACE_NAME)
TEST_COURSE_FLAG = CourseWaffleFlag(TEST_NAMESPACE, FLAG_NAME)
def setUp(self):
super(OverrideWaffleFlagTests, self).setUp()
RequestCache.clear_request_cache()
@override_waffle_flag(TEST_COURSE_FLAG, True)
def check_is_enabled_with_decorator(self):
# test flag while overridden with decorator
self.assertTrue(self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_KEY))
def test_override_waffle_flag_pre_cached(self):
# checks and caches the is_enabled value
self.assertFalse(self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_KEY))
flag_cache = self.TEST_COURSE_FLAG.waffle_namespace._cached_flags
self.assertIn(self.NAMESPACED_FLAG_NAME, flag_cache)
# test flag while overridden with decorator
self.check_is_enabled_with_decorator()
# test cached flag is restored
self.assertIn(self.NAMESPACED_FLAG_NAME, flag_cache)
self.assertEquals(self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_KEY), False)
def test_override_waffle_flag_not_pre_cached(self):
# check that the flag is not yet cached
flag_cache = self.TEST_COURSE_FLAG.waffle_namespace._cached_flags
self.assertNotIn(self.NAMESPACED_FLAG_NAME, flag_cache)
# test flag while overridden with decorator
self.check_is_enabled_with_decorator()
# test cache is removed when no longer using decorator/context manager
self.assertNotIn(self.NAMESPACED_FLAG_NAME, flag_cache)
"""
Test utilities for waffle utilities.
"""
from functools import wraps
from waffle.testutils import override_flag
def override_waffle_flag(flag, active):
"""
To be used as a decorator for a test function to override a namespaced
waffle flag.
flag (WaffleFlag): The namespaced cached waffle flag.
active (Boolean): The value to which the flag will be set.
Example usage:
@override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
"""
def real_decorator(function):
"""
Actual decorator function.
"""
@wraps(function)
def wrapper(*args, **kwargs):
"""
Provides the actual override functionality of the decorator.
Saves the previous cached value of the flag and restores it (if it
was set), after overriding it.
"""
waffle_namespace = flag.waffle_namespace
namespaced_flag_name = waffle_namespace._namespaced_name(flag.flag_name)
# save previous value and whether it existed in the cache
cached_value_existed = namespaced_flag_name in waffle_namespace._cached_flags
if cached_value_existed:
previous_value = waffle_namespace._cached_flags[namespaced_flag_name]
# set new value
waffle_namespace._cached_flags[namespaced_flag_name] = active
with override_flag(namespaced_flag_name, active):
# call wrapped function
function(*args, **kwargs)
# restore value
if cached_value_existed:
waffle_namespace._cached_flags[namespaced_flag_name] = previous_value
elif namespaced_flag_name in waffle_namespace._cached_flags:
del waffle_namespace._cached_flags[namespaced_flag_name]
return wrapper
return real_decorator
""" """
Common utility functions related to courses. Common utility functions related to courses.
""" """
from django import forms
from django.conf import settings from django.conf import settings
from opaque_keys import InvalidKeyError
from opaque_keys.edx.locator import CourseKey
from xmodule.assetstore.assetmgr import AssetManager from xmodule.assetstore.assetmgr import AssetManager
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import modulestore
def course_image_url(course, image_key='course_image'): def course_image_url(course, image_key='course_image'):
...@@ -43,3 +47,37 @@ def create_course_image_thumbnail(course, dimensions): ...@@ -43,3 +47,37 @@ def create_course_image_thumbnail(course, dimensions):
_content, thumb_loc = contentstore().generate_thumbnail(course_image, dimensions=dimensions) _content, thumb_loc = contentstore().generate_thumbnail(course_image, dimensions=dimensions)
return StaticContent.serialize_asset_key_with_slash(thumb_loc) return StaticContent.serialize_asset_key_with_slash(thumb_loc)
def clean_course_id(model_form, is_required=True):
"""
Cleans and validates a course_id for use with a Django ModelForm.
Arguments:
model_form (form.ModelForm): The form that has a course_id.
is_required (Boolean): Default True. When True, validates that the
course_id is not empty. In all cases, when course_id is supplied,
validates that it is a valid course.
Returns:
(CourseKey) The cleaned and validated course_id as a CourseKey.
NOTE: This should ultimately replace all copies of "def clean_course_id".
"""
cleaned_id = model_form.cleaned_data["course_id"]
if not cleaned_id and not is_required:
return None
try:
course_key = CourseKey.from_string(cleaned_id)
except InvalidKeyError:
msg = u'Course id invalid. Entered course id was: "{0}."'.format(cleaned_id)
raise forms.ValidationError(msg)
if not modulestore().has_course(course_key):
msg = u'Course not found. Entered course id was: "{0}". '.format(course_key.to_deprecated_string())
raise forms.ValidationError(msg)
return course_key
""" """
Unified course experience settings and helper methods. Unified course experience settings and helper methods.
""" """
import waffle import waffle
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace
from request_cache.middleware import RequestCache from request_cache.middleware import RequestCache
# Waffle flag to enable a single unified "Course" tab. # Waffle flag to enable the full screen course content view along with a unified
UNIFIED_COURSE_EXPERIENCE_FLAG = 'unified_course_experience' # course home page.
# NOTE: This is the only legacy flag that does not use the namespace.
# Waffle flag to enable the full screen course content view
# along with a unified course home page.
UNIFIED_COURSE_VIEW_FLAG = 'unified_course_view' UNIFIED_COURSE_VIEW_FLAG = 'unified_course_view'
# Namespace for course experience waffle flags.
WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='course_experience')
# Waffle flag to enable a single unified "Course" tab.
UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'unified_course_tab')
def default_course_url_name(request=None): def default_course_url_name(request=None):
""" """
...@@ -24,11 +28,16 @@ def default_course_url_name(request=None): ...@@ -24,11 +28,16 @@ def default_course_url_name(request=None):
return 'courseware' return 'courseware'
def course_home_url_name(request=None): def course_home_url_name(course_key):
""" """
Returns the course home page's URL name for the current user. Returns the course home page's URL name for the current user.
Arguments:
course_key (CourseKey): The course key for which the home url is being
requested.
""" """
if waffle.flag_is_active(request or RequestCache.get_current_request(), UNIFIED_COURSE_EXPERIENCE_FLAG): if UNIFIED_COURSE_TAB_FLAG.is_enabled(course_key):
return 'openedx.course_experience.course_home' return 'openedx.course_experience.course_home'
else: else:
return 'info' return 'info'
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
<%! <%!
import json import json
import waffle
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -15,7 +14,7 @@ from django.core.urlresolvers import reverse ...@@ -15,7 +14,7 @@ from django.core.urlresolvers import reverse
from django_comment_client.permissions import has_permission from django_comment_client.permissions import has_permission
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
from openedx.core.djangolib.markup import HTML from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import UNIFIED_COURSE_EXPERIENCE_FLAG from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
%> %>
<%block name="content"> <%block name="content">
...@@ -58,7 +57,7 @@ from openedx.features.course_experience import UNIFIED_COURSE_EXPERIENCE_FLAG ...@@ -58,7 +57,7 @@ from openedx.features.course_experience import UNIFIED_COURSE_EXPERIENCE_FLAG
<div class="page-content"> <div class="page-content">
<div class="layout layout-1q3q"> <div class="layout layout-1q3q">
<main class="layout-col layout-col-b"> <main class="layout-col layout-col-b">
% if welcome_message_fragment and waffle.flag_is_active(request, UNIFIED_COURSE_EXPERIENCE_FLAG): % if welcome_message_fragment and UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id):
<div class="section section-dates"> <div class="section section-dates">
${HTML(welcome_message_fragment.body_html())} ${HTML(welcome_message_fragment.body_html())}
</div> </div>
...@@ -76,7 +75,7 @@ from openedx.features.course_experience import UNIFIED_COURSE_EXPERIENCE_FLAG ...@@ -76,7 +75,7 @@ from openedx.features.course_experience import UNIFIED_COURSE_EXPERIENCE_FLAG
${_("Bookmarks")} ${_("Bookmarks")}
</a> </a>
</li> </li>
% if waffle.flag_is_active(request, UNIFIED_COURSE_EXPERIENCE_FLAG): % if UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id):
<li> <li>
<a href="${reverse('openedx.course_experience.course_updates', args=[course.id])}"> <a href="${reverse('openedx.course_experience.course_updates', args=[course.id])}">
<span class="icon fa fa-newspaper-o" aria-hidden="true"></span> <span class="icon fa fa-newspaper-o" aria-hidden="true"></span>
......
...@@ -4,17 +4,9 @@ ...@@ -4,17 +4,9 @@
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
<%! <%!
import json
from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.template.defaultfilters import escapejs
from django.core.urlresolvers import reverse
from django_comment_client.permissions import has_permission
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
from openedx.core.djangolib.markup import HTML from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import UNIFIED_COURSE_EXPERIENCE_FLAG
%> %>
<%block name="content"> <%block name="content">
......
...@@ -2,18 +2,16 @@ ...@@ -2,18 +2,16 @@
Tests for the course home page. Tests for the course home page.
""" """
from waffle.testutils import override_flag
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
from openedx.features.course_experience import UNIFIED_COURSE_EXPERIENCE_FLAG
from .test_course_updates import create_course_update, remove_course_updates from .test_course_updates import create_course_update, remove_course_updates
TEST_PASSWORD = 'test' TEST_PASSWORD = 'test'
...@@ -71,22 +69,13 @@ class TestCourseHomePage(SharedModuleStoreTestCase): ...@@ -71,22 +69,13 @@ class TestCourseHomePage(SharedModuleStoreTestCase):
remove_course_updates(self.course) remove_course_updates(self.course)
super(TestCourseHomePage, self).tearDown() super(TestCourseHomePage, self).tearDown()
@override_flag(UNIFIED_COURSE_EXPERIENCE_FLAG, active=True) @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
def test_unified_page(self):
"""
Verify the rendering of the unified page.
"""
url = course_home_url(self.course)
response = self.client.get(url)
self.assertContains(response, '<h2 class="hd hd-3 page-title">Test Course</h2>')
@override_flag(UNIFIED_COURSE_EXPERIENCE_FLAG, active=True)
def test_welcome_message_when_unified(self): def test_welcome_message_when_unified(self):
url = course_home_url(self.course) url = course_home_url(self.course)
response = self.client.get(url) response = self.client.get(url)
self.assertContains(response, TEST_WELCOME_MESSAGE, status_code=200) self.assertContains(response, TEST_WELCOME_MESSAGE, status_code=200)
@override_flag(UNIFIED_COURSE_EXPERIENCE_FLAG, active=False) @override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=False)
def test_welcome_message_when_not_unified(self): def test_welcome_message_when_not_unified(self):
url = course_home_url(self.course) url = course_home_url(self.course)
response = self.client.get(url) response = self.client.get(url)
...@@ -100,7 +89,7 @@ class TestCourseHomePage(SharedModuleStoreTestCase): ...@@ -100,7 +89,7 @@ class TestCourseHomePage(SharedModuleStoreTestCase):
course_home_url(self.course) course_home_url(self.course)
# Fetch the view and verify the query counts # Fetch the view and verify the query counts
with self.assertNumQueries(43): with self.assertNumQueries(42):
with check_mongo_calls(5): with check_mongo_calls(5):
url = course_home_url(self.course) url = course_home_url(self.course)
self.client.get(url) self.client.get(url)
...@@ -124,7 +124,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase): ...@@ -124,7 +124,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
course_updates_url(self.course) course_updates_url(self.course)
# Fetch the view and verify that the query counts haven't changed # Fetch the view and verify that the query counts haven't changed
with self.assertNumQueries(32): with self.assertNumQueries(33):
with check_mongo_calls(4): with check_mongo_calls(4):
url = course_updates_url(self.course) url = course_updates_url(self.course)
self.client.get(url) self.client.get(url)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment