diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index 2c5c6c82f0762155d7be5c3e1dfaab65ee47fe5f..d8a706fc4f72679aa51e66628740499cc1990590 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -9,13 +9,15 @@ from django.conf import settings
 from django.urls import reverse
 from django.utils.translation import ugettext as _
 from opaque_keys.edx.keys import CourseKey, UsageKey
+from opaque_keys.edx.locator import LibraryLocator
 from pytz import UTC
 from six import text_type
 
 from django_comment_common.models import assign_default_role
 from django_comment_common.utils import seed_permissions_roles
 from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_STUDIO_UI_FLAG
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
+from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
 from student import auth
 from student.models import CourseEnrollment
 from student.roles import CourseInstructorRole, CourseStaffRole
@@ -458,7 +460,12 @@ def get_visibility_partition_info(xblock, course=None):
         if len(partition["groups"]) > 1 or any(group["selected"] for group in partition["groups"]):
             selectable_partitions.append(partition)
 
-    if CONTENT_TYPE_GATING_STUDIO_UI_FLAG.is_enabled():
+    flag_enabled = CONTENT_TYPE_GATING_FLAG.is_enabled()
+    course_key = xblock.scope_ids.usage_id.course_key
+    is_library = isinstance(course_key, LibraryLocator)
+    if not is_library and (
+        flag_enabled or ContentTypeGatingConfig.current(course_key=course_key).studio_override_enabled
+    ):
         selectable_partitions += get_user_partition_info(xblock, schemes=["content_type_gate"], course=course)
 
     # Now add the cohort user partitions.
diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py
index 15a8a365289e9f267b8181bc24c38d8da2bef9cf..ae2be04a7ad4581b58ec706b5436be5a825863a8 100644
--- a/common/djangoapps/course_modes/views.py
+++ b/common/djangoapps/course_modes/views.py
@@ -30,7 +30,7 @@ from openedx.core.djangoapps.catalog.utils import get_currency_data
 from openedx.core.djangoapps.embargo import api as embargo_api
 from openedx.core.djangoapps.programs.utils import ProgramDataExtender, ProgramProgressMeter
 from openedx.core.djangoapps.waffle_utils import WaffleFlag, WaffleFlagNamespace
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
 from student.models import CourseEnrollment
 from util.db import outer_atomic
 from xmodule.modulestore.django import modulestore
@@ -188,7 +188,7 @@ class ChooseModeView(View):
             "error": error,
             "responsive": True,
             "nav_hidden": True,
