diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py
index c2645fdbee3c7669af3ea134f3682715d94b80b1..9f80c4e733a0222b5f4648ec525f4fae3263ed9f 100644
--- a/common/djangoapps/course_modes/models.py
+++ b/common/djangoapps/course_modes/models.py
@@ -312,7 +312,7 @@ class CourseMode(models.Model):
 
     @classmethod
     @request_cached(CACHE_NAMESPACE)
-    def modes_for_course(cls, course_id, include_expired=False, only_selectable=True):
+    def modes_for_course(cls, course_id=None, include_expired=False, only_selectable=True, course=None):
         """
         Returns a list of the non-expired modes for a given course id
 
@@ -330,11 +330,25 @@ class CourseMode(models.Model):
                 aren't available to users until they complete the course, so
                 they are hidden in track selection.)
 
+            course (CourseOverview): The course to select course modes from.
+
         Returns:
             list of `Mode` tuples
 
         """
-        found_course_modes = cls.objects.filter(course_id=course_id)
+        if course_id is None and course is None:
+            raise ValueError("One of course_id or course must not be None.")
+
+        if course is not None and not isinstance(course, CourseOverview):
+            # CourseModules don't have the data needed to pull related modes,
+            # so we'll fall back on course_id-based lookup instead
+            course_id = course.id
+            course = None
+
+        if course_id is not None:
+            found_course_modes = cls.objects.filter(course_id=course_id)
+        else:
+            found_course_modes = course.modes
 
         # Filter out expired course modes if include_expired is not set
         if not include_expired:
@@ -347,7 +361,10 @@ class CourseMode(models.Model):
         # we exclude them from the list if we're only looking for selectable modes
         # (e.g. on the track selection page or in the payment/verification flows).
         if only_selectable:
-            found_course_modes = found_course_modes.exclude(mode_slug__in=cls.CREDIT_MODES)
+            if course is not None and hasattr(course, 'selectable_modes'):
+                found_course_modes = course.selectable_modes
+            else:
+                found_course_modes = found_course_modes.exclude(mode_slug__in=cls.CREDIT_MODES)
 
         modes = ([mode.to_tuple() for mode in found_course_modes])
         if not modes:
@@ -356,7 +373,7 @@ class CourseMode(models.Model):
         return modes
 
     @classmethod
-    def modes_for_course_dict(cls, course_id, modes=None, **kwargs):
+    def modes_for_course_dict(cls, course_id=None, modes=None, **kwargs):
         """Returns the non-expired modes for a particular course.
 
         Arguments:
@@ -418,7 +435,7 @@ class CourseMode(models.Model):
             return None
 
     @classmethod
-    def verified_mode_for_course(cls, course_id, modes=None, include_expired=False):
+    def verified_mode_for_course(cls, course_id=None, modes=None, include_expired=False, course=None):
         """Find a verified mode for a particular course.
 
         Since we have multiple modes that can go through the verify flow,