-            "content_gating_enabled": CONTENT_TYPE_GATING_FLAG.is_enabled(),
+            "content_gating_enabled": ContentTypeGatingConfig.enabled_for_course(course_key=course_key),
         }
         context.update(
             get_experiment_user_metadata_context(
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index a17b864e3b1a9b1952ce0283703bc403f69b538c..95c90111d3397e23ecb933683ac2040dea678215 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -1309,7 +1309,7 @@ class CourseEnrollment(models.Model):
         return enrollment
 
     @classmethod
-    def get_enrollment(cls, user, course_key):
+    def get_enrollment(cls, user, course_key, select_related=None):
         """Returns a CourseEnrollment object.
 
         Args:
@@ -1324,7 +1324,10 @@ class CourseEnrollment(models.Model):
         if user.is_anonymous:
             return None
         try:
-            return cls.objects.get(
+            query = cls.objects
+            if select_related is not None:
+                query = query.select_related(*select_related)
+            return query.get(
                 user=user,
                 course_id=course_key
             )
diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py
index b88613a1871f61fc1d04f7926e595f35abb74ae1..8c64d0cdc65efc6d0f42e74f68adfff220414461 100644
--- a/common/djangoapps/student/tests/test_views.py
+++ b/common/djangoapps/student/tests/test_views.py
@@ -5,7 +5,7 @@ import itertools
 import json
 import re
 import unittest
-from datetime import timedelta
+from datetime import timedelta, date
 
 import ddt
 from completion.test_utils import submit_completions_for_testing, CompletionWaffleTestMixin
@@ -31,7 +31,7 @@ from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
 from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
 from openedx.core.djangoapps.user_authn.cookies import _get_user_info_cookie_data
 from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
 from student.helpers import DISABLE_UNENROLL_CERT_STATES
 from student.models import CourseEnrollment, UserProfile
 from student.signals import REFUND_ORDER
@@ -723,13 +723,13 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
         )
 
     @override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     def test_content_gating_course_card_changes(self):
         """
         When a course is expired, the links on the course card should be removed.
         Links will be removed from the course title, course image and button (View Course/Resume Course).
         The course card should have an access expired message.
         """
+        CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
         self.override_waffle_switch(True)
 
         course = CourseFactory.create(start=self.THREE_YEARS_AGO)
diff --git a/common/lib/xmodule/xmodule/partitions/tests/test_partitions.py b/common/lib/xmodule/xmodule/partitions/tests/test_partitions.py
index 6db47039d8cf5c8e9c475eb31147de5ed5cdb85f..a1c136aee0807a586445a6e9df3df60bf9d6ccfc 100644
--- a/common/lib/xmodule/xmodule/partitions/tests/test_partitions.py
+++ b/common/lib/xmodule/xmodule/partitions/tests/test_partitions.py
@@ -3,7 +3,8 @@ Test the partitions and partitions service
 
 """
 
-from unittest import TestCase
+from datetime import date
+from django.test import TestCase
 from mock import Mock, patch
 
 from opaque_keys.edx.locator import CourseLocator
@@ -15,6 +16,7 @@ from xmodule.partitions.partitions import (
 from xmodule.partitions.partitions_service import (
     PartitionService, get_all_partitions_for_course, FEATURES
 )
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
 
 
 class TestGroup(TestCase):
@@ -435,17 +437,11 @@ class PartitionServiceBaseClass(PartitionTestCase):
     def setUp(self):
         super(PartitionServiceBaseClass, self).setUp()
 
-        content_gating_flag_patcher = patch(
-            'openedx.features.content_type_gating.partitions.CONTENT_TYPE_GATING_FLAG.is_enabled',
-            return_value=True,
-        ).start()
-        self.addCleanup(content_gating_flag_patcher.stop)
-        content_gating_ui_flag_patcher = patch(
-            'openedx.features.content_type_gating.partitions.CONTENT_TYPE_GATING_STUDIO_UI_FLAG.is_enabled',
-            return_value=True,
-        ).start()
-        self.addCleanup(content_gating_ui_flag_patcher.stop)
-
+        ContentTypeGatingConfig.objects.create(
+            enabled=True,
+            enabled_as_of=date(2018, 1, 1),
+            studio_override_enabled=True
+        )
         self.course = Mock(id=CourseLocator('org_0', 'course_0', 'run_0'))
         self.partition_service = self._create_service("ma")
 
diff --git a/common/lib/xmodule/xmodule/tests/test_split_test_module.py b/common/lib/xmodule/xmodule/tests/test_split_test_module.py
index c9706d9f9e1b4733f83fa88ca7a84fd7227dc342..cc211a92e5e7176cc32d1d4a4272130c85ae0642 100644
--- a/common/lib/xmodule/xmodule/tests/test_split_test_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_split_test_module.py
@@ -129,16 +129,12 @@ class SplitTestModuleLMSTest(SplitTestModuleTest):
 
     def setUp(self):
         super(SplitTestModuleLMSTest, self).setUp()
+
         content_gating_flag_patcher = patch(
-            'openedx.features.content_type_gating.partitions.CONTENT_TYPE_GATING_FLAG.is_enabled',
-            return_value=False,
+            'openedx.features.content_type_gating.partitions.ContentTypeGatingConfig.current',
+            return_value=Mock(enabled=False, studio_override_enabled=False),
         ).start()
         self.addCleanup(content_gating_flag_patcher.stop)
-        content_gating_ui_flag_patcher = patch(
-            'openedx.features.content_type_gating.partitions.CONTENT_TYPE_GATING_STUDIO_UI_FLAG.is_enabled',
-            return_value=False,
-        ).start()
-        self.addCleanup(content_gating_ui_flag_patcher.stop)
 
     @ddt.data((0, 'split_test_cond0'), (1, 'split_test_cond1'))
     @ddt.unpack
diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py
index 3343931283da488d4bba0663578ff609e1a17bce..b5de22f46093196827adf3a3887b5fe0062107b4 100644
--- a/lms/djangoapps/ccx/tests/test_field_override_performance.py
+++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py
@@ -3,7 +3,7 @@
 Performance tests for field overrides.
 """
 import itertools
-from datetime import datetime
+from datetime import datetime, date
 
 import ddt
 import mock
@@ -22,7 +22,7 @@ from lms.djangoapps.ccx.tests.factories import CcxFactory
 from opaque_keys.edx.keys import CourseKey
 from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache
 from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
 from pytz import UTC
 from student.models import CourseEnrollment
 from student.tests.factories import UserFactory
@@ -198,11 +198,15 @@ class FieldOverridePerformanceTestCase(FieldOverrideTestMixin, ProceduralCourseT
         XBLOCK_FIELD_DATA_WRAPPERS=[],
         MODULESTORE_FIELD_OVERRIDE_PROVIDERS=[],
     )
-    @mock.patch.object(CONTENT_TYPE_GATING_FLAG, 'is_enabled', return_value=True)
-    def test_field_overrides(self, overrides, course_width, enable_ccx, view_as_ccx, _mock_flag):
+    def test_field_overrides(self, overrides, course_width, enable_ccx, view_as_ccx):
         """
         Test without any field overrides.
         """
+        ContentTypeGatingConfig.objects.create(
+            enabled=True,
+            enabled_as_of=date(2018, 1, 1),
+        )
+
         providers = {
             'no_overrides': (),
             'ccx': ('ccx.overrides.CustomCoursesForEdxOverrideProvider',)
@@ -240,18 +244,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
         #     # of sql queries to default,
         #     # of mongo queries,
         # )
-        ('no_overrides', 1, True, False): (24, 1),
-        ('no_overrides', 2, True, False): (24, 1),
-        ('no_overrides', 3, True, False): (24, 1),
-        ('ccx', 1, True, False): (24, 1),
-        ('ccx', 2, True, False): (24, 1),
-        ('ccx', 3, True, False): (24, 1),
-        ('no_overrides', 1, False, False): (24, 1),
-        ('no_overrides', 2, False, False): (24, 1),
-        ('no_overrides', 3, False, False): (24, 1),
-        ('ccx', 1, False, False): (24, 1),
-        ('ccx', 2, False, False): (24, 1),
-        ('ccx', 3, False, False): (24, 1),
+        ('no_overrides', 1, True, False): (26, 1),
+        ('no_overrides', 2, True, False): (26, 1),
+        ('no_overrides', 3, True, False): (26, 1),
+        ('ccx', 1, True, False): (26, 1),
+        ('ccx', 2, True, False): (26, 1),
+        ('ccx', 3, True, False): (26, 1),
+        ('no_overrides', 1, False, False): (26, 1),
+        ('no_overrides', 2, False, False): (26, 1),
+        ('no_overrides', 3, False, False): (26, 1),
+        ('ccx', 1, False, False): (26, 1),
+        ('ccx', 2, False, False): (26, 1),
+        ('ccx', 3, False, False): (26, 1),
     }
 
 
@@ -263,19 +267,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
     __test__ = True
 
     TEST_DATA = {
-        ('no_overrides', 1, True, False): (24, 3),
-        ('no_overrides', 2, True, False): (24, 3),
-        ('no_overrides', 3, True, False): (24, 3),
-        ('ccx', 1, True, False): (24, 3),
-        ('ccx', 2, True, False): (24, 3),
-        ('ccx', 3, True, False): (24, 3),
-        ('ccx', 1, True, True): (25, 3),
-        ('ccx', 2, True, True): (25, 3),
-        ('ccx', 3, True, True): (25, 3),
-        ('no_overrides', 1, False, False): (24, 3),
-        ('no_overrides', 2, False, False): (24, 3),
-        ('no_overrides', 3, False, False): (24, 3),
-        ('ccx', 1, False, False): (24, 3),
-        ('ccx', 2, False, False): (24, 3),
-        ('ccx', 3, False, False): (24, 3),
+        ('no_overrides', 1, True, False): (26, 3),
+        ('no_overrides', 2, True, False): (26, 3),
+        ('no_overrides', 3, True, False): (26, 3),
+        ('ccx', 1, True, False): (26, 3),
+        ('ccx', 2, True, False): (26, 3),
+        ('ccx', 3, True, False): (26, 3),
+        ('ccx', 1, True, True): (27, 3),
+        ('ccx', 2, True, True): (27, 3),
+        ('ccx', 3, True, True): (27, 3),
+        ('no_overrides', 1, False, False): (26, 3),
+        ('no_overrides', 2, False, False): (26, 3),
+        ('no_overrides', 3, False, False): (26, 3),
+        ('ccx', 1, False, False): (26, 3),
+        ('ccx', 2, False, False): (26, 3),
+        ('ccx', 3, False, False): (26, 3),
     }
diff --git a/lms/djangoapps/course_api/blocks/tests/test_api.py b/lms/djangoapps/course_api/blocks/tests/test_api.py
index 1c03f26d7404b095796f3ba636833d5696fe99f1..14eb1992b849647060333561145597872eb1e065 100644
--- a/lms/djangoapps/course_api/blocks/tests/test_api.py
+++ b/lms/djangoapps/course_api/blocks/tests/test_api.py
@@ -162,7 +162,7 @@ class TestGetBlocksQueryCounts(TestGetBlocksQueryCountsBase):
             self._get_blocks(
                 course,
                 expected_mongo_queries=0,
-                expected_sql_queries=6 if with_storage_backing else 5,
+                expected_sql_queries=7 if with_storage_backing else 6,
             )
 
     @ddt.data(
@@ -179,9 +179,9 @@ class TestGetBlocksQueryCounts(TestGetBlocksQueryCountsBase):
             clear_course_from_cache(course.id)
 
             if with_storage_backing:
-                num_sql_queries = 16
+                num_sql_queries = 17
             else:
-                num_sql_queries = 6
+                num_sql_queries = 7
 
             self._get_blocks(
                 course,
@@ -211,7 +211,7 @@ class TestQueryCountsWithIndividualOverrideProvider(TestGetBlocksQueryCountsBase
             self._get_blocks(
                 course,
                 expected_mongo_queries=0,
-                expected_sql_queries=7 if with_storage_backing else 6,
+                expected_sql_queries=8 if with_storage_backing else 7,
             )
 
     @ddt.data(
@@ -228,9 +228,9 @@ class TestQueryCountsWithIndividualOverrideProvider(TestGetBlocksQueryCountsBase
             clear_course_from_cache(course.id)
 
             if with_storage_backing:
-                num_sql_queries = 17
+                num_sql_queries = 18
             else:
-                num_sql_queries = 7
+                num_sql_queries = 8
 
             self._get_blocks(
                 course,
diff --git a/lms/djangoapps/course_blocks/api.py b/lms/djangoapps/course_blocks/api.py
index 27b161354e16d834a343f99c1539a98ce0ad9b2c..e7139b64b1f7d10fd67c8d5e84b7fc3a987d99ed 100644
--- a/lms/djangoapps/course_blocks/api.py
+++ b/lms/djangoapps/course_blocks/api.py
@@ -7,7 +7,6 @@ from django.conf import settings
 from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager
 from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
 from openedx.features.content_type_gating.block_transformers import ContentTypeGateTransformer
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
 
 from .transformers import (
     library_content,
@@ -41,22 +40,13 @@ def get_course_block_access_transformers(user):
             which the block structure is to be transformed.
 
     """
-    if CONTENT_TYPE_GATING_FLAG.is_enabled():
-        # [REV/Revisit] remove this duplicated code when flag is removed
-        course_block_access_transformers = [
-            library_content.ContentLibraryTransformer(),
-            start_date.StartDateTransformer(),
-            ContentTypeGateTransformer(),
-            user_partitions.UserPartitionTransformer(),
-            visibility.VisibilityTransformer(),
-        ]
-    else:
-        course_block_access_transformers = [
-            library_content.ContentLibraryTransformer(),
-            start_date.StartDateTransformer(),
-            user_partitions.UserPartitionTransformer(),
-            visibility.VisibilityTransformer(),
-        ]
+    course_block_access_transformers = [
+        library_content.ContentLibraryTransformer(),
+        start_date.StartDateTransformer(),
+        ContentTypeGateTransformer(),
+        user_partitions.UserPartitionTransformer(),
+        visibility.VisibilityTransformer(),
+    ]
 
     if has_individual_student_override_provider():
         course_block_access_transformers += [load_override_data.OverrideDataTransformer(user)]
diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py
index 6c43c1511cb5a0fc97509dd85d8b6a109332803a..376f167e54b2bd0ddbf3d4d0d36eabd15136065b 100644
--- a/lms/djangoapps/courseware/access.py
+++ b/lms/djangoapps/courseware/access.py
@@ -45,7 +45,6 @@ from mobile_api.models import IgnoreMobileAvailableFlagConfig
 from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
 from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
 from openedx.features.course_duration_limits.access import check_course_expired
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
 from student import auth
 from student.models import CourseEnrollmentAllowed
 from student.roles import (
@@ -361,7 +360,7 @@ def _has_access_course(user, action, courselike):
             else:
                 return view_with_prereqs
 
-        if CONTENT_TYPE_GATING_FLAG.is_enabled():
+        if CourseDurationLimitConfig.enabled_for_enrollment(user=user, course_key=courselike.id):
             has_not_expired = check_course_expired(user, courselike)
             if not has_not_expired:
                 staff_access = _has_staff_access_to_descriptor(user, courselike, courselike.id)
diff --git a/lms/djangoapps/courseware/tests/test_access.py b/lms/djangoapps/courseware/tests/test_access.py
index f5662c2e6aeea4b271fe3a339631d946d96e41b1..127c76be62618e99a403f7247e69cbe8a8b333d2 100644
--- a/lms/djangoapps/courseware/tests/test_access.py
+++ b/lms/djangoapps/courseware/tests/test_access.py
@@ -31,7 +31,7 @@ from lms.djangoapps.ccx.models import CustomCourseForEdX
 from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
 from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
 from openedx.core.lib.tests import attr
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
 from student.models import CourseEnrollment
 from student.roles import CourseCcxCoachRole, CourseStaffRole
 from student.tests.factories import (
@@ -827,8 +827,9 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
     )
     @ddt.unpack
     @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     def test_course_catalog_access_num_queries(self, user_attr_name, action, course_attr_name):
+        ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime.date(2018, 1, 1))
+
         course = getattr(self, course_attr_name)
 
         # get a fresh user object that won't have any cached role information
@@ -839,16 +840,23 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
             user = User.objects.get(id=user.id)
 
         if user_attr_name == 'user_staff' and action == 'see_exists':
-            # checks staff role
-            num_queries = 1
-        elif user_attr_name == 'user_normal' and action == 'see_exists':
+            # always checks staff role, and if the course has started, check the duration configuration
             if course_attr_name == 'course_started':
+                num_queries = 4
+            else:
                 num_queries = 1
+        elif user_attr_name == 'user_normal' and action == 'see_exists':
+            if course_attr_name == 'course_started':
+                num_queries = 4
             else:
                 # checks staff role and enrollment data
                 num_queries = 2
         else:
-            num_queries = 0
+            # if the course has started, check the duration configuration
+            if action == 'see_exists' and course_attr_name == 'course_started':
+                num_queries = 3
+            else:
+                num_queries = 0
 
         course_overview = CourseOverview.get_from_id(course.id)
         with self.assertNumQueries(num_queries, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
diff --git a/lms/djangoapps/courseware/tests/test_course_info.py b/lms/djangoapps/courseware/tests/test_course_info.py
index f031c5786806853652838ed9c7abdff7a32a38c9..18bbd16a8189613ff03dff2399dec3a610191bd0 100644
--- a/lms/djangoapps/courseware/tests/test_course_info.py
+++ b/lms/djangoapps/courseware/tests/test_course_info.py
@@ -2,6 +2,7 @@
 """
 Test the course_info xblock
 """
+from datetime import date
 import ddt
 import mock
 from django.conf import settings
@@ -15,7 +16,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
 from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context
 from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
 from openedx.core.lib.tests import attr
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
 from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
 from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
 from pyquery import PyQuery as pq
@@ -417,6 +418,8 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
 
     def setUp(self):
         super(SelfPacedCourseInfoTestCase, self).setUp()
+        ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
+
         self.setup_user()
 
     def fetch_course_info_with_queries(self, course, sql_queries, mongo_queries):
@@ -431,10 +434,8 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
                     resp = self.client.get(url)
         self.assertEqual(resp.status_code, 200)
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
-    def gitest_num_queries_instructor_paced(self):
-        self.fetch_course_info_with_queries(self.instructor_paced_course, 31, 3)
+    def test_num_queries_instructor_paced(self):
+        self.fetch_course_info_with_queries(self.instructor_paced_course, 38, 3)
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     def test_num_queries_self_paced(self):
-        self.fetch_course_info_with_queries(self.self_paced_course, 31, 3)
+        self.fetch_course_info_with_queries(self.self_paced_course, 38, 3)
diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py
index e6bcb6808a32b4d4bfcc5a771188a6dd56007a2b..da935fd4e781e33d86d004cf6980627aa3a6d6a7 100644
--- a/lms/djangoapps/courseware/tests/test_views.py
+++ b/lms/djangoapps/courseware/tests/test_views.py
@@ -5,7 +5,7 @@ Tests courseware views.py
 import itertools
 import json
 import unittest
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, date
 from HTMLParser import HTMLParser
 from urllib import quote, urlencode
 from uuid import uuid4
@@ -65,9 +65,12 @@ from openedx.core.djangolib.testing.utils import get_mock_request
 from openedx.core.lib.gating import api as gating_api
 from openedx.core.lib.tests import attr
 from openedx.core.lib.url_utils import quote_slashes
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
-from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG, UNIFIED_COURSE_TAB_FLAG
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
 from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
+from openedx.features.course_experience import (
+    COURSE_OUTLINE_PAGE_FLAG,
+    UNIFIED_COURSE_TAB_FLAG,
+)
 from student.models import CourseEnrollment
 from student.tests.factories import TEST_PASSWORD, AdminFactory, CourseEnrollmentFactory, UserFactory
 from util.tests.test_date_utils import fake_pgettext, fake_ugettext
@@ -206,13 +209,13 @@ class IndexQueryTestCase(ModuleStoreTestCase):
     CREATE_USER = False
     NUM_PROBLEMS = 20
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     @ddt.data(
         (ModuleStoreEnum.Type.mongo, 10, 162),
-        (ModuleStoreEnum.Type.split, 4, 158),
+        (ModuleStoreEnum.Type.split, 4, 160),
     )
     @ddt.unpack
     def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
+        ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
         with self.store.default_store(store_type):
             course = CourseFactory.create()
             with self.store.bulk_operations(course.id):
@@ -1432,26 +1435,26 @@ class ProgressPageTests(ProgressPageBaseTests):
                 resp = self._get_progress_page()
                 self.assertContains(resp, u"Download Your Certificate")
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     @ddt.data(
-        (True, 40),
-        (False, 39)
+        (True, 46),
+        (False, 45)
     )
     @ddt.unpack
     def test_progress_queries_paced_courses(self, self_paced, query_count):
         """Test that query counts remain the same for self-paced and instructor-paced courses."""
+        ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
         self.setup_course(self_paced=self_paced)
         with self.assertNumQueries(query_count, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(1):
             self._get_progress_page()
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     @patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
     @ddt.data(
-        (False, 47, 30),
-        (True, 39, 26)
+        (False, 53, 33),
+        (True, 45, 29)
     )
     @ddt.unpack
     def test_progress_queries(self, enable_waffle, initial, subsequent):
+        ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
         self.setup_course()
         with grades_waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=enable_waffle):
             with self.assertNumQueries(
@@ -2703,12 +2706,12 @@ class TestIndexViewWithCourseDurationLimits(ModuleStoreTestCase):
 
         CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     def test_index_with_course_duration_limits(self):
         """
         Test that the courseware contains the course expiration banner
         when course_duration_limits are enabled.
         """
+        CourseDurationLimitConfig.objects.create(enabled=True)
         self.assertTrue(self.client.login(username=self.user.username, password='test'))
         response = self.client.get(
             reverse(
@@ -2723,12 +2726,12 @@ class TestIndexViewWithCourseDurationLimits(ModuleStoreTestCase):
         bannerText = get_expiration_banner_text(self.user, self.course)
         self.assertContains(response, bannerText, html=True)
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, False)
     def test_index_without_course_duration_limits(self):
         """
         Test that the courseware does not contain the course expiration banner
         when course_duration_limits are disabled.
         """
+        CourseDurationLimitConfig.objects.create(enabled=False)
         self.assertTrue(self.client.login(username=self.user.username, password='test'))
         response = self.client.get(
             reverse(
diff --git a/lms/djangoapps/discussion/tests/test_views.py b/lms/djangoapps/discussion/tests/test_views.py
index 04c09a77f6230652a2af4b44429861c7ad2e89b2..0e22f02d2ef74ee8a31242d91347f050e965d5ee 100644
--- a/lms/djangoapps/discussion/tests/test_views.py
+++ b/lms/djangoapps/discussion/tests/test_views.py
@@ -1,6 +1,6 @@
 import json
 import logging
-from datetime import datetime
+from datetime import datetime, date
 
 import ddt
 from django.urls import reverse
@@ -46,7 +46,7 @@ from openedx.core.djangoapps.course_groups.tests.helpers import config_course_co
 from openedx.core.djangoapps.course_groups.tests.test_views import CohortViewsTestCase
 from openedx.core.djangoapps.util.testing import ContentGroupTestCase
 from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
 from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
 from student.roles import CourseStaffRole, UserBasedRole
 from student.tests.factories import CourseEnrollmentFactory, UserFactory
@@ -424,7 +424,6 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
     def setUp(self):
         super(SingleThreadQueryCountTestCase, self).setUp()
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     @ddt.data(
         # Old mongo with cache. There is an additional SQL query for old mongo
         # because the first time that disabled_xblocks is queried is in call_single_thread,
@@ -432,18 +431,18 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
         # course is outside the context manager that is verifying the number of queries,
         # and with split mongo, that method ends up querying disabled_xblocks (which is then
         # cached and hence not queried as part of call_single_thread).
-        (ModuleStoreEnum.Type.mongo, False, 1, 5, 2, 19, 7),
-        (ModuleStoreEnum.Type.mongo, False, 50, 5, 2, 19, 7),
+        (ModuleStoreEnum.Type.mongo, False, 1, 5, 2, 21, 6),
+        (ModuleStoreEnum.Type.mongo, False, 50, 5, 2, 21, 6),
         # split mongo: 3 queries, regardless of thread response size.
-        (ModuleStoreEnum.Type.split, False, 1, 3, 3, 19, 7),
-        (ModuleStoreEnum.Type.split, False, 50, 3, 3, 19, 7),
+        (ModuleStoreEnum.Type.split, False, 1, 3, 3, 21, 6),
+        (ModuleStoreEnum.Type.split, False, 50, 3, 3, 21, 6),
 
         # Enabling Enterprise integration should have no effect on the number of mongo queries made.
-        (ModuleStoreEnum.Type.mongo, True, 1, 5, 2, 19, 7),
-        (ModuleStoreEnum.Type.mongo, True, 50, 5, 2, 19, 7),
+        (ModuleStoreEnum.Type.mongo, True, 1, 5, 2, 21, 6),
+        (ModuleStoreEnum.Type.mongo, True, 50, 5, 2, 21, 6),
         # split mongo: 3 queries, regardless of thread response size.
-        (ModuleStoreEnum.Type.split, True, 1, 3, 3, 19, 7),
-        (ModuleStoreEnum.Type.split, True, 50, 3, 3, 19, 7),
+        (ModuleStoreEnum.Type.split, True, 1, 3, 3, 21, 6),
+        (ModuleStoreEnum.Type.split, True, 50, 3, 3, 21, 6),
     )
     @ddt.unpack
     def test_number_of_mongo_queries(
@@ -457,6 +456,7 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
             num_cached_sql_queries,
             mock_request
     ):
+        ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
         with modulestore().default_store(default_store):
             course = CourseFactory.create(discussion_topics={'dummy discussion': {'id': 'dummy_discussion_id'}})
 
diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py
index b6a8f235b91b5bea00b1cb2a23f2275fc7c0930b..a60b37edf10bdf0b0667b2e5dd0c65afeebb1692 100644
--- a/lms/djangoapps/django_comment_client/base/tests.py
+++ b/lms/djangoapps/django_comment_client/base/tests.py
@@ -39,7 +39,6 @@ from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted
 from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
 from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
 from openedx.core.lib.tests import attr
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
 from student.roles import CourseStaffRole, UserBasedRole
 from student.tests.factories import CourseAccessRoleFactory, CourseEnrollmentFactory, UserFactory
 from util.testing import UrlResetMixin
@@ -403,20 +402,18 @@ class ViewsQueryCountTestCase(
                         func(self, *args, **kwargs)
         return inner
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     @ddt.data(
-        (ModuleStoreEnum.Type.mongo, 3, 4, 38),
-        (ModuleStoreEnum.Type.split, 3, 13, 38),
+        (ModuleStoreEnum.Type.mongo, 3, 4, 39),
+        (ModuleStoreEnum.Type.split, 3, 13, 39),
     )
     @ddt.unpack
     @count_queries
     def test_create_thread(self, mock_request):
         self.create_thread_helper(mock_request)
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     @ddt.data(
-        (ModuleStoreEnum.Type.mongo, 3, 3, 34),
-        (ModuleStoreEnum.Type.split, 3, 10, 34),
+        (ModuleStoreEnum.Type.mongo, 3, 3, 35),
+        (ModuleStoreEnum.Type.split, 3, 10, 35),
     )
     @ddt.unpack
     @count_queries
diff --git a/lms/djangoapps/grades/tests/test_course_grade_factory.py b/lms/djangoapps/grades/tests/test_course_grade_factory.py
index 6aa15af83436556d52642a72a6613bf11bbec35c..b365dd4d4f1a55242198638ea7d04037221500fb 100644
--- a/lms/djangoapps/grades/tests/test_course_grade_factory.py
+++ b/lms/djangoapps/grades/tests/test_course_grade_factory.py
@@ -92,35 +92,35 @@ class TestCourseGradeFactory(GradeTestBase):
                 [self.sequence.display_name, self.sequence2.display_name]
             )
 
-        with self.assertNumQueries(2), mock_get_score(1, 2):
+        with self.assertNumQueries(3), mock_get_score(1, 2):
             _assert_read(expected_pass=False, expected_percent=0)  # start off with grade of 0
 
-        num_queries = 41
+        num_queries = 42
         with self.assertNumQueries(num_queries), mock_get_score(1, 2):
             grade_factory.update(self.request.user, self.course, force_update_subsections=True)
 
-        with self.assertNumQueries(2):
+        with self.assertNumQueries(3):
             _assert_read(expected_pass=True, expected_percent=0.5)  # updated to grade of .5
 
-        num_queries = 6
+        num_queries = 7
         with self.assertNumQueries(num_queries), mock_get_score(1, 4):
             grade_factory.update(self.request.user, self.course, force_update_subsections=False)
 
-        with self.assertNumQueries(2):
+        with self.assertNumQueries(3):
             _assert_read(expected_pass=True, expected_percent=0.5)  # NOT updated to grade of .25
 
-        num_queries = 20
+        num_queries = 21
         with self.assertNumQueries(num_queries), mock_get_score(2, 2):
             grade_factory.update(self.request.user, self.course, force_update_subsections=True)
 
-        with self.assertNumQueries(2):
+        with self.assertNumQueries(3):
             _assert_read(expected_pass=True, expected_percent=1.0)  # updated to grade of 1.0
 
-        num_queries = 23
+        num_queries = 24
         with self.assertNumQueries(num_queries), mock_get_score(0, 0):  # the subsection now is worth zero
             grade_factory.update(self.request.user, self.course, force_update_subsections=True)
 
-        with self.assertNumQueries(2):
+        with self.assertNumQueries(3):
             _assert_read(expected_pass=False, expected_percent=0.0)  # updated to grade of 0.0
 
     @patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
@@ -311,7 +311,7 @@ class TestGradeIteration(SharedModuleStoreTestCase):
             else mock_course_grade.return_value
             for student in self.students
         ]
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(8):
             all_course_grades, all_errors = self._course_grades_and_errors_for(self.course, self.students)
         self.assertEqual(
             {student: text_type(all_errors[student]) for student in all_errors},
diff --git a/lms/djangoapps/grades/tests/test_tasks.py b/lms/djangoapps/grades/tests/test_tasks.py
index 5400afa90ccbc7afcd34213d327e3514fe99443f..18cf2da5f633032350898c2bf4fa7ffa133746a7 100644
--- a/lms/djangoapps/grades/tests/test_tasks.py
+++ b/lms/djangoapps/grades/tests/test_tasks.py
@@ -176,10 +176,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
             self.assertEquals(mock_block_structure_create.call_count, 1)
 
     @ddt.data(
-        (ModuleStoreEnum.Type.mongo, 1, 31, True),
-        (ModuleStoreEnum.Type.mongo, 1, 31, False),
-        (ModuleStoreEnum.Type.split, 3, 31, True),
-        (ModuleStoreEnum.Type.split, 3, 31, False),
+        (ModuleStoreEnum.Type.mongo, 1, 32, True),
+        (ModuleStoreEnum.Type.mongo, 1, 32, False),
+        (ModuleStoreEnum.Type.split, 3, 32, True),
+        (ModuleStoreEnum.Type.split, 3, 32, False),
     )
     @ddt.unpack
     def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections):
@@ -191,8 +191,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
                     self._apply_recalculate_subsection_grade()
 
     @ddt.data(
-        (ModuleStoreEnum.Type.mongo, 1, 31),
-        (ModuleStoreEnum.Type.split, 3, 31),
+        (ModuleStoreEnum.Type.mongo, 1, 32),
+        (ModuleStoreEnum.Type.split, 3, 32),
     )
     @ddt.unpack
     def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls):
@@ -237,8 +237,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
         )
 
     @ddt.data(
-        (ModuleStoreEnum.Type.mongo, 1, 15),
-        (ModuleStoreEnum.Type.split, 3, 15),
+        (ModuleStoreEnum.Type.mongo, 1, 16),
+        (ModuleStoreEnum.Type.split, 3, 16),
     )
     @ddt.unpack
     def test_persistent_grades_not_enabled_on_course(self, default_store, num_mongo_queries, num_sql_queries):
@@ -252,8 +252,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
             self.assertEqual(len(PersistentSubsectionGrade.bulk_read_grades(self.user.id, self.course.id)), 0)
 
     @ddt.data(
-        (ModuleStoreEnum.Type.mongo, 1, 32),
-        (ModuleStoreEnum.Type.split, 3, 32),
+        (ModuleStoreEnum.Type.mongo, 1, 33),
+        (ModuleStoreEnum.Type.split, 3, 33),
     )
     @ddt.unpack
     def test_persistent_grades_enabled_on_course(self, default_store, num_mongo_queries, num_sql_queries):
diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py
index de07b194e4ab218960e405c090f169e911bae891..63b6448761a8db6649d1f7cee029e1641b78520a 100644
--- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py
@@ -413,7 +413,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
 
         RequestCache.clear_all_namespaces()
 
-        expected_query_count = 45
+        expected_query_count = 47
         with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'):
             with check_mongo_calls(mongo_count):
                 with self.assertNumQueries(expected_query_count):
diff --git a/lms/djangoapps/mobile_api/users/tests.py b/lms/djangoapps/mobile_api/users/tests.py
index dd16ade8e88bddbc887beed0c62520e9af26a8dc..1f4989861f696ecba71ee49dc83adb868e885e6e 100644
--- a/lms/djangoapps/mobile_api/users/tests.py
+++ b/lms/djangoapps/mobile_api/users/tests.py
@@ -29,8 +29,7 @@ from mobile_api.utils import API_V05, API_V1
 from openedx.core.lib.courses import course_image_url
 from openedx.core.lib.tests import attr
 from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
-from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
 from student.models import CourseEnrollment
 from student.tests.factories import CourseEnrollmentFactory
 from util.milestones_helpers import set_prerequisite_courses
@@ -289,12 +288,12 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
         (API_V1, False, 1),
     )
     @ddt.unpack
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     def test_enrollment_with_gating(self, api_version, expired, num_courses_returned):
         '''
         Test that expired courses are only returned in v1 of API
         when waffle flag enabled, and un-expired courses always returned
         '''
+        CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime.date(2018, 1, 1))
         courses = self._get_enrollment_data(api_version, expired)
         self._assert_enrollment_results(api_version, courses, num_courses_returned)
 
@@ -305,12 +304,12 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
         (API_V1, False, 1),
     )
     @ddt.unpack
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, False)
     def test_enrollment_no_gating(self, api_version, expired, num_courses_returned):
         '''
         Test that expired and non-expired courses returned if waffle flag is disabled
         regarless of version of API
         '''
+        CourseDurationLimitConfig.objects.create(enabled=False)
         courses = self._get_enrollment_data(api_version, expired)
         self._assert_enrollment_results(api_version, courses, num_courses_returned)
 
diff --git a/lms/djangoapps/mobile_api/users/views.py b/lms/djangoapps/mobile_api/users/views.py
index 1898d68dc25b59524c7693d58e9e720cd7ba25cd..841752889529d815f7a9847c78eaa58cea2da245 100644
--- a/lms/djangoapps/mobile_api/users/views.py
+++ b/lms/djangoapps/mobile_api/users/views.py
@@ -22,7 +22,6 @@ from experiments.models import ExperimentData, ExperimentKeyValue
 from lms.djangoapps.courseware.access_utils import ACCESS_GRANTED
 from mobile_api.utils import API_V05
 from openedx.features.course_duration_limits.access import check_course_expired
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
 from student.models import CourseEnrollment, User
 from xmodule.modulestore.django import modulestore
 from xmodule.modulestore.exceptions import ItemNotFoundError
diff --git a/openedx/core/djangoapps/config_model_utils/admin.py b/openedx/core/djangoapps/config_model_utils/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d4b7e530f6005cb0987ca044d7154dd88adfc27
--- /dev/null
+++ b/openedx/core/djangoapps/config_model_utils/admin.py
@@ -0,0 +1,42 @@
+"""
+Convenience classes for defining StackedConfigModel Admin pages.
+"""
+
+from django import forms
+
+from opaque_keys.edx.keys import CourseKey
+
+from config_models.admin import ConfigurationModelAdmin
+
+
+class CourseOverviewField(forms.ModelChoiceField):
+    def to_python(self, value):
+        if value in self.empty_values:
+            return None
+        return super(CourseOverviewField, self).to_python(CourseKey.from_string(value))
+
+
+class StackedConfigModelAdminForm(forms.ModelForm):
+    class Meta:
+        field_classes = {
+            'course': CourseOverviewField
+        }
+
+
+class StackedConfigModelAdmin(ConfigurationModelAdmin):
+    """
+    A specialized ConfigurationModel ModelAdmin for StackedConfigModels.
+    """
+    form = StackedConfigModelAdminForm
+
+    def get_fields(self, request, obj=None):
+        fields = super(StackedConfigModelAdmin, self).get_fields(request, obj)
+        return list(self.model.KEY_FIELDS) + [field for field in fields if field not in self.model.KEY_FIELDS]
+
+    def get_displayable_field_names(self):
+        """
+        Return all field names, excluding reverse foreign key relationships.
+        """
+        names = super(StackedConfigModelAdmin, self).get_displayable_field_names()
+        fixed_names = ['id', 'change_date', 'changed_by'] + list(self.model.KEY_FIELDS)
+        return fixed_names + [name for name in names if name not in fixed_names]
diff --git a/openedx/core/djangoapps/config_model_utils/models.py b/openedx/core/djangoapps/config_model_utils/models.py
index d8d1ab797a68a8cc0dfaa051cc9752bed249eab8..eb3e850388c51e20374b103e177c47cab4c40b09 100644
--- a/openedx/core/djangoapps/config_model_utils/models.py
+++ b/openedx/core/djangoapps/config_model_utils/models.py
@@ -11,13 +11,14 @@ from collections import namedtuple
 
 from django.conf import settings
 from django.db import models
+from django.db.models import Q, F
 from django.contrib.sites.models import Site
 from django.contrib.sites.requests import RequestSite
 from django.core.exceptions import ValidationError
 from django.utils.translation import ugettext_lazy as _
 import crum
 
-from config_models.models import ConfigurationModel
+from config_models.models import ConfigurationModel, cache
 from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
 from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
 
@@ -34,43 +35,17 @@ class StackedConfigurationModel(ConfigurationModel):
     STACKABLE_FIELDS = ('enabled',)
 
     enabled = models.NullBooleanField(default=None, verbose_name=_("Enabled"))
-    site = models.ForeignKey(Site, on_delete=models.CASCADE, null=True)
-    org = models.CharField(max_length=255, db_index=True, null=True)
+    site = models.ForeignKey(Site, on_delete=models.CASCADE, null=True, blank=True)
+    org = models.CharField(max_length=255, db_index=True, null=True, blank=True)
     course = models.ForeignKey(
         CourseOverview,
         on_delete=models.DO_NOTHING,
         null=True,
+        blank=True,
     )
 
     @classmethod
-    def attribute_tuple(cls):
-        """
-        Returns a namedtuple with all attributes that can be overridden on this config model.
-
-        For example, if MyStackedConfig.STACKABLE_FIELDS = ('enabled', 'enabled_as_of', 'studio_enabled'),
-        then:
-
-            # These lines are the same
-            MyStackedConfig.attribute_tuple()
-            namedtuple('MyStackedConfigValues', ('enabled', 'enabled_as_of', 'studio_enabled'))
-
-            # attribute_tuple() behavior
-            MyStackedConfigValues = MyStackedConfig.attribute_tuple()
-            MyStackedConfigValues(True, '10/1/18', False).enabled  # True
-            MyStackedConfigValues(True, '10/1/18', False).enabled_as_of  # '10/1/18'
-            MyStackedConfigValues(True, '10/1/18', False).studio_enabled  # False
-        """
-        if hasattr(cls, '_attribute_tuple'):
-            return cls._attribute_tuple
-
-        cls._attribute_tuple = namedtuple(
-            '{}Values'.format(cls.__name__),
-            cls.STACKABLE_FIELDS
-        )
-        return cls._attribute_tuple
-
-    @classmethod
-    def current(cls, site=None, org=None, course=None):  # pylint: disable=arguments-differ
+    def current(cls, site=None, org=None, course_key=None):  # pylint: disable=arguments-differ
         """
         Return the current overridden configuration at the specified level.
 
@@ -112,12 +87,18 @@ class StackedConfigurationModel(ConfigurationModel):
             specified down to the level of the supplied argument (or global values if
             no arguments are supplied).
         """
+        cache_key_name = cls.cache_key_name(site, org, course_key=course_key)
+        cached = cache.get(cache_key_name)
+
+        if cached is not None:
+            return cached
+
         # Raise an error if more than one of site/org/course are specified simultaneously.
-        if len([arg for arg in [site, org, course] if arg is not None]) > 1:
+        if len([arg for arg in [site, org, course_key] if arg is not None]) > 1:
             raise ValueError("Only one of site, org, and course can be specified")
 
-        if org is None and course is not None:
-            org = cls._org_from_course(course)
+        if org is None and course_key is not None:
+            org = cls._org_from_course_key(course_key)
 
         if site is None and org is not None:
             site = cls._site_from_org(org)
@@ -130,40 +111,62 @@ class StackedConfigurationModel(ConfigurationModel):
 
         values = field_defaults.copy()
 
-        global_current = super(StackedConfigurationModel, cls).current(None, None, None)
-        for field in stackable_fields:
-            values[field.name] = field.value_from_object(global_current)
+        global_override_q = Q(site=None, org=None, course_id=None)
+        site_override_q = Q(site=site, org=None, course_id=None)
+        org_override_q = Q(site=None, org=org, course_id=None)
+        course_override_q = Q(site=None, org=None, course_id=course_key)
+
+        overrides = cls.objects.current_set().filter(
+            global_override_q |
+            site_override_q |
+            org_override_q |
+            course_override_q
+        ).order_by(
+            # Sort nulls first, and in reverse specificity order
+            # so that the overrides are in the order of general to specific.
+            #
+            # Site | Org  | Course
+            # --------------------
+            # Null | Null | Null
+            # site | Null | Null
+            # Null | org  | Null
+            # Null | Null | Course
+            F('course').desc(nulls_first=True),
+            F('org').desc(nulls_first=True),
+            F('site').desc(nulls_first=True),
+        )
 
-        def _override_fields_with_level(level_config):
+        for override in overrides:
             for field in stackable_fields:
-                value = field.value_from_object(level_config)
+                value = field.value_from_object(override)
                 if value != field_defaults[field.name]:
                     values[field.name] = value
 
-        if site is not None:
-            _override_fields_with_level(
-                super(StackedConfigurationModel, cls).current(site, None, None)
-            )
-
-        if org is not None:
-            _override_fields_with_level(
-                super(StackedConfigurationModel, cls).current(None, org, None)
-            )
+        current = cls(**values)
+        cache.set(cache_key_name, current, cls.cache_timeout)
+        return current
 
+    @classmethod
+    def cache_key_name(cls, site, org, course=None, course_key=None):  # pylint: disable=arguments-differ
+        if course is not None and course_key is not None:
+            raise ValueError("Only one of course and course_key can be specified at a time")
         if course is not None:
-            _override_fields_with_level(
-                super(StackedConfigurationModel, cls).current(None, None, course)
-            )
+            course_key = course
+
+        if site is None:
+            site_id = None
+        else:
+            site_id = site.id
 
-        return cls.attribute_tuple()(**values)
+        return super(StackedConfigurationModel, cls).cache_key_name(site_id, org, course_key)
 
     @classmethod
-    def _org_from_course(cls, course_key):
+    def _org_from_course_key(cls, course_key):
         return course_key.org
 
     @classmethod
     def _site_from_org(cls, org):
-        configuration = SiteConfiguration.get_configuration_for_org(org)
+        configuration = SiteConfiguration.get_configuration_for_org(org, select_related=['site'])
         if configuration is None:
             try:
                 return Site.objects.get(id=settings.SITE_ID)
diff --git a/openedx/core/djangoapps/site_configuration/models.py b/openedx/core/djangoapps/site_configuration/models.py
index 9fce936698f877f5a10462c5fcd73fdf836bdf2d..ff1471115513c218f4484c2f597e1e291fd0ead3 100644
--- a/openedx/core/djangoapps/site_configuration/models.py
+++ b/openedx/core/djangoapps/site_configuration/models.py
@@ -61,15 +61,19 @@ class SiteConfiguration(models.Model):
         return default
 
     @classmethod
-    def get_configuration_for_org(cls, org):
+    def get_configuration_for_org(cls, org, select_related=None):
         """
         This returns a SiteConfiguration object which has an org_filter that matches
         the supplied org
 
         Args:
             org (str): Org to use to filter SiteConfigurations
+            select_related (list or None): A list of values to pass as arguments to select_related
         """
-        for configuration in cls.objects.filter(values__contains=org, enabled=True).all():
+        query = cls.objects.filter(values__contains=org, enabled=True).all()
+        if select_related is not None:
+            query = query.select_related(*select_related)
+        for configuration in query:
             course_org_filter = configuration.get_value('course_org_filter', [])
             # The value of 'course_org_filter' can be configured as a string representing
             # a single organization or a list of strings representing multiple organizations.
diff --git a/openedx/features/content_type_gating/admin.py b/openedx/features/content_type_gating/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb27e9aaea7bcf0d5de8d13ffa52a84219289366
--- /dev/null
+++ b/openedx/features/content_type_gating/admin.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+"""
+Django Admin pages for ContentTypeGatingConfig.
+"""
+
+from __future__ import unicode_literals
+
+from django.contrib import admin
+from django.utils.translation import ugettext_lazy as _
+
+from openedx.core.djangoapps.config_model_utils.admin import StackedConfigModelAdmin
+from .models import ContentTypeGatingConfig
+
+
+class ContentTypeGatingConfigAdmin(StackedConfigModelAdmin):
+    fieldsets = (
+        ('Context', {
+            'fields': ('site', 'org', 'course'),
+            'description': _(
+                'These define the context to enable course duration limits on. '
+                'If no values are set, then the configuration applies globally. '
+                'If a single value is set, then the configuration applies to all courses '
+                'within that context. At most one value can be set at a time.<br>'
+                'If multiple contexts apply to a course (for example, if configuration '
+                'is specified for the course specifically, and for the org that the course '
+                'is in, then the more specific context overrides the more general context.'
+            ),
+        }),
+        ('Configuration', {
+            'fields': ('enabled', 'enabled_as_of', 'studio_override_enabled'),
+            'description': _(
+                'If any of these values is left empty or "Unknown", then their value '
+                'at runtime will be retrieved from the next most specific context that applies. '
+                'For example, if "Enabled" is left as "Unknown" in the course context, then that '
+                'course will be Enabled only if the org that it is in is Enabled.'
+            ),
+        })
+    )
+
+admin.site.register(ContentTypeGatingConfig, ContentTypeGatingConfigAdmin)
diff --git a/openedx/features/content_type_gating/block_transformers.py b/openedx/features/content_type_gating/block_transformers.py
index b4e5df133f9b2ceb96262e53d2b8967b1439d707..daf1f3f96468908557c05120e0e146e997398c2b 100644
--- a/openedx/features/content_type_gating/block_transformers.py
+++ b/openedx/features/content_type_gating/block_transformers.py
@@ -8,6 +8,7 @@ from openedx.core.djangoapps.content.block_structure.transformer import (
     BlockStructureTransformer,
 )
 from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
 
 
 class ContentTypeGateTransformer(BlockStructureTransformer):
@@ -35,6 +36,12 @@ class ContentTypeGateTransformer(BlockStructureTransformer):
         block_structure.request_xblock_fields('group_access', 'graded', 'has_score', 'weight')
 
     def transform(self, usage_info, block_structure):
+        if not ContentTypeGatingConfig.enabled_for_enrollment(
+            user=usage_info.user,
+            course_key=usage_info.course_key,
+        ):
+            return
+
         for block_key in block_structure.topological_traversal():
             graded = block_structure.get_xblock_field(block_key, 'graded')
             has_score = block_structure.get_xblock_field(block_key, 'has_score')
diff --git a/openedx/features/content_type_gating/field_override.py b/openedx/features/content_type_gating/field_override.py
index 42d22a811a58748f53d5f45f00672ead69650786..76b01b4fae35dd6e7cf50626f4c53d0f898d01b4 100644
--- a/openedx/features/content_type_gating/field_override.py
+++ b/openedx/features/content_type_gating/field_override.py
@@ -6,9 +6,7 @@ from django.conf import settings
 
 from lms.djangoapps.courseware.field_overrides import FieldOverrideProvider
 from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
-from openedx.features.course_duration_limits.config import (
-    CONTENT_TYPE_GATING_FLAG,
-)
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
 
 
 class ContentTypeGatingFieldOverride(FieldOverrideProvider):
@@ -18,9 +16,6 @@ class ContentTypeGatingFieldOverride(FieldOverrideProvider):
     graded content to only be accessible to the Full Access group
     """
     def get(self, block, name, default):
-        if not CONTENT_TYPE_GATING_FLAG.is_enabled():
-            return default
-
         if name != 'group_access':
             return default
 
@@ -61,4 +56,4 @@ class ContentTypeGatingFieldOverride(FieldOverrideProvider):
     @classmethod
     def enabled_for(cls, course):
         """This simple override provider is always enabled"""
-        return True
+        return ContentTypeGatingConfig.enabled_for_course(course_key=course.scope_ids.usage_id.course_key)
diff --git a/openedx/features/content_type_gating/migrations/0001_initial.py b/openedx/features/content_type_gating/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..ab0e3f13df426cc907fb74d09a24d96351ebe641
--- /dev/null
+++ b/openedx/features/content_type_gating/migrations/0001_initial.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.16 on 2018-11-08 19:43
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('course_overviews', '0014_courseoverview_certificate_available_date'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('sites', '0002_alter_domain_unique'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ContentTypeGatingConfig',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
+                ('enabled', models.NullBooleanField(default=None, verbose_name='Enabled')),
+                ('org', models.CharField(blank=True, db_index=True, max_length=255, null=True)),
+                ('enabled_as_of', models.DateField(blank=True, default=None, null=True, verbose_name='Enabled As Of')),
+                ('studio_override_enabled', models.NullBooleanField(default=None, verbose_name='Studio Override Enabled')),
+                ('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')),
+                ('course', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='course_overviews.CourseOverview')),
+                ('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+    ]
diff --git a/openedx/features/content_type_gating/migrations/0002_auto_20181119_0959.py b/openedx/features/content_type_gating/migrations/0002_auto_20181119_0959.py
new file mode 100644
index 0000000000000000000000000000000000000000..a71bec3f612575dbe94e23684d6ad94087d290fd
--- /dev/null
+++ b/openedx/features/content_type_gating/migrations/0002_auto_20181119_0959.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.16 on 2018-11-19 14:59
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('content_type_gating', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='contenttypegatingconfig',
+            name='enabled_as_of',
+            field=models.DateField(blank=True, default=None, help_text='If the configuration is Enabled, then all enrollments created after this date (UTC) will be affected.', null=True, verbose_name='Enabled As Of'),
+        ),
+        migrations.AlterField(
+            model_name='contenttypegatingconfig',
+            name='studio_override_enabled',
+            field=models.NullBooleanField(default=None, help_text='Allow Feature Based Enrollment visibility to be overriden on a per-component basis in Studio.', verbose_name='Studio Override Enabled'),
+        ),
+    ]
diff --git a/openedx/features/content_type_gating/models.py b/openedx/features/content_type_gating/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..f69d1fc0cc56a67cba7128d017d7167dfbe57fc5
--- /dev/null
+++ b/openedx/features/content_type_gating/models.py
@@ -0,0 +1,132 @@
+"""
+Content Type Gating Configuration Models
+"""
+
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from datetime import datetime
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
+from django.utils.translation import ugettext_lazy as _
+
+from student.models import CourseEnrollment
+from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel
+from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+
+
+@python_2_unicode_compatible
+class ContentTypeGatingConfig(StackedConfigurationModel):
+    """
+    A ConfigurationModel used to manage configuration for Content Type Gating (Feature Based Enrollments).
+    """
+
+    STACKABLE_FIELDS = ('enabled', 'enabled_as_of', 'studio_override_enabled')
+
+    enabled_as_of = models.DateField(
+        default=None,
+        null=True,
+        verbose_name=_('Enabled As Of'),
+        blank=True,
+        help_text=_(
+            'If the configuration is Enabled, then all enrollments '
+            'created after this date (UTC) will be affected.'
+        )
+    )
+    studio_override_enabled = models.NullBooleanField(
+        default=None,
+        verbose_name=_('Studio Override Enabled'),
+        blank=True,
+        help_text=_(
+            'Allow Feature Based Enrollment visibility to be overriden '
+            'on a per-component basis in Studio.'
+        )
+    )
+
+    @classmethod
+    def enabled_for_enrollment(cls, enrollment=None, user=None, course_key=None):
+        """
+        Return whether Content Type Gating is enabled for this enrollment.
+
+        Content Type Gating is enabled for an enrollment if it is enabled for
+        the course being enrolled in (either specifically, or via a containing context,
+        such as the org, site, or globally), and if the configuration is specified to be
+        ``enabled_as_of`` before the enrollment was created.
+
+        Only one of enrollment and (user, course_key) may be specified at a time.
+
+        Arguments:
+            enrollment: The enrollment being queried.
+            user: The user being queried.
+            course_key: The CourseKey of the course being queried.
+        """
+        if CONTENT_TYPE_GATING_FLAG.is_enabled():
+            return True
+
+        if enrollment is not None and (user is not None or course_key is not None):
+            raise ValueError('Specify enrollment or user/course_key, but not both')
+
+        if enrollment is None and (user is None or course_key is None):
+            raise ValueError('Both user and course_key must be specified if no enrollment is provided')
+
+        if enrollment is None and user is None and course_key is None:
+            raise ValueError('At least one of enrollment or user and course_key must be specified')
+
+        if course_key is None:
+            course_key = enrollment.course_id
+
+        if enrollment is None:
+            enrollment = CourseEnrollment.get_enrollment(user, course_key)
+
+        # enrollment might be None if the user isn't enrolled. In that case,
+        # return enablement as if the user enrolled today
+        if enrollment is None:
+            return cls.enabled_for_course(course_key=course_key, target_date=datetime.utcnow().date())
+        else:
+            current_config = cls.current(course_key=enrollment.course_id)
+            return current_config.enabled_as_of_date(target_date=enrollment.created.date())
+
+    @classmethod
+    def enabled_for_course(cls, course_key, target_date=None):
+        """
+        Return whether Content Type Gating is enabled for this course as of a particular date.
+
+        Content Type Gating is enabled for a course on a date if it is enabled either specifically,
+        or via a containing context, such as the org, site, or globally, and if the configuration
+        is specified to be ``enabled_as_of`` before ``target_date``.
+
+        Only one of enrollment and (user, course_key) may be specified at a time.
+
+        Arguments:
+            course_key: The CourseKey of the course being queried.
+            target_date: The date to checked enablement as of. Defaults to the current date.
+        """
+        if CONTENT_TYPE_GATING_FLAG.is_enabled():
+            return True
+
+        if target_date is None:
+            target_date = datetime.utcnow().date()
+
+        current_config = cls.current(course_key=course_key)
+        return current_config.enabled_as_of_date(target_date=target_date)
+
+    def clean(self):
+        if self.enabled and self.enabled_as_of is None:
+            raise ValidationError({'enabled_as_of': _('enabled_as_of must be set when enabled is True')})
+
+    def enabled_as_of_date(self, target_date):
+        """
+        Return whether this Content Type Gating configuration context is enabled as of a date.
+
+        Arguments:
+            target_date (:class:`datetime.date`): The date that ``enabled_as_of`` must be equal to or before
+        """
+        if CONTENT_TYPE_GATING_FLAG.is_enabled():
+            return True
+
+        # Explicitly cast this to bool, so that when self.enabled is None the method doesn't return None
+        return bool(self.enabled and self.enabled_as_of <= target_date)
+
+    def __str__(self):
+        return "ContentTypeGatingConfig(enabled={!r}, enabled_as_of={!r}, studio_override_enabled={!r})"
diff --git a/openedx/features/content_type_gating/partitions.py b/openedx/features/content_type_gating/partitions.py
index 7be8236ad5c7fd9d1b91c01cfdc6ce15a054bc75..e1a464d9bc0370e55bd0c61411baa1e264fec010 100644
--- a/openedx/features/content_type_gating/partitions.py
+++ b/openedx/features/content_type_gating/partitions.py
@@ -23,10 +23,7 @@ from lms.djangoapps.courseware.masquerade import (
 )
 from xmodule.partitions.partitions import Group, UserPartition, UserPartitionError
 from openedx.core.lib.mobile_utils import is_request_from_mobile_app
-from openedx.features.course_duration_limits.config import (
-    CONTENT_TYPE_GATING_FLAG,
-    CONTENT_TYPE_GATING_STUDIO_UI_FLAG,
-)
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
 
 LOG = logging.getLogger(__name__)
 
@@ -46,7 +43,7 @@ def create_content_gating_partition(course):
     Create and return the Content Gating user partition.
     """
 
-    if not (CONTENT_TYPE_GATING_FLAG.is_enabled() or CONTENT_TYPE_GATING_STUDIO_UI_FLAG.is_enabled()):
+    if not ContentTypeGatingConfig.enabled_for_course(course_key=course.id):
         return None
 
     try:
@@ -144,7 +141,7 @@ class ContentTypeGatingPartitionScheme(object):
         # For now, treat everyone as a Full-access user, until we have the rest of the
         # feature gating logic in place.
 
-        if not CONTENT_TYPE_GATING_FLAG.is_enabled():
+        if not ContentTypeGatingConfig.enabled_for_enrollment(user=user, course_key=course_key):
             return cls.FULL_ACCESS
 
         # If CONTENT_TYPE_GATING is enabled use the following logic to determine whether a user should have FULL_ACCESS
diff --git a/openedx/features/content_type_gating/tests/test_access.py b/openedx/features/content_type_gating/tests/test_access.py
index 93fa0c07028ca3b78426aa82370ce4a87084a9a5..51a9f418293526ab87366c6c56e9077c20dd9d2c 100644
--- a/openedx/features/content_type_gating/tests/test_access.py
+++ b/openedx/features/content_type_gating/tests/test_access.py
@@ -2,6 +2,7 @@
 Test audit user's access to various content based on content-gating features.
 """
 
+from datetime import date
 import ddt
 from django.conf import settings
 from django.test.client import RequestFactory
@@ -14,7 +15,7 @@ from lms.djangoapps.courseware.module_render import load_single_xblock
 from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
 from openedx.core.lib.url_utils import quote_slashes
 from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
 from student.roles import CourseInstructorRole, CourseStaffRole
 from student.tests.factories import (
     AdminFactory,
@@ -28,7 +29,6 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
 
 
 @ddt.ddt
-@override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
 @override_settings(FIELD_OVERRIDE_PROVIDERS=(
     'openedx.features.content_type_gating.field_override.ContentTypeGatingFieldOverride',
 ))
@@ -176,6 +176,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
             course_id=self.courses['audit_only']['course'].id,
             mode='audit'
         )
+        ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
 
     @classmethod
     def _create_course(cls, run, display_name, modes, component_types):
@@ -240,7 +241,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
             }
 
     @patch("crum.get_current_request")
-    def _assert_block_is_gated(self, mock_get_current_request, block, is_gated, user_id, course_id):
+    def _assert_block_is_gated(self, mock_get_current_request, block, is_gated, user_id, course):
         """
         Asserts that a block in a specific course is gated for a specific user
 
@@ -258,9 +259,9 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
         vertical_xblock = load_single_xblock(
             request=fake_request,
             user_id=user_id,
-            course_id=unicode(course_id),
+            course_id=unicode(course.id),
             usage_key_string=unicode(self.blocks_dict['vertical'].scope_ids.usage_id),
-            course=None
+            course=course
         )
         runtime = vertical_xblock.runtime
 
@@ -290,13 +291,13 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
         self._assert_block_is_gated(
             block=self.blocks_dict[prob_type],
             user_id=self.users['audit'].id,
-            course_id=self.course.id,
+            course=self.course,
             is_gated=is_gated
         )
         self._assert_block_is_gated(
             block=self.blocks_dict[prob_type],
             user_id=self.users['verified'].id,
-            course_id=self.course.id,
+            course=self.course,
             is_gated=False
         )
 
@@ -310,7 +311,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
         self._assert_block_is_gated(
             block=block,
             user_id=self.audit_user.id,
-            course_id=self.course.id,
+            course=self.course,
             is_gated=is_gated
         )
 
@@ -344,7 +345,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
         self._assert_block_is_gated(
             block=self.courses[course]['blocks'][component_type],
             user_id=self.users[user_track].id,
-            course_id=self.courses[course]['course'].id,
+            course=self.courses[course]['course'],
             is_gated=is_gated,
         )
 
@@ -393,6 +394,6 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
             self._assert_block_is_gated(
                 block=self.blocks_dict['problem'],
                 user_id=course_team_member.id,
-                course_id=self.course.id,
+                course=self.course,
                 is_gated=False
             )
diff --git a/openedx/features/content_type_gating/tests/test_models.py b/openedx/features/content_type_gating/tests/test_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..c8e56d55a275fecd0f476168b7d0bc6f323cab28
--- /dev/null
+++ b/openedx/features/content_type_gating/tests/test_models.py
@@ -0,0 +1,304 @@
+import ddt
+from datetime import timedelta, date, datetime, time
+import itertools
+from mock import Mock
+
+from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
+from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
+from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
+from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
+from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from student.tests.factories import CourseEnrollmentFactory, UserFactory
+
+
+@ddt.ddt
+class TestContentTypeGatingConfig(CacheIsolationTestCase):
+
+    ENABLED_CACHES = ['default']
+
+    def setUp(self):
+        self.course_overview = CourseOverviewFactory.create()
+        self.user = UserFactory.create()
+        super(TestContentTypeGatingConfig, self).setUp()
+
+    @ddt.data(
+        (True, True, True),
+        (True, True, False),
+        (True, False, True),
+        (True, False, False),
+        (False, False, True),
+        (False, False, False),
+    )
+    @ddt.unpack
+    def test_enabled_for_enrollment(
+        self,
+        already_enrolled,
+        pass_enrollment,
+        enrolled_before_enabled,
+    ):
+
+        # Tweak the day to enable the config so that it is either before
+        # or after today (which is when the enrollment will be created)
+        if enrolled_before_enabled:
+            enabled_as_of = date.today() + timedelta(days=1)
+        else:
+            enabled_as_of = date.today() - timedelta(days=1)
+
+        config = ContentTypeGatingConfig.objects.create(
+            enabled=True,
+            course=self.course_overview,
+            enabled_as_of=enabled_as_of,
+        )
+
+        if already_enrolled:
+            existing_enrollment = CourseEnrollmentFactory.create(
+                user=self.user,
+                course=self.course_overview,
+            )
+        else:
+            existing_enrollment = None
+
+        if pass_enrollment:
+            enrollment = existing_enrollment
+            user = None
+            course_key = None
+        else:
+            enrollment = None
+            user = self.user
+            course_key = self.course_overview.id
+
+        if pass_enrollment:
+            query_count = 4
+        else:
+            query_count = 5
+
+        with self.assertNumQueries(query_count):
+            enabled = ContentTypeGatingConfig.enabled_for_enrollment(
+                enrollment=enrollment,
+                user=user,
+                course_key=course_key,
+            )
+            self.assertEqual(not enrolled_before_enabled, enabled)
+
+    def test_enabled_for_enrollment_failure(self):
+        with self.assertRaises(ValueError):
+            ContentTypeGatingConfig.enabled_for_enrollment(None, None, None)
+        with self.assertRaises(ValueError):
+            ContentTypeGatingConfig.enabled_for_enrollment(Mock(name='enrollment'), Mock(name='user'), None)
+        with self.assertRaises(ValueError):
+            ContentTypeGatingConfig.enabled_for_enrollment(Mock(name='enrollment'), None, Mock(name='course_key'))
+
+    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
+    def test_enabled_for_enrollment_flag_override(self):
+        self.assertTrue(ContentTypeGatingConfig.enabled_for_enrollment(None, None, None))
+        self.assertTrue(ContentTypeGatingConfig.enabled_for_enrollment(Mock(name='enrollment'), Mock(name='user'), None))
+        self.assertTrue(ContentTypeGatingConfig.enabled_for_enrollment(Mock(name='enrollment'), None, Mock(name='course_key')))
+
+    @ddt.data(True, False)
+    def test_enabled_for_course(
+        self,
+        before_enabled,
+    ):
+        config = ContentTypeGatingConfig.objects.create(
+            enabled=True,
+            course=self.course_overview,
+            enabled_as_of=date(2018, 1, 1),
+        )
+
+        # Tweak the day to check for course enablement so it is either
+        # before or after when the configuration was enabled
+        if before_enabled:
+            target_date = config.enabled_as_of - timedelta(days=1)
+        else:
+            target_date = config.enabled_as_of + timedelta(days=1)
+
+        course_key = self.course_overview.id
+
+        self.assertEqual(
+            not before_enabled,
+            ContentTypeGatingConfig.enabled_for_course(
+                course_key=course_key,
+                target_date=target_date,
+            )
+        )
+
+    @ddt.data(
+        # Generate all combinations of setting each configuration level to True/False/None
+        *itertools.product(*[(True, False, None)] * 4)
+    )
+    @ddt.unpack
+    def test_config_overrides(self, global_setting, site_setting, org_setting, course_setting):
+        """
+        Test that the stacked configuration overrides happen in the correct order and priority.
+
+        This is tested by exhaustively setting each combination of contexts, and validating that only
+        the lowest level context that is set to not-None is applied.
+        """
+        # Add a bunch of configuration outside the contexts that are being tested, to make sure
+        # there are no leaks of configuration across contexts
+        non_test_course_enabled = CourseOverviewFactory.create(org='non-test-org-enabled')
+        non_test_course_disabled = CourseOverviewFactory.create(org='non-test-org-disabled')
+        non_test_site_cfg_enabled = SiteConfigurationFactory.create(values={'course_org_filter': non_test_course_enabled.org})
+        non_test_site_cfg_disabled = SiteConfigurationFactory.create(values={'course_org_filter': non_test_course_disabled.org})
+
+        ContentTypeGatingConfig.objects.create(course=non_test_course_enabled, enabled=True, enabled_as_of=date(2018, 1, 1))
+        ContentTypeGatingConfig.objects.create(course=non_test_course_disabled, enabled=False)
+        ContentTypeGatingConfig.objects.create(org=non_test_course_enabled.org, enabled=True, enabled_as_of=date(2018, 1, 1))
+        ContentTypeGatingConfig.objects.create(org=non_test_course_disabled.org, enabled=False)
+        ContentTypeGatingConfig.objects.create(site=non_test_site_cfg_enabled.site, enabled=True, enabled_as_of=date(2018, 1, 1))
+        ContentTypeGatingConfig.objects.create(site=non_test_site_cfg_disabled.site, enabled=False)
+
+        # Set up test objects
+        test_course = CourseOverviewFactory.create(org='test-org')
+        test_site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': test_course.org})
+
+        ContentTypeGatingConfig.objects.create(enabled=global_setting, enabled_as_of=date(2018, 1, 1))
+        ContentTypeGatingConfig.objects.create(course=test_course, enabled=course_setting, enabled_as_of=date(2018, 1, 1))
+        ContentTypeGatingConfig.objects.create(org=test_course.org, enabled=org_setting, enabled_as_of=date(2018, 1, 1))
+        ContentTypeGatingConfig.objects.create(site=test_site_cfg.site, enabled=site_setting, enabled_as_of=date(2018, 1, 1))
+
+        all_settings = [global_setting, site_setting, org_setting, course_setting]
+        expected_global_setting = self._resolve_settings([global_setting])
+        expected_site_setting = self._resolve_settings([global_setting, site_setting])
+        expected_org_setting = self._resolve_settings([global_setting, site_setting, org_setting])
+        expected_course_setting = self._resolve_settings([global_setting, site_setting, org_setting, course_setting])
+
+        self.assertEqual(expected_global_setting, ContentTypeGatingConfig.current().enabled)
+        self.assertEqual(expected_site_setting, ContentTypeGatingConfig.current(site=test_site_cfg.site).enabled)
+        self.assertEqual(expected_org_setting, ContentTypeGatingConfig.current(org=test_course.org).enabled)
+        self.assertEqual(expected_course_setting, ContentTypeGatingConfig.current(course_key=test_course.id).enabled)
+
+    def test_caching_global(self):
+        global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
+        global_config.save()
+
+        # Check that the global value is not retrieved from cache after save
+        with self.assertNumQueries(1):
+            self.assertTrue(ContentTypeGatingConfig.current().enabled)
+
+        # Check that the global value can be retrieved from cache after read
+        with self.assertNumQueries(0):
+            self.assertTrue(ContentTypeGatingConfig.current().enabled)
+
+        global_config.enabled = False
+        global_config.save()
+
+        # Check that the global value in cache was deleted on save
+        with self.assertNumQueries(1):
+            self.assertFalse(ContentTypeGatingConfig.current().enabled)
+
+    def test_caching_site(self):
+        site_cfg = SiteConfigurationFactory()
+        site_config = ContentTypeGatingConfig(site=site_cfg.site, enabled=True, enabled_as_of=date(2018, 1, 1))
+        site_config.save()
+
+        # Check that the site value is not retrieved from cache after save
+        with self.assertNumQueries(1):
+            self.assertTrue(ContentTypeGatingConfig.current(site=site_cfg.site).enabled)
+
+        # Check that the site value can be retrieved from cache after read
+        with self.assertNumQueries(0):
+            self.assertTrue(ContentTypeGatingConfig.current(site=site_cfg.site).enabled)
+
+        site_config.enabled = False
+        site_config.save()
+
+        # Check that the site value in cache was deleted on save
+        with self.assertNumQueries(1):
+            self.assertFalse(ContentTypeGatingConfig.current(site=site_cfg.site).enabled)
+
+        global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
+        global_config.save()
+
+        # Check that the site value is not updated in cache by changing the global value
+        with self.assertNumQueries(0):
+            self.assertFalse(ContentTypeGatingConfig.current(site=site_cfg.site).enabled)
+
+    def test_caching_org(self):
+        course = CourseOverviewFactory.create(org='test-org')
+        site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': course.org})
+        org_config = ContentTypeGatingConfig(org=course.org, enabled=True, enabled_as_of=date(2018, 1, 1))
+        org_config.save()
+
+        # Check that the org value is not retrieved from cache after save
+        with self.assertNumQueries(2):
+            self.assertTrue(ContentTypeGatingConfig.current(org=course.org).enabled)
+
+        # Check that the org value can be retrieved from cache after read
+        with self.assertNumQueries(0):
+            self.assertTrue(ContentTypeGatingConfig.current(org=course.org).enabled)
+
+        org_config.enabled = False
+        org_config.save()
+
+        # Check that the org value in cache was deleted on save
+        with self.assertNumQueries(2):
+            self.assertFalse(ContentTypeGatingConfig.current(org=course.org).enabled)
+
+        global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
+        global_config.save()
+
+        # Check that the org value is not updated in cache by changing the global value
+        with self.assertNumQueries(0):
+            self.assertFalse(ContentTypeGatingConfig.current(org=course.org).enabled)
+
+        site_config = ContentTypeGatingConfig(site=site_cfg.site, enabled=True, enabled_as_of=date(2018, 1, 1))
+        site_config.save()
+
+        # Check that the org value is not updated in cache by changing the site value
+        with self.assertNumQueries(0):
+            self.assertFalse(ContentTypeGatingConfig.current(org=course.org).enabled)
+
+    def test_caching_course(self):
+        course = CourseOverviewFactory.create(org='test-org')
+        site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': course.org})
+        course_config = ContentTypeGatingConfig(course=course, enabled=True, enabled_as_of=date(2018, 1, 1))
+        course_config.save()
+
+        # Check that the org value is not retrieved from cache after save
+        with self.assertNumQueries(2):
+            self.assertTrue(ContentTypeGatingConfig.current(course_key=course.id).enabled)
+
+        # Check that the org value can be retrieved from cache after read
+        with self.assertNumQueries(0):
+            self.assertTrue(ContentTypeGatingConfig.current(course_key=course.id).enabled)
+
+        course_config.enabled = False
+        course_config.save()
+
+        # Check that the org value in cache was deleted on save
+        with self.assertNumQueries(2):
+            self.assertFalse(ContentTypeGatingConfig.current(course_key=course.id).enabled)
+
+        global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
+        global_config.save()
+
+        # Check that the org value is not updated in cache by changing the global value
+        with self.assertNumQueries(0):
+            self.assertFalse(ContentTypeGatingConfig.current(course_key=course.id).enabled)
+
+        site_config = ContentTypeGatingConfig(site=site_cfg.site, enabled=True, enabled_as_of=date(2018, 1, 1))
+        site_config.save()
+
+        # Check that the org value is not updated in cache by changing the site value
+        with self.assertNumQueries(0):
+            self.assertFalse(ContentTypeGatingConfig.current(course_key=course.id).enabled)
+
+        org_config = ContentTypeGatingConfig(org=course.org, enabled=True, enabled_as_of=date(2018, 1, 1))
+        org_config.save()
+
+        # Check that the org value is not updated in cache by changing the site value
+        with self.assertNumQueries(0):
+            self.assertFalse(ContentTypeGatingConfig.current(course_key=course.id).enabled)
+
+    def _resolve_settings(self, settings):
+        if all(setting is None for setting in settings):
+            return None
+
+        return [
+            setting
+            for setting
+            in settings
+            if setting is not None
+        ][-1]
diff --git a/openedx/features/course_duration_limits/access.py b/openedx/features/course_duration_limits/access.py
index ecaee92806bef8f1f1dc1bfe767d87a471d75ab5..e19c8bfb6348bf46dc9b4a2d6f1cc3a78897068a 100644
--- a/openedx/features/course_duration_limits/access.py
+++ b/openedx/features/course_duration_limits/access.py
@@ -17,7 +17,7 @@ from openedx.core.djangoapps.catalog.utils import get_course_run_details
 from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
 from openedx.core.djangoapps.util.user_messages import PageLevelMessages
 from openedx.core.djangolib.markup import HTML
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
 
 MIN_DURATION = timedelta(weeks=4)
 MAX_DURATION = timedelta(weeks=12)
@@ -100,7 +100,7 @@ def register_course_expired_message(request, course):
     """
     Add a banner notifying the user of the user course expiration date if it exists.
     """
-    if CONTENT_TYPE_GATING_FLAG.is_enabled():
+    if CourseDurationLimitConfig.enabled_for_enrollment(user=request.user, course_key=course.id):
         expiration_date = get_user_course_expiration_date(request.user, course)
         if expiration_date:
             upgrade_message = _('Your access to this course expires on {expiration_date}. \
diff --git a/openedx/features/course_duration_limits/admin.py b/openedx/features/course_duration_limits/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..1686c5bb38e233a89cbd3c82cecce72401660206
--- /dev/null
+++ b/openedx/features/course_duration_limits/admin.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+"""
+Django Admin pages for CourseDurationLimitConfig.
+"""
+
+from __future__ import unicode_literals
+
+from django.contrib import admin
+from django.utils.translation import ugettext_lazy as _
+
+from openedx.core.djangoapps.config_model_utils.admin import StackedConfigModelAdmin
+from .models import CourseDurationLimitConfig
+
+
+class CourseDurationLimitConfigAdmin(StackedConfigModelAdmin):
+    fieldsets = (
+        ('Context', {
+            'fields': ('site', 'org', 'course'),
+            'description': _(
+                'These define the context to enable course duration limits on. '
+                'If no values are set, then the configuration applies globally. '
+                'If a single value is set, then the configuration applies to all courses '
+                'within that context. At most one value can be set at a time.<br>'
+                'If multiple contexts apply to a course (for example, if configuration '
+                'is specified for the course specifically, and for the org that the course '
+                'is in, then the more specific context overrides the more general context.'
+            ),
+        }),
+        ('Configuration', {
+            'fields': ('enabled', 'enabled_as_of'),
+            'description': _(
+                'If any of these values is left empty or "Unknown", then their value '
+                'at runtime will be retrieved from the next most specific context that applies. '
+                'For example, if "Enabled" is left as "Unknown" in the course context, then that '
+                'course will be Enabled only if the org that it is in is Enabled.'
+            ),
+        })
+    )
+
+admin.site.register(CourseDurationLimitConfig, CourseDurationLimitConfigAdmin)
diff --git a/openedx/features/course_duration_limits/config.py b/openedx/features/course_duration_limits/config.py
index 23836c11334f82ea3220cff369f49f56b0d98c1b..eff4f675e7f3a54967c1fd0a6613053d83e2cc61 100644
--- a/openedx/features/course_duration_limits/config.py
+++ b/openedx/features/course_duration_limits/config.py
@@ -11,9 +11,3 @@ CONTENT_TYPE_GATING_FLAG = WaffleFlag(
     flag_name=u'debug',
     flag_undefined_default=False
 )
-
-CONTENT_TYPE_GATING_STUDIO_UI_FLAG = WaffleFlag(
-    waffle_namespace=WAFFLE_FLAG_NAMESPACE,
-    flag_name=u'studio_ui',
-    flag_undefined_default=False
-)
diff --git a/openedx/features/course_duration_limits/migrations/0001_initial.py b/openedx/features/course_duration_limits/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..85bfedf5907b99e4d0874ba62f0a0e16fc046643
--- /dev/null
+++ b/openedx/features/course_duration_limits/migrations/0001_initial.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.16 on 2018-11-08 19:43
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('course_overviews', '0014_courseoverview_certificate_available_date'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('sites', '0002_alter_domain_unique'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='CourseDurationLimitConfig',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
+                ('enabled', models.NullBooleanField(default=None, verbose_name='Enabled')),
+                ('org', models.CharField(blank=True, db_index=True, max_length=255, null=True)),
+                ('enabled_as_of', models.DateField(blank=True, default=None, null=True, verbose_name='Enabled As Of')),
+                ('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')),
+                ('course', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='course_overviews.CourseOverview')),
+                ('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+    ]
diff --git a/openedx/features/course_duration_limits/migrations/0002_auto_20181119_0959.py b/openedx/features/course_duration_limits/migrations/0002_auto_20181119_0959.py
new file mode 100644
index 0000000000000000000000000000000000000000..32468ce598c2d7be3acdfbd61b2fad6464e78a58
--- /dev/null
+++ b/openedx/features/course_duration_limits/migrations/0002_auto_20181119_0959.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.16 on 2018-11-19 14:59
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('course_duration_limits', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='coursedurationlimitconfig',
+            name='enabled_as_of',
+            field=models.DateField(blank=True, default=None, help_text='If the configuration is Enabled, then all enrollments created after this date (UTC) will be affected.', null=True, verbose_name='Enabled As Of'),
+        ),
+    ]
diff --git a/openedx/features/course_duration_limits/migrations/__init__.py b/openedx/features/course_duration_limits/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/openedx/features/course_duration_limits/models.py b/openedx/features/course_duration_limits/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..5364b96b19088f3907145482e4497d3195f9356b
--- /dev/null
+++ b/openedx/features/course_duration_limits/models.py
@@ -0,0 +1,123 @@
+"""
+Course Duration Limit Configuration Models
+"""
+
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from datetime import datetime
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
+from django.utils.translation import ugettext_lazy as _
+
+from student.models import CourseEnrollment
+from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel
+from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+
+
+@python_2_unicode_compatible
+class CourseDurationLimitConfig(StackedConfigurationModel):
+    """
+    Configuration to manage the Course Duration Limit facility.
+    """
+
+    STACKABLE_FIELDS = ('enabled', 'enabled_as_of')
+
+    enabled_as_of = models.DateField(
+        default=None,
+        null=True,
+        verbose_name=_('Enabled As Of'),
+        blank=True,
+        help_text=_(
+            'If the configuration is Enabled, then all enrollments '
+            'created after this date (UTC) will be affected.'
+        )
+    )
+
+    @classmethod
+    def enabled_for_enrollment(cls, enrollment=None, user=None, course_key=None):
+        """
+        Return whether Course Duration Limits are enabled for this enrollment.
+
+        Course Duration Limits are enabled for an enrollment if they are enabled for
+        the course being enrolled in (either specifically, or via a containing context,
+        such as the org, site, or globally), and if the configuration is specified to be
+        ``enabled_as_of`` before the enrollment was created.
+
+        Only one of enrollment and (user, course_key) may be specified at a time.
+
+        Arguments:
+            enrollment: The enrollment being queried.
+            user: The user being queried.
+            course_key: The CourseKey of the course being queried.
+        """
+        if CONTENT_TYPE_GATING_FLAG.is_enabled():
+            return True
+
+        if enrollment is not None and (user is not None or course_key is not None):
+            raise ValueError('Specify enrollment or user/course_key, but not both')
+
+        if enrollment is None and (user is None or course_key is None):
+            raise ValueError('Both user and course_key must be specified if no enrollment is provided')
+
+        if enrollment is None and user is None and course_key is None:
+            raise ValueError('At least one of enrollment or user and course_key must be specified')
+
+        if course_key is None:
+            course_key = enrollment.course_id
+
+        if enrollment is None:
+            enrollment = CourseEnrollment.get_enrollment(user, course_key)
+
+        # enrollment might be None if the user isn't enrolled. In that case,
+        # return enablement as if the user enrolled today
+        if enrollment is None:
+            return cls.enabled_for_course(course_key=course_key, target_date=datetime.utcnow().date())
+        else:
+            current_config = cls.current(course_key=enrollment.course_id)
+            return current_config.enabled_as_of_date(target_date=enrollment.created.date())
+
+    @classmethod
+    def enabled_for_course(cls, course_key, target_date=None):
+        """
+        Return whether Course Duration Limits are enabled for this course as of a particular date.
+
+        Course Duration Limits are enabled for a course on a date if they are enabled either specifically,
+        or via a containing context, such as the org, site, or globally, and if the configuration
+        is specified to be ``enabled_as_of`` before ``target_date``.
+
+        Only one of enrollment and (user, course_key) may be specified at a time.
+
+        Arguments:
+            course_key: The CourseKey of the course being queried.
+            target_date: The date to checked enablement as of. Defaults to the current date.
+        """
+        if CONTENT_TYPE_GATING_FLAG.is_enabled():
+            return True
+
+        if target_date is None:
+            target_date = datetime.utcnow().date()
+
+        current_config = cls.current(course_key=course_key)
+        return current_config.enabled_as_of_date(target_date=target_date)
+
+    def clean(self):
+        if self.enabled and self.enabled_as_of is None:
+            raise ValidationError({'enabled_as_of': _('enabled_as_of must be set when enabled is True')})
+
+    def enabled_as_of_date(self, target_date):
+        """
+        Return whether this Course Duration Limit configuration context is enabled as of a date.
+
+        Arguments:
+            target_date (:class:`datetime.date`): The date that ``enabled_as_of`` must be equal to or before
+        """
+        if CONTENT_TYPE_GATING_FLAG.is_enabled():
+            return True
+
+        # Explicitly cast this to bool, so that when self.enabled is None the method doesn't return None
+        return bool(self.enabled and self.enabled_as_of <= target_date)
+
+    def __str__(self):
+        return "CourseDurationLimits(enabled={!r}, enabled_as_of={!r})"
diff --git a/openedx/features/course_duration_limits/tests/test_models.py b/openedx/features/course_duration_limits/tests/test_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..1d38736bf871558c11f980f83cb74c1d79548ef4
--- /dev/null
+++ b/openedx/features/course_duration_limits/tests/test_models.py
@@ -0,0 +1,335 @@
+"""
+Tests of CourseDurationLimitConfig.
+"""
+
+from datetime import timedelta, date
+import itertools
+
+import ddt
+from mock import Mock
+
+from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
+from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
+from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
+from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
+from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
+from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from student.tests.factories import CourseEnrollmentFactory, UserFactory
+
+
+@ddt.ddt
+class TestCourseDurationLimitConfig(CacheIsolationTestCase):
+    """
+    Tests of CourseDurationLimitConfig
+    """
+
+    ENABLED_CACHES = ['default']
+
+    def setUp(self):
+        self.course_overview = CourseOverviewFactory.create()
+        self.user = UserFactory.create()
+        super(TestCourseDurationLimitConfig, self).setUp()
+
+    @ddt.data(
+        (True, True, True),
+        (True, True, False),
+        (True, False, True),
+        (True, False, False),
+        (False, False, True),
+        (False, False, False),
+    )
+    @ddt.unpack
+    def test_enabled_for_enrollment(
+        self,
+        already_enrolled,
+        pass_enrollment,
+        enrolled_before_enabled,
+    ):
+
+        # Tweak the day to enable the config so that it is either before
+        # or after today (which is when the enrollment will be created)
+        if enrolled_before_enabled:
+            enabled_as_of = date.today() + timedelta(days=1)
+        else:
+            enabled_as_of = date.today() - timedelta(days=1)
+
+        CourseDurationLimitConfig.objects.create(
+            enabled=True,
+            course=self.course_overview,
+            enabled_as_of=enabled_as_of,
+        )
+
+        if already_enrolled:
+            existing_enrollment = CourseEnrollmentFactory.create(
+                user=self.user,
+                course=self.course_overview,
+            )
+        else:
+            existing_enrollment = None
+
+        if pass_enrollment:
+            enrollment = existing_enrollment
+            user = None
+            course_key = None
+        else:
+            enrollment = None
+            user = self.user
+            course_key = self.course_overview.id
+
+        if pass_enrollment:
+            query_count = 4
+        else:
+            query_count = 5
+
+        with self.assertNumQueries(query_count):
+            enabled = CourseDurationLimitConfig.enabled_for_enrollment(
+                enrollment=enrollment,
+                user=user,
+                course_key=course_key,
+            )
+            self.assertEqual(not enrolled_before_enabled, enabled)
+
+    def test_enabled_for_enrollment_failure(self):
+        with self.assertRaises(ValueError):
+            CourseDurationLimitConfig.enabled_for_enrollment(None, None, None)
+        with self.assertRaises(ValueError):
+            CourseDurationLimitConfig.enabled_for_enrollment(
+                Mock(name='enrollment'),
+                Mock(name='user'),
+                None
+            )
+        with self.assertRaises(ValueError):
+            CourseDurationLimitConfig.enabled_for_enrollment(
+                Mock(name='enrollment'),
+                None,
+                Mock(name='course_key')
+            )
+
+    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
+    def test_enabled_for_enrollment_flag_override(self):
+        self.assertTrue(CourseDurationLimitConfig.enabled_for_enrollment(
+            None,
+            None,
+            None
+        ))
+        self.assertTrue(CourseDurationLimitConfig.enabled_for_enrollment(
+            Mock(name='enrollment'),
+            Mock(name='user'),
+            None
+        ))
+        self.assertTrue(CourseDurationLimitConfig.enabled_for_enrollment(
+            Mock(name='enrollment'),
+            None,
+            Mock(name='course_key')
+        ))
+
+    @ddt.data(True, False)
+    def test_enabled_for_course(
+        self,
+        before_enabled,
+    ):
+        config = CourseDurationLimitConfig.objects.create(
+            enabled=True,
+            course=self.course_overview,
+            enabled_as_of=date.today(),
+        )
+
+        # Tweak the day to check for course enablement so it is either
+        # before or after when the configuration was enabled
+        if before_enabled:
+            target_date = config.enabled_as_of - timedelta(days=1)
+        else:
+            target_date = config.enabled_as_of + timedelta(days=1)
+
+        course_key = self.course_overview.id
+
+        self.assertEqual(
+            not before_enabled,
+            CourseDurationLimitConfig.enabled_for_course(
+                course_key=course_key,
+                target_date=target_date,
+            )
+        )
+
+    @ddt.data(
+        # Generate all combinations of setting each configuration level to True/False/None
+        *itertools.product(*[(True, False, None)] * 4)
+    )
+    @ddt.unpack
+    def test_config_overrides(self, global_setting, site_setting, org_setting, course_setting):
+        """
+        Test that the stacked configuration overrides happen in the correct order and priority.
+
+        This is tested by exhaustively setting each combination of contexts, and validating that only
+        the lowest level context that is set to not-None is applied.
+        """
+        # Add a bunch of configuration outside the contexts that are being tested, to make sure
+        # there are no leaks of configuration across contexts
+        non_test_course_enabled = CourseOverviewFactory.create(org='non-test-org-enabled')
+        non_test_course_disabled = CourseOverviewFactory.create(org='non-test-org-disabled')
+        non_test_site_cfg_enabled = SiteConfigurationFactory.create(
+            values={'course_org_filter': non_test_course_enabled.org}
+        )
+        non_test_site_cfg_disabled = SiteConfigurationFactory.create(
+            values={'course_org_filter': non_test_course_disabled.org}
+        )
+
+        CourseDurationLimitConfig.objects.create(course=non_test_course_enabled, enabled=True)
+        CourseDurationLimitConfig.objects.create(course=non_test_course_disabled, enabled=False)
+        CourseDurationLimitConfig.objects.create(org=non_test_course_enabled.org, enabled=True)
+        CourseDurationLimitConfig.objects.create(org=non_test_course_disabled.org, enabled=False)
+        CourseDurationLimitConfig.objects.create(site=non_test_site_cfg_enabled.site, enabled=True)
+        CourseDurationLimitConfig.objects.create(site=non_test_site_cfg_disabled.site, enabled=False)
+
+        # Set up test objects
+        test_course = CourseOverviewFactory.create(org='test-org')
+        test_site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': test_course.org})
+
+        CourseDurationLimitConfig.objects.create(enabled=global_setting)
+        CourseDurationLimitConfig.objects.create(course=test_course, enabled=course_setting)
+        CourseDurationLimitConfig.objects.create(org=test_course.org, enabled=org_setting)
+        CourseDurationLimitConfig.objects.create(site=test_site_cfg.site, enabled=site_setting)
+
+        expected_global_setting = self._resolve_settings([global_setting])
+        expected_site_setting = self._resolve_settings([global_setting, site_setting])
+        expected_org_setting = self._resolve_settings([global_setting, site_setting, org_setting])
+        expected_course_setting = self._resolve_settings([global_setting, site_setting, org_setting, course_setting])
+
+        self.assertEqual(expected_global_setting, CourseDurationLimitConfig.current().enabled)
+        self.assertEqual(expected_site_setting, CourseDurationLimitConfig.current(site=test_site_cfg.site).enabled)
+        self.assertEqual(expected_org_setting, CourseDurationLimitConfig.current(org=test_course.org).enabled)
+        self.assertEqual(expected_course_setting, CourseDurationLimitConfig.current(course_key=test_course.id).enabled)
+
+    def test_caching_global(self):
+        global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
+        global_config.save()
+
+        # Check that the global value is not retrieved from cache after save
+        with self.assertNumQueries(1):
+            self.assertTrue(CourseDurationLimitConfig.current().enabled)
+
+        # Check that the global value can be retrieved from cache after read
+        with self.assertNumQueries(0):
+            self.assertTrue(CourseDurationLimitConfig.current().enabled)
+
+        global_config.enabled = False
+        global_config.save()
+
+        # Check that the global value in cache was deleted on save
+        with self.assertNumQueries(1):
+            self.assertFalse(CourseDurationLimitConfig.current().enabled)
+
+    def test_caching_site(self):
+        site_cfg = SiteConfigurationFactory()
+        site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=date(2018, 1, 1))
+        site_config.save()
+
+        # Check that the site value is not retrieved from cache after save
+        with self.assertNumQueries(1):
+            self.assertTrue(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)
+
+        # Check that the site value can be retrieved from cache after read
+        with self.assertNumQueries(0):
+            self.assertTrue(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)
+
+        site_config.enabled = False
+        site_config.save()
+
+        # Check that the site value in cache was deleted on save
+        with self.assertNumQueries(1):
+            self.assertFalse(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)
+
+        global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
+        global_config.save()
+
+        # Check that the site value is not updated in cache by changing the global value
+        with self.assertNumQueries(0):
+            self.assertFalse(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)
+
+    def test_caching_org(self):
+        course = CourseOverviewFactory.create(org='test-org')
+        site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': course.org})
+        org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=date(2018, 1, 1))
+        org_config.save()
+
+        # Check that the org value is not retrieved from cache after save
+        with self.assertNumQueries(2):
+            self.assertTrue(CourseDurationLimitConfig.current(org=course.org).enabled)
+
+        # Check that the org value can be retrieved from cache after read
+        with self.assertNumQueries(0):
+            self.assertTrue(CourseDurationLimitConfig.current(org=course.org).enabled)
+
+        org_config.enabled = False
+        org_config.save()
+
+        # Check that the org value in cache was deleted on save
+        with self.assertNumQueries(2):
+            self.assertFalse(CourseDurationLimitConfig.current(org=course.org).enabled)
+
+        global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
+        global_config.save()
+
+        # Check that the org value is not updated in cache by changing the global value
+        with self.assertNumQueries(0):
+            self.assertFalse(CourseDurationLimitConfig.current(org=course.org).enabled)
+
+        site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=date(2018, 1, 1))
+        site_config.save()
+
+        # Check that the org value is not updated in cache by changing the site value
+        with self.assertNumQueries(0):
+            self.assertFalse(CourseDurationLimitConfig.current(org=course.org).enabled)
+
+    def test_caching_course(self):
+        course = CourseOverviewFactory.create(org='test-org')
+        site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': course.org})
+        course_config = CourseDurationLimitConfig(course=course, enabled=True, enabled_as_of=date(2018, 1, 1))
+        course_config.save()
+
+        # Check that the org value is not retrieved from cache after save
+        with self.assertNumQueries(2):
+            self.assertTrue(CourseDurationLimitConfig.current(course_key=course.id).enabled)
+
+        # Check that the org value can be retrieved from cache after read
+        with self.assertNumQueries(0):
+            self.assertTrue(CourseDurationLimitConfig.current(course_key=course.id).enabled)
+
+        course_config.enabled = False
+        course_config.save()
+
+        # Check that the org value in cache was deleted on save
+        with self.assertNumQueries(2):
+            self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)
+
+        global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
+        global_config.save()
+
+        # Check that the org value is not updated in cache by changing the global value
+        with self.assertNumQueries(0):
+            self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)
+
+        site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=date(2018, 1, 1))
+        site_config.save()
+
+        # Check that the org value is not updated in cache by changing the site value
+        with self.assertNumQueries(0):
+            self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)
+
+        org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=date(2018, 1, 1))
+        org_config.save()
+
+        # Check that the org value is not updated in cache by changing the site value
+        with self.assertNumQueries(0):
+            self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)
+
+    def _resolve_settings(self, settings):
+        if all(setting is None for setting in settings):
+            return None
+
+        return [
+            setting
+            for setting
+            in settings
+            if setting is not None
+        ][-1]
diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py
index 6b27b676d724cf7e4d419719a8b9701d9c100f6d..565d18118a241a9f50fce1117fbea4d6da2628a9 100644
--- a/openedx/features/course_experience/tests/views/test_course_home.py
+++ b/openedx/features/course_experience/tests/views/test_course_home.py
@@ -2,7 +2,7 @@
 """
 Tests for the course home page.
 """
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, date
 
 import ddt
 import mock