@@ -439,7 +456,12 @@ class CourseMode(models.Model):
             Mode or None
 
         """
-        modes_dict = cls.modes_for_course_dict(course_id, modes=modes, include_expired=include_expired)
+        modes_dict = cls.modes_for_course_dict(
+            course_id=course_id,
+            modes=modes,
+            include_expired=include_expired,
+            course=course
+        )
         verified_mode = modes_dict.get('verified', None)
         professional_mode = modes_dict.get('professional', None)
         # we prefer professional over verify
@@ -703,7 +725,7 @@ class CourseMode(models.Model):
         Takes a mode model and turns it into a model named tuple.
 
         Returns:
-            A 'Model' namedtuple with all the same attributes as the model.
+            A 'Mode' namedtuple with all the same attributes as the model.
 
         """
         return Mode(
diff --git a/lms/djangoapps/branding/__init__.py b/lms/djangoapps/branding/__init__.py
index 534c0c4faffa332b8220a535794a889bf3da2306..e467d96ef4060ec79a43836d1b0a491808353acd 100644
--- a/lms/djangoapps/branding/__init__.py
+++ b/lms/djangoapps/branding/__init__.py
@@ -15,7 +15,7 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
 
 def get_visible_courses(org=None, filter_=None):
     """
-    Return the set of CourseOverviews that should be visible in this branded
+    Yield the CourseOverviews that should be visible in this branded
     instance.
 
     Arguments:
@@ -27,9 +27,10 @@ def get_visible_courses(org=None, filter_=None):
     # Import is placed here to avoid model import at project startup.
     from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
 
-    courses = []
     current_site_orgs = configuration_helpers.get_current_site_orgs()
 
+    courses = CourseOverview.objects.none()
+
     if org:
         # Check the current site's orgs to make sure the org's courses should be displayed
         if not current_site_orgs or org in current_site_orgs:
@@ -40,7 +41,7 @@ def get_visible_courses(org=None, filter_=None):
     else:
         courses = CourseOverview.get_all_courses(filter_=filter_)
 
-    courses = sorted(courses, key=lambda course: course.number)
+    courses = courses.order_by('id')
 
     # Filtering can stop here.
     if current_site_orgs:
@@ -57,11 +58,11 @@ def get_visible_courses(org=None, filter_=None):
         )
 
     if filtered_visible_ids:
-        return [course for course in courses if course.id in filtered_visible_ids]
+        return courses.filter(id__in=filtered_visible_ids)
     else:
         # Filter out any courses based on current org, to avoid leaking these.
         orgs = configuration_helpers.get_all_orgs()
-        return [course for course in courses if course.location.org not in orgs]
+        return courses.exclude(org__in=orgs)
 
 
 def get_university_for_request():
diff --git a/lms/djangoapps/course_api/api.py b/lms/djangoapps/course_api/api.py
index 79a3b387963a7b9159cf7f6dc28f48573dddbe56..ef25e61399b3e97c821b512c47c2e19ff6f288e1 100644
--- a/lms/djangoapps/course_api/api.py
+++ b/lms/djangoapps/course_api/api.py
@@ -57,7 +57,7 @@ def course_detail(request, username, course_key):
 
 def list_courses(request, username, org=None, filter_=None):
     """
-    Return a list of available courses.
+    Yield all available courses.
 
     The courses returned are all be visible to the user identified by
     `username` and the logged in user should have permission to view courses
@@ -81,7 +81,7 @@ def list_courses(request, username, org=None, filter_=None):
             by the given key-value pairs.
 
     Return value:
-        List of `CourseOverview` objects representing the collection of courses.
+        Yield `CourseOverview` objects representing the collection of courses.
     """
     user = get_effective_user(request.user, username)
     return get_courses(user, org=org, filter_=filter_)
diff --git a/lms/djangoapps/course_api/tests/test_api.py b/lms/djangoapps/course_api/tests/test_api.py
index cef5d4c73978d3d02192d9398e4a286b982a39c6..7670acbfa1d4775cffc42105441fd5ef6686cedf 100644
--- a/lms/djangoapps/course_api/tests/test_api.py
+++ b/lms/djangoapps/course_api/tests/test_api.py
@@ -231,12 +231,12 @@ class TestGetCourseListExtras(CourseListTestMixin, ModuleStoreTestCase):
 
     def test_no_courses(self):
         courses = self._make_api_call(self.honor_user, self.honor_user)
-        self.assertEqual(len(courses), 0)
+        self.assertEqual(len(list(courses)), 0)
 
     def test_hidden_course_for_honor(self):
         self.create_course(visible_to_staff_only=True)
         courses = self._make_api_call(self.honor_user, self.honor_user)
-        self.assertEqual(len(courses), 0)
+        self.assertEqual(len(list(courses)), 0)
 
     def test_hidden_course_for_staff(self):
         self.create_course(visible_to_staff_only=True)
diff --git a/lms/djangoapps/course_api/tests/test_views.py b/lms/djangoapps/course_api/tests/test_views.py
index 974752be37a5d22bd2b620f05ff8e2ccb4a0591c..641a7c915d4899c24fd2091cbf2386c9f6322216 100644
--- a/lms/djangoapps/course_api/tests/test_views.py
+++ b/lms/djangoapps/course_api/tests/test_views.py
@@ -1,6 +1,7 @@
 """
 Tests for Course API views.
 """
+from datetime import datetime
 import ddt
 from hashlib import md5
 
@@ -12,8 +13,14 @@ from search.tests.test_course_discovery import DemoCourse
 from search.tests.tests import TEST_INDEX_NAME
 from search.tests.utils import SearcherMixin
 
+from course_modes.models import CourseMode
+from course_modes.tests.factories import CourseModeFactory
+from edx_django_utils.cache import RequestCache
 from openedx.core.lib.tests import attr
+from openedx.features.content_type_gating.models import ContentTypeGatingConfig
+from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
 from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
+from xmodule.modulestore.tests.factories import CourseFactory
 from waffle.testutils import override_switch
 
 from ..views import CourseDetailView, CourseListUserThrottle
@@ -283,6 +290,7 @@ class CourseListSearchViewTest(CourseApiTestViewMixin, ModuleStoreTestCase, Sear
     """
 
     ENABLED_SIGNALS = ['course_published']
+    ENABLED_CACHES = ModuleStoreTestCase.ENABLED_CACHES + ['configuration']
 
     def setUp(self):
         super(CourseListSearchViewTest, self).setUp()
@@ -298,6 +306,7 @@ class CourseListSearchViewTest(CourseApiTestViewMixin, ModuleStoreTestCase, Sear
         self.url = reverse('course-list')
         self.staff_user = self.create_user(username='staff', is_staff=True)
         self.honor_user = self.create_user(username='honor', is_staff=False)
+        self.audit_user = self.create_user(username='audit', is_staff=False)
 
     def create_and_index_course(self, org_code, short_description):
         """
@@ -347,4 +356,56 @@ class CourseListSearchViewTest(CourseApiTestViewMixin, ModuleStoreTestCase, Sear
         res = self.verify_response(params={'search_term': 'unique search term'})
         self.assertIn('results', res.data)
         self.assertNotEqual(res.data['results'], [])
-        self.assertEqual(res.data['pagination']['count'], 1)  # Should list a single course
+        # Returns a count of 3 courses because that's the estimate before filtering
+        self.assertEqual(res.data['pagination']['count'], 3)
+        self.assertEqual(len(res.data['results']), 1)  # Should return a single course
+
+    def test_too_many_courses(self):
+        """
+        Test that search results are limited to 100 courses, and that they don't
+        blow up the database.
+        """
+
+        ContentTypeGatingConfig.objects.create(
+            enabled=True,
+            enabled_as_of=datetime(2018, 1, 1),
+        )
+
+        CourseDurationLimitConfig.objects.create(
+            enabled=True,
+            enabled_as_of=datetime(2018, 1, 1),
+        )
+
+        course_ids = []
+
+        # Create 300 courses across 30 organizations
+        for org_num in range(10):
+            org_id = 'org{}'.format(org_num)
+            for course_num in range(30):
+                course_name = 'course{}.{}'.format(org_num, course_num)
+                course_run_name = 'run{}.{}'.format(org_num, course_num)
+                course = CourseFactory.create(org=org_id, number=course_name, run=course_run_name, emit_signals=True)
+                CourseModeFactory.create(course_id=course.id, mode_slug=CourseMode.AUDIT)
+                CourseModeFactory.create(course_id=course.id, mode_slug=CourseMode.VERIFIED)
+                course_ids.append(course.id)
+
+        self.setup_user(self.audit_user)
+
+        # These query counts were found empirically
+        query_counts = [65, 52, 52, 52, 52, 52, 52, 52, 52, 52, 22]
+        ordered_course_ids = sorted([str(cid) for cid in (course_ids + [c.id for c in self.courses])])
+
+        self.clear_caches()
+
+        for page in range(1, 12):
+            RequestCache.clear_all_namespaces()
+            with self.assertNumQueries(query_counts[page - 1]):
+                response = self.verify_response(params={'page': page, 'page_size': 30})
+
+                self.assertIn('results', response.data)
+                self.assertEqual(response.data['pagination']['count'], 303)
+                self.assertEqual(len(response.data['results']), 30 if page < 11 else 3)
+                self.assertEqual(
+                    [c['id'] for c in response.data['results']],
+                    ordered_course_ids[(page - 1) * 30:page * 30]
+                )
diff --git a/lms/djangoapps/course_api/views.py b/lms/djangoapps/course_api/views.py
index 66743ea0798ccbddae70a3cd599f66f45549d60c..26d798634243b7eee9ec058ada9bbd4462696473 100644
--- a/lms/djangoapps/course_api/views.py
+++ b/lms/djangoapps/course_api/views.py
@@ -10,6 +10,7 @@ from rest_framework.throttling import UserRateThrottle
 
 from edx_rest_framework_extensions.paginators import NamespacedPageNumberPagination
 from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
+from openedx.core.lib.api.view_utils import LazySequence
 
 from . import USE_RATE_LIMIT_2_FOR_COURSE_LIST_API, USE_RATE_LIMIT_10_FOR_COURSE_LIST_API
 from .api import course_detail, list_courses
@@ -243,7 +244,7 @@ class CourseListView(DeveloperErrorViewMixin, ListAPIView):
 
     def get_queryset(self):
         """
-        Return a list of courses visible to the user.
+        Yield courses visible to the user.
         """
         form = CourseListGetForm(self.request.query_params, initial={'requesting_user': self.request.user})
         if not form.is_valid():
@@ -264,9 +265,12 @@ class CourseListView(DeveloperErrorViewMixin, ListAPIView):
             size=self.results_size_infinity,
         )
 
-        search_courses_ids = {course['data']['id']: True for course in search_courses['results']}
+        search_courses_ids = {course['data']['id'] for course in search_courses['results']}
 
-        return [
-            course for course in db_courses
-            if unicode(course.id) in search_courses_ids
-        ]
+        return LazySequence(
+            (
+                course for course in db_courses
+                if unicode(course.id) in search_courses_ids
+            ),
+            est_len=len(db_courses)
+        )
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index caccff13132dcadd1c272e1924d820ca151e3c57..30461cbf1f0df94b0a0e0adcc0c370623d7e5330 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -23,7 +23,9 @@ from courseware.date_summary import (
 from courseware.masquerade import check_content_start_date_for_masquerade_user
 from courseware.model_data import FieldDataCache
 from courseware.module_render import get_module
+from course_modes.models import CourseMode
 from django.conf import settings
+from django.db.models import Prefetch
 from django.urls import reverse
 from django.http import Http404, QueryDict
 from enrollment.api import get_course_enrollment_details
@@ -35,6 +37,7 @@ from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
 from opaque_keys.edx.keys import UsageKey
 from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
 from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
+from openedx.core.lib.api.view_utils import LazySequence
 from path import Path as path
 from six import text_type
 from static_replace import replace_static_urls
@@ -451,19 +454,29 @@ def get_course_syllabus_section(course, section_key):
 
 def get_courses(user, org=None, filter_=None):
     """
-    Returns a list of courses available, sorted by course.number and optionally
-    filtered by org code (case-insensitive).
-    """
-    courses = branding.get_visible_courses(org=org, filter_=filter_)
+    Return a LazySequence of courses available, optionally filtered by org code (case-insensitive).
+    """
+    courses = branding.get_visible_courses(
+        org=org,
+        filter_=filter_,
+    ).prefetch_related(
+        Prefetch(
+            'modes',
+            queryset=CourseMode.objects.exclude(mode_slug__in=CourseMode.CREDIT_MODES),
+            to_attr='selectable_modes',
+        ),
+        'image_set',
+    )
 
     permission_name = configuration_helpers.get_value(
         'COURSE_CATALOG_VISIBILITY_PERMISSION',
         settings.COURSE_CATALOG_VISIBILITY_PERMISSION
     )
 
-    courses = [c for c in courses if has_access(user, permission_name, c)]
-
-    return courses
+    return LazySequence(
+        (c for c in courses if has_access(user, permission_name, c)),
+        est_len=courses.count()
+    )
 
 
 def get_permission_for_course_about():
diff --git a/lms/djangoapps/courseware/tests/test_courses.py b/lms/djangoapps/courseware/tests/test_courses.py
index c54d98e4e204ecaadbf48157496057fb67a86c70..12d694524c993368acc4d73ac1c1801603b86914 100644
--- a/lms/djangoapps/courseware/tests/test_courses.py
+++ b/lms/djangoapps/courseware/tests/test_courses.py
@@ -141,7 +141,7 @@ class CoursesTest(ModuleStoreTestCase):
 
             # Request filtering for an org distinct from the designated org.
             no_courses = get_courses(user, org=primary)
-            self.assertEqual(no_courses, [])
+            self.assertEqual(list(no_courses), [])
 
             # Request filtering for an org matching the designated org.
             site_courses = get_courses(user, org=alternate)
diff --git a/openedx/core/djangoapps/config_model_utils/models.py b/openedx/core/djangoapps/config_model_utils/models.py
index 7e47db7d1b3b500ef622b4af873fcab391b9746d..2d29ac360557b1987f4ee20684d21d95030c0c14 100644
--- a/openedx/core/djangoapps/config_model_utils/models.py
+++ b/openedx/core/djangoapps/config_model_utils/models.py
@@ -22,6 +22,7 @@ import crum
 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
+from openedx.core.lib.cache_utils import request_cached
 
 
 class Provenance(Enum):
@@ -252,7 +253,9 @@ class StackedConfigurationModel(ConfigurationModel):
         return course_key.org
 
     @classmethod
+    @request_cached()
     def _site_from_org(cls, org):
+
         configuration = SiteConfiguration.get_configuration_for_org(org, select_related=['site'])
         if configuration is None:
             try:
diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py
index e852cce1b05343bbd815231794e2b6dd41e3075d..6d1998c92f06a29d052b97fa0a22dd641d264d0e 100644
--- a/openedx/core/djangoapps/content/course_overviews/models.py
+++ b/openedx/core/djangoapps/content/course_overviews/models.py
@@ -409,7 +409,8 @@ class CourseOverview(TimeStampedModel):
         migrate and test switching to display_name_with_default, which is no
         longer escaped.
         """
-        return block_metadata_utils.display_name_with_default_escaped(self)
+        # pylint: disable=line-too-long
+        return block_metadata_utils.display_name_with_default_escaped(self)  # xss-lint: disable=python-deprecated-display-name
 
     @property
     def dashboard_start_display(self):
@@ -560,7 +561,7 @@ class CourseOverview(TimeStampedModel):
     @classmethod
     def get_all_courses(cls, orgs=None, filter_=None):
         """
-        Returns all CourseOverview objects in the database.
+        Return a queryset containing all CourseOverview objects in the database.
 
         Arguments:
             orgs (list[string]): Optional parameter that allows case-insensitive
diff --git a/openedx/core/djangolib/testing/utils.py b/openedx/core/djangolib/testing/utils.py
index 710f5a35ba75780efd098cbbd83b1ed7a536b266..3d620385e8d496ec333a4757d0c916e44d0c55b3 100644
--- a/openedx/core/djangolib/testing/utils.py
+++ b/openedx/core/djangolib/testing/utils.py
@@ -73,6 +73,9 @@ class CacheIsolationMixin(object):
                     'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
                     'LOCATION': cache_name,
                     'KEY_FUNCTION': 'util.memcache.safe_key',
+                    'OPTIONS': {
+                        'MAX_ENTRIES': 1000,
+                    },
                 } for cache_name in cls.ENABLED_CACHES
             })
 