@@ -25,7 +25,7 @@ from lms.djangoapps.course_goals.api import add_course_goal, remove_course_goal
 from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
 from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
 from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
 from openedx.features.course_experience import (
     SHOW_REVIEWS_TOOL_FLAG,
     SHOW_UPGRADE_MSG_ON_COURSE_HOME,
@@ -174,16 +174,16 @@ class TestCourseHomePage(CourseHomePageTestCase):
         response = self.client.get(url)
         self.assertContains(response, TEST_COURSE_UPDATES_TOOL, status_code=200)
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     def test_queries(self):
         """
         Verify that the view's query count doesn't regress.
         """
+        CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
         # Pre-fetch the view to populate any caches
         course_home_url(self.course)
 
         # Fetch the view and verify the query counts
-        with self.assertNumQueries(70, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
+        with self.assertNumQueries(84, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
             with check_mongo_calls(4):
                 url = course_home_url(self.course)
                 self.client.get(url)
@@ -327,7 +327,6 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
         )
         self.assertRedirects(response, expected_url)
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     @mock.patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False})
     def test_course_does_not_expire_for_different_roles(self):
         """
@@ -371,14 +370,14 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
                 "Should not expire access for user [{}]".format(user_description)
             )
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     @mock.patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False})
     def test_expired_course(self):
         """
         Ensure that a user accessing an expired course sees a redirect to
         the student dashboard, not a 404.
-
         """
+        CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=date(2010, 1, 1))
+
         course = CourseFactory.create(start=THREE_YEARS_AGO)
         url = course_home_url(course)
 
@@ -476,18 +475,28 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
         self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START)
 
         # Verify that enrolled users are shown the course expiration banner if content gating is enabled
-        with override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True):
-            url = course_home_url(self.course)
-            response = self.client.get(url)
-            bannerText = get_expiration_banner_text(user, self.course)
-            self.assertContains(response, bannerText, html=True)
+
+        # We use .save() explicitly here (rather than .objects.create) in order to force the
+        # cache to refresh.
+        config = CourseDurationLimitConfig(
+            course=CourseOverview.get_from_id(self.course.id),
+            enabled=True,
+            enabled_as_of=date(2018, 1, 1)
+        )
+        config.save()
+
+        url = course_home_url(self.course)
+        response = self.client.get(url)
+        bannerText = get_expiration_banner_text(user, self.course)
+        self.assertContains(response, bannerText, html=True)
 
         # Verify that enrolled users are not shown the course expiration banner if content gating is disabled
-        with override_waffle_flag(CONTENT_TYPE_GATING_FLAG, False):
-            url = course_home_url(self.course)
-            response = self.client.get(url)
-            bannerText = get_expiration_banner_text(user, self.course)
-            self.assertNotContains(response, bannerText, html=True)
+        config.enabled = False
+        config.save()
+        url = course_home_url(self.course)
+        response = self.client.get(url)
+        bannerText = get_expiration_banner_text(user, self.course)
+        self.assertNotContains(response, bannerText, html=True)
 
         # Verify that enrolled users are shown 'days until start' message before start date
         future_course = self.create_future_course()
diff --git a/openedx/features/course_experience/tests/views/test_course_updates.py b/openedx/features/course_experience/tests/views/test_course_updates.py
index c441483abf90373e8a47b48d41ba7335bee17239..b94e9649e1c68fca6f0cff92361944d989ca99a1 100644
--- a/openedx/features/course_experience/tests/views/test_course_updates.py
+++ b/openedx/features/course_experience/tests/views/test_course_updates.py
@@ -1,10 +1,12 @@
 """
 Tests for the course updates page.
 """
+from datetime import date
+
 from courseware.courses import get_course_info_usage_key
 from django.urls import reverse
 from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
-from openedx.features.course_duration_limits.config import CONTENT_TYPE_GATING_FLAG
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
 from openedx.features.course_experience.views.course_updates import STATUS_VISIBLE
 from student.models import CourseEnrollment
 from student.tests.factories import UserFactory
@@ -119,15 +121,15 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
         self.assertContains(response, 'First Message')
         self.assertContains(response, 'Second Message')
 
-    @override_waffle_flag(CONTENT_TYPE_GATING_FLAG, True)
     def test_queries(self):
+        ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
         create_course_update(self.course, self.user, 'First Message')
 
         # Pre-fetch the view to populate any caches
         course_updates_url(self.course)
 
         # Fetch the view and verify that the query counts haven't changed
-        with self.assertNumQueries(44, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
+        with self.assertNumQueries(46, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
             with check_mongo_calls(4):
                 url = course_updates_url(self.course)
                 self.client.get(url)
diff --git a/openedx/tests/settings.py b/openedx/tests/settings.py
index 840dc3f1960aceae2180040e5c2b67dd5cd443a9..3b14b9f5f84e177a64d2f325c4881934947f9b6f 100644
--- a/openedx/tests/settings.py
+++ b/openedx/tests/settings.py
@@ -75,6 +75,8 @@ INSTALLED_APPS = (
     'openedx.core.djangoapps.content.block_structure.apps.BlockStructureConfig',
     'openedx.core.djangoapps.catalog',
     'openedx.core.djangoapps.self_paced',
+    'openedx.features.content_type_gating',
+    'openedx.features.course_duration_limits',
     'milestones',
     'celery_utils',
     'waffle',
@@ -91,6 +93,7 @@ MICROSITE_BACKEND = 'microsite_configuration.backends.filebased.FilebasedMicrosi
 MICROSITE_TEMPLATE_BACKEND = 'microsite_configuration.backends.filebased.FilebasedMicrositeTemplateBackend'
 
 SECRET_KEY = 'insecure-secret-key'
+SITE_ID = 1
 
 TRACK_MAX_EVENT = 50000