diff --git a/openedx/core/lib/api/view_utils.py b/openedx/core/lib/api/view_utils.py
index ef67c4eb82819d0b2f9e2d4d5756aacfcdf9e7fc..61c8dbd319270bfc8d998097ae653c7f78aa9e61 100644
--- a/openedx/core/lib/api/view_utils.py
+++ b/openedx/core/lib/api/view_utils.py
@@ -1,6 +1,7 @@
 """
 Utilities related to API views
 """
+from collections import Sequence
 from django.core.exceptions import NON_FIELD_ERRORS, ObjectDoesNotExist, ValidationError
 from django.http import Http404
 from django.utils.translation import ugettext as _
@@ -205,3 +206,100 @@ class RetrievePatchAPIView(RetrieveModelMixin, UpdateModelMixin, GenericAPIView)
                 # PATCH requests where the object does not exist should still
                 # return a 404 response.
                 raise
+
+
+class LazySequence(Sequence):
+    """
+    This class provides an immutable Sequence interface on top of an existing
+    iterable.
+
+    It is immutable, and accepts an estimated length in order to support __len__
+    without exhausting the underlying sequence
+    """
+    def __init__(self, iterable, est_len=None):  # pylint: disable=super-init-not-called
+        self.iterable = iterable
+        self.est_len = est_len
+        self._data = []
+        self._exhausted = False
+
+    def __len__(self):
+        # Return the actual data length if we know it exactly (because
+        # the underlying sequence is exhausted), or it's greater than
+        # the initial estimated length
+        if len(self._data) > self.est_len or self._exhausted:
+            return len(self._data)
+        else:
+            return self.est_len
+
+    def __iter__(self):
+        # Yield all the known data first
+        for item in self._data:
+            yield item
+
+        # Capture and yield data from the underlying iterator
+        # until it is exhausted
+        while True:
+            try:
+                item = next(self.iterable)
+                self._data.append(item)
+                yield item
+            except StopIteration:
+                self._exhausted = True
+                return
+
+    def __getitem__(self, index):
+        if isinstance(index, int):
+            # For a single index, if we haven't already loaded enough
+            # data, we can load data until we have enough, and then
+            # return the value from the loaded data
+            if index < 0:
+                raise IndexError("Negative indexes aren't supported")
+
+            while len(self._data) <= index:
+                try:
+                    self._data.append(next(self.iterable))
+                except StopIteration:
+                    self._exhausted = True
+                    raise IndexError("Underlying sequence exhausted")
+
+            return self._data[index]
+        elif isinstance(index, slice):
+            # For a slice, we can load data until we reach 'stop'.
+            # Once we have data including 'stop', then we can use
+            # the underlying list to actually understand the mechanics
+            # of the slicing operation.
+            if index.start is not None and index.start < 0:
+                raise IndexError("Negative indexes aren't supported")
+            if index.stop is not None and index.stop < 0:
+                raise IndexError("Negative indexes aren't supported")
+
+            if index.step is not None and index.step < 0:
+                largest_value = index.start + 1
+            else:
+                largest_value = index.stop
+
+            if largest_value is not None:
+                while len(self._data) <= largest_value:
+                    try:
+                        self._data.append(next(self.iterable))
+                    except StopIteration:
+                        self._exhausted = True
+                        break
+            else:
+                self._data.extend(self.iterable)
+            return self._data[index]
+        else:
+            raise TypeError("Unsupported index type")
+
+    def __repr__(self):
+        if self._exhausted:
+            return "LazySequence({!r}, {!r})".format(
+                self._data,
+                self.est_len,
+            )
+        else:
+            return "LazySequence(itertools.chain({!r}, {!r}), {!r})".format(
+                self._data,
+                self.iterable,
+                self.est_len,
+            )
diff --git a/openedx/core/lib/cache_utils.py b/openedx/core/lib/cache_utils.py
index ba4324143d106d9657c751a489c1f4e1c6e30801..3910d33f7c09bb2af8e043f4d7a37f0c6549af2c 100644
--- a/openedx/core/lib/cache_utils.py
+++ b/openedx/core/lib/cache_utils.py
@@ -9,6 +9,7 @@ import zlib
 
 from django.utils.encoding import force_text
 from edx_django_utils.cache import RequestCache
+import wrapt
 
 
 def request_cached(namespace=None, arg_map_function=None, request_cache_getter=None):
@@ -47,38 +48,32 @@ def request_cached(namespace=None, arg_map_function=None, request_cache_getter=N
               cache the value it returns, and return that cached value for subsequent calls with the
               same args/kwargs within a single request.
     """
-    def decorator(f):
+    @wrapt.decorator
+    def decorator(wrapped, instance, args, kwargs):
         """
         Arguments:
-            f (func): the function to wrap
+            args, kwargs: values passed into the wrapped function
         """
-        @functools.wraps(f)
-        def _decorator(*args, **kwargs):
-            """
-            Arguments:
-                args, kwargs: values passed into the wrapped function
-            """
-            # Check to see if we have a result in cache.  If not, invoke our wrapped
-            # function.  Cache and return the result to the caller.
-            if request_cache_getter:
-                request_cache = request_cache_getter(args, kwargs)
-            else:
-                request_cache = RequestCache(namespace)
-
-            if request_cache:
-                cache_key = _func_call_cache_key(f, arg_map_function, *args, **kwargs)
-                cached_response = request_cache.get_cached_response(cache_key)
-                if cached_response.is_found:
-                    return cached_response.value
-
-            result = f(*args, **kwargs)
-
-            if request_cache:
-                request_cache.set(cache_key, result)
-
-            return result
-
-        return _decorator
+        # Check to see if we have a result in cache.  If not, invoke our wrapped
+        # function.  Cache and return the result to the caller.
+        if request_cache_getter:
+            request_cache = request_cache_getter(args if instance is None else (instance,) + args, kwargs)
+        else:
+            request_cache = RequestCache(namespace)
+
+        if request_cache:
+            cache_key = _func_call_cache_key(wrapped, arg_map_function, *args, **kwargs)
+            cached_response = request_cache.get_cached_response(cache_key)
+            if cached_response.is_found:
+                return cached_response.value
+
+        result = wrapped(*args, **kwargs)
+
+        if request_cache:
+            request_cache.set(cache_key, result)
+
+        return result
+
     return decorator
 
 
diff --git a/openedx/features/content_type_gating/tests/test_models.py b/openedx/features/content_type_gating/tests/test_models.py
index e51a16b5298685afacd42f387e8cb343403685e5..aca44f17bcf62e589cb3228f26197fdca7781843 100644
--- a/openedx/features/content_type_gating/tests/test_models.py
+++ b/openedx/features/content_type_gating/tests/test_models.py
@@ -6,6 +6,7 @@ from django.utils import timezone
 from mock import Mock
 import pytz
 
+from edx_django_utils.cache import RequestCache
 from opaque_keys.edx.locator import CourseLocator
 from openedx.core.djangoapps.config_model_utils.models import Provenance
 from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
@@ -233,10 +234,14 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
         global_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the global value is not retrieved from cache after save
         with self.assertNumQueries(1):
             self.assertTrue(ContentTypeGatingConfig.current().enabled)
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the global value can be retrieved from cache after read
         with self.assertNumQueries(0):
             self.assertTrue(ContentTypeGatingConfig.current().enabled)
@@ -244,6 +249,8 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         global_config.enabled = False
         global_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the global value in cache was deleted on save
         with self.assertNumQueries(1):
             self.assertFalse(ContentTypeGatingConfig.current().enabled)
@@ -253,10 +260,14 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         site_config = ContentTypeGatingConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
         site_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -264,6 +275,8 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         site_config.enabled = False
         site_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the site value in cache was deleted on save
         with self.assertNumQueries(1):
             self.assertFalse(ContentTypeGatingConfig.current(site=site_cfg.site).enabled)
@@ -271,6 +284,8 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
         global_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -281,10 +296,14 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         org_config = ContentTypeGatingConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1))
         org_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the org value is not retrieved from cache after save
         with self.assertNumQueries(2):
             self.assertTrue(ContentTypeGatingConfig.current(org=course.org).enabled)
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the org value can be retrieved from cache after read
         with self.assertNumQueries(0):
             self.assertTrue(ContentTypeGatingConfig.current(org=course.org).enabled)
@@ -292,6 +311,8 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         org_config.enabled = False
         org_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the org value in cache was deleted on save
         with self.assertNumQueries(2):
             self.assertFalse(ContentTypeGatingConfig.current(org=course.org).enabled)
@@ -299,6 +320,8 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
         global_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -306,6 +329,8 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         site_config = ContentTypeGatingConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
         site_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -316,10 +341,14 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         course_config = ContentTypeGatingConfig(course=course, enabled=True, enabled_as_of=datetime(2018, 1, 1))
         course_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -327,6 +356,8 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         course_config.enabled = False
         course_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the org value in cache was deleted on save
         with self.assertNumQueries(2):
             self.assertFalse(ContentTypeGatingConfig.current(course_key=course.id).enabled)
@@ -334,6 +365,8 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
         global_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -341,6 +374,8 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         site_config = ContentTypeGatingConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
         site_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -348,6 +383,8 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
         org_config = ContentTypeGatingConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1))
         org_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
diff --git a/openedx/features/course_duration_limits/access.py b/openedx/features/course_duration_limits/access.py
index a024475a23cd43843e52c9567d108546855e2338..7d4f1c3abed066ff51c9d22a02b7420db3402fb3 100644
--- a/openedx/features/course_duration_limits/access.py
+++ b/openedx/features/course_duration_limits/access.py
@@ -66,7 +66,9 @@ def get_user_course_expiration_date(user, course):
 
     access_duration = MIN_DURATION
 
-    if not CourseMode.verified_mode_for_course(course.id, include_expired=True):
+    verified_mode = CourseMode.verified_mode_for_course(course=course, include_expired=True)
+
+    if not verified_mode:
         return None
 
     enrollment = CourseEnrollment.get_enrollment(user, course.id)
diff --git a/openedx/features/course_duration_limits/tests/test_course_expiration.py b/openedx/features/course_duration_limits/tests/test_course_expiration.py
index a87abe0825f7d70bb071692e23f2f0d69430a3c2..fb957ab6974faadd733d259c2b34f5740206867a 100644
--- a/openedx/features/course_duration_limits/tests/test_course_expiration.py
+++ b/openedx/features/course_duration_limits/tests/test_course_expiration.py
@@ -63,7 +63,7 @@ class CourseExpirationTestCase(ModuleStoreTestCase):
     def test_enrollment_mode(self):
         """Tests that verified enrollments do not have an expiration"""
         CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED)
-        result = get_user_course_expiration_date(self.user, self.course)
+        result = get_user_course_expiration_date(self.user, CourseOverview.get_from_id(self.course.id))
         self.assertEqual(result, None)
 
     @mock.patch("openedx.features.course_duration_limits.access.get_course_run_details")
@@ -94,7 +94,10 @@ class CourseExpirationTestCase(ModuleStoreTestCase):
             self.course.self_paced = True
         mock_get_course_run_details.return_value = {'weeks_to_complete': weeks_to_complete}
         enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT)
-        result = get_user_course_expiration_date(self.user, self.course)
+        result = get_user_course_expiration_date(
+            self.user,
+            CourseOverview.get_from_id(self.course.id),
+        )
         self.assertEqual(result, enrollment.created + access_duration)
 
     @mock.patch("openedx.features.course_duration_limits.access.get_course_run_details")
@@ -109,11 +112,17 @@ class CourseExpirationTestCase(ModuleStoreTestCase):
         start_date = now() - timedelta(weeks=10)
         past_course = CourseFactory(start=start_date)
         enrollment = CourseEnrollment.enroll(self.user, past_course.id, CourseMode.AUDIT)
-        result = get_user_course_expiration_date(self.user, past_course)
+        result = get_user_course_expiration_date(
+            self.user,
+            CourseOverview.get_from_id(past_course.id),
+        )
         self.assertEqual(result, None)
 
         add_course_mode(past_course, upgrade_deadline_expired=False)
-        result = get_user_course_expiration_date(self.user, past_course)
+        result = get_user_course_expiration_date(
+            self.user,
+            CourseOverview.get_from_id(past_course.id),
+        )
         content_availability_date = enrollment.created
         self.assertEqual(result, content_availability_date + access_duration)
 
@@ -121,12 +130,18 @@ class CourseExpirationTestCase(ModuleStoreTestCase):
         start_date = now() + timedelta(weeks=10)
         future_course = CourseFactory(start=start_date)
         enrollment = CourseEnrollment.enroll(self.user, future_course.id, CourseMode.AUDIT)
-        result = get_user_course_expiration_date(self.user, future_course)
+        result = get_user_course_expiration_date(
+            self.user,
+            CourseOverview.get_from_id(future_course.id),
+        )
         self.assertEqual(result, None)
 
         add_course_mode(future_course, upgrade_deadline_expired=False)
-        result = get_user_course_expiration_date(self.user, future_course)
-        content_availability_date = start_date
+        result = get_user_course_expiration_date(
+            self.user,
+            CourseOverview.get_from_id(future_course.id),
+        )
+        content_availability_date = start_date.replace(microsecond=0)
         self.assertEqual(result, content_availability_date + access_duration)
 
     @mock.patch("openedx.features.course_duration_limits.access.get_course_run_details")
@@ -141,7 +156,10 @@ class CourseExpirationTestCase(ModuleStoreTestCase):
         course = CourseFactory(start=start_date)
         enrollment = CourseEnrollment.enroll(self.user, course.id, CourseMode.AUDIT)
         add_course_mode(course, upgrade_deadline_expired=True)
-        result = get_user_course_expiration_date(self.user, course)
+        result = get_user_course_expiration_date(
+            self.user,
+            CourseOverview.get_from_id(course.id),
+        )
         content_availability_date = enrollment.created
         self.assertEqual(result, content_availability_date + access_duration)
 
diff --git a/openedx/features/course_duration_limits/tests/test_models.py b/openedx/features/course_duration_limits/tests/test_models.py
index 6a6db087cdbf1d8cb7c0b4ea5a46b51bf0b93af1..584a4b210b9de909f0d424cbd4df1cc3202d421a 100644
--- a/openedx/features/course_duration_limits/tests/test_models.py
+++ b/openedx/features/course_duration_limits/tests/test_models.py
@@ -10,6 +10,7 @@ from django.utils import timezone
 from mock import Mock
 import pytz
 
+from edx_django_utils.cache import RequestCache
 from opaque_keys.edx.locator import CourseLocator
 from openedx.core.djangoapps.config_model_utils.models import Provenance
 from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
@@ -266,10 +267,14 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
         global_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the global value is not retrieved from cache after save
         with self.assertNumQueries(1):
             self.assertTrue(CourseDurationLimitConfig.current().enabled)
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the global value can be retrieved from cache after read
         with self.assertNumQueries(0):
             self.assertTrue(CourseDurationLimitConfig.current().enabled)
@@ -277,6 +282,8 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         global_config.enabled = False
         global_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the global value in cache was deleted on save
         with self.assertNumQueries(1):
             self.assertFalse(CourseDurationLimitConfig.current().enabled)
@@ -286,10 +293,14 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
         site_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -297,6 +308,8 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         site_config.enabled = False
         site_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the site value in cache was deleted on save
         with self.assertNumQueries(1):
             self.assertFalse(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)
@@ -304,6 +317,8 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
         global_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -314,10 +329,14 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1))
         org_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the org value is not retrieved from cache after save
         with self.assertNumQueries(2):
             self.assertTrue(CourseDurationLimitConfig.current(org=course.org).enabled)
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the org value can be retrieved from cache after read
         with self.assertNumQueries(0):
             self.assertTrue(CourseDurationLimitConfig.current(org=course.org).enabled)
@@ -325,6 +344,8 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         org_config.enabled = False
         org_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the org value in cache was deleted on save
         with self.assertNumQueries(2):
             self.assertFalse(CourseDurationLimitConfig.current(org=course.org).enabled)
@@ -332,6 +353,8 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
         global_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -339,6 +362,8 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
         site_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -349,10 +374,14 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         course_config = CourseDurationLimitConfig(course=course, enabled=True, enabled_as_of=datetime(2018, 1, 1))
         course_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -360,6 +389,8 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         course_config.enabled = False
         course_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # Check that the org value in cache was deleted on save
         with self.assertNumQueries(2):
             self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)
@@ -367,6 +398,8 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
         global_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -374,6 +407,8 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
         site_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
@@ -381,6 +416,8 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
         org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1))
         org_config.save()
 
+        RequestCache.clear_all_namespaces()
+
         # 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)
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 048ebcf8c4ebcf7161d4c8008f691e244a3bbc9a..9975da81f9f91ab5ed3f017de93729e1ca30e571 100644
--- a/openedx/features/course_experience/tests/views/test_course_home.py
+++ b/openedx/features/course_experience/tests/views/test_course_home.py
@@ -204,7 +204,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
 
         # Fetch the view and verify the query counts
         # TODO: decrease query count as part of REVO-28
-        with self.assertNumQueries(87, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
+        with self.assertNumQueries(89, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
             with check_mongo_calls(4):
                 url = course_home_url(self.course)
                 self.client.get(url)
diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py
index 5c1449c3f87fb96531eca6099dc5a82e8561f069..d3fe698adeb00222f369d9655a79b8454fd3bcaf 100644
--- a/openedx/features/course_experience/views/course_home.py
+++ b/openedx/features/course_experience/views/course_home.py
@@ -25,6 +25,7 @@ from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
 from lms.djangoapps.courseware.views.views import CourseTabView
 from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
 from openedx.core.djangoapps.util.maintenance_banner import add_maintenance_banner
+from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
 from openedx.features.course_experience.course_tools import CourseToolsPluginManager
 from openedx.features.course_duration_limits.access import generate_course_expired_fragment
 from student.models import CourseEnrollment
@@ -153,7 +154,10 @@ class CourseHomeFragmentView(EdxFragmentView):
             course_sock_fragment = CourseSockFragmentView().render_to_fragment(request, course=course, **kwargs)
             has_visited_course, resume_course_url = self._get_resume_course_info(request, course_id)
             handouts_html = self._get_course_handouts(request, course)
-            course_expiration_fragment = generate_course_expired_fragment(request.user, course)
+            course_expiration_fragment = generate_course_expired_fragment(
+                request.user,
+                CourseOverview.get_from_id(course.id)
+            )
         elif allow_public_outline or allow_public:
             outline_fragment = CourseOutlineFragmentView().render_to_fragment(
                 request, course_id=course_id, user_is_enrolled=False, **kwargs