From c4cf0b9bb7a37446fb17bf675f9010aaf73db318 Mon Sep 17 00:00:00 2001
From: Matt Tuchfarber <mtuchfarber@edx.org>
Date: Mon, 17 Aug 2020 15:42:49 -0400
Subject: [PATCH] Refactor program type enrollment checks

Checking if a user was enrolled in a program type was using the `name`
field which is subject to be translated. This change allows for us to check by
the type's slug which will be constant. This also includes the addition
of api.py files for the course_modes, catalog, and programs apps.
---
 common/djangoapps/course_modes/api.py         | 17 ++++++
 common/djangoapps/course_modes/api/urls.py    | 12 -----
 .../{api => rest_api}/__init__.py             |  0
 .../{api => rest_api}/serializers.py          |  0
 .../djangoapps/course_modes/rest_api/urls.py  | 12 +++++
 .../{api => rest_api}/v1/__init__.py          |  0
 .../{api => rest_api}/v1/tests/__init__.py    |  0
 .../{api => rest_api}/v1/tests/test_views.py  |  5 +-
 .../course_modes/{api => rest_api}/v1/urls.py |  2 +-
 .../{api => rest_api}/v1/views.py             |  2 +-
 lms/urls.py                                   |  3 +-
 openedx/core/djangoapps/catalog/api.py        | 21 ++++++++
 openedx/core/djangoapps/catalog/cache.py      |  4 ++
 .../management/commands/cache_programs.py     | 18 +++++++
 .../commands/tests/test_cache_programs.py     |  6 +++
 .../djangoapps/catalog/tests/test_utils.py    | 53 +++++++++++++++----
 openedx/core/djangoapps/catalog/utils.py      | 24 +++++++++
 openedx/core/djangoapps/programs/api.py       | 30 +++++++++++
 .../djangoapps/programs/tests/test_utils.py   | 10 ++--
 openedx/core/djangoapps/programs/utils.py     | 20 ++++---
 20 files changed, 200 insertions(+), 39 deletions(-)
 create mode 100644 common/djangoapps/course_modes/api.py
 delete mode 100644 common/djangoapps/course_modes/api/urls.py
 rename common/djangoapps/course_modes/{api => rest_api}/__init__.py (100%)
 rename common/djangoapps/course_modes/{api => rest_api}/serializers.py (100%)
 create mode 100644 common/djangoapps/course_modes/rest_api/urls.py
 rename common/djangoapps/course_modes/{api => rest_api}/v1/__init__.py (100%)
 rename common/djangoapps/course_modes/{api => rest_api}/v1/tests/__init__.py (100%)
 rename common/djangoapps/course_modes/{api => rest_api}/v1/tests/test_views.py (99%)
 rename common/djangoapps/course_modes/{api => rest_api}/v1/urls.py (92%)
 rename common/djangoapps/course_modes/{api => rest_api}/v1/views.py (99%)
 create mode 100644 openedx/core/djangoapps/catalog/api.py
 create mode 100644 openedx/core/djangoapps/programs/api.py

diff --git a/common/djangoapps/course_modes/api.py b/common/djangoapps/course_modes/api.py
new file mode 100644
index 00000000000..2dce97f412c
--- /dev/null
+++ b/common/djangoapps/course_modes/api.py
@@ -0,0 +1,17 @@
+"""
+Python APIs exposed by the course_modes app to other in-process apps.
+"""
+
+from course_modes.models import CourseMode as _CourseMode
+
+
+def get_paid_modes_for_course(course_run_id):
+    """
+    Returns a list of non-expired mode slugs for a course run ID that have a set minimum price.
+
+    Params:
+        course_run_id (CourseKey): The course run you want to get the paid modes for
+    Returns:
+        A list of paid modes (strings) that the course has attached to it.
+    """
+    return _CourseMode.paid_modes_for_course(course_run_id)
diff --git a/common/djangoapps/course_modes/api/urls.py b/common/djangoapps/course_modes/api/urls.py
deleted file mode 100644
index e4cf046defb..00000000000
--- a/common/djangoapps/course_modes/api/urls.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""
-URL definitions for the course_modes API.
-"""
-
-
-from django.conf.urls import include, url
-
-app_name = 'common.djangoapps.course_modes.api'
-
-urlpatterns = [
-    url(r'^v1/', include('course_modes.api.v1.urls', namespace='v1')),
-]
diff --git a/common/djangoapps/course_modes/api/__init__.py b/common/djangoapps/course_modes/rest_api/__init__.py
similarity index 100%
rename from common/djangoapps/course_modes/api/__init__.py
rename to common/djangoapps/course_modes/rest_api/__init__.py
diff --git a/common/djangoapps/course_modes/api/serializers.py b/common/djangoapps/course_modes/rest_api/serializers.py
similarity index 100%
rename from common/djangoapps/course_modes/api/serializers.py
rename to common/djangoapps/course_modes/rest_api/serializers.py
diff --git a/common/djangoapps/course_modes/rest_api/urls.py b/common/djangoapps/course_modes/rest_api/urls.py
new file mode 100644
index 00000000000..349efb8a3f2
--- /dev/null
+++ b/common/djangoapps/course_modes/rest_api/urls.py
@@ -0,0 +1,12 @@
+"""
+URL definitions for the course_modes API.
+"""
+
+
+from django.conf.urls import include, url
+
+app_name = 'common.djangoapps.course_modes.rest_api'
+
+urlpatterns = [
+    url(r'^v1/', include('course_modes.rest_api.v1.urls', namespace='v1')),
+]
diff --git a/common/djangoapps/course_modes/api/v1/__init__.py b/common/djangoapps/course_modes/rest_api/v1/__init__.py
similarity index 100%
rename from common/djangoapps/course_modes/api/v1/__init__.py
rename to common/djangoapps/course_modes/rest_api/v1/__init__.py
diff --git a/common/djangoapps/course_modes/api/v1/tests/__init__.py b/common/djangoapps/course_modes/rest_api/v1/tests/__init__.py
similarity index 100%
rename from common/djangoapps/course_modes/api/v1/tests/__init__.py
rename to common/djangoapps/course_modes/rest_api/v1/tests/__init__.py
diff --git a/common/djangoapps/course_modes/api/v1/tests/test_views.py b/common/djangoapps/course_modes/rest_api/v1/tests/test_views.py
similarity index 99%
rename from common/djangoapps/course_modes/api/v1/tests/test_views.py
rename to common/djangoapps/course_modes/rest_api/v1/tests/test_views.py
index da5b881b80a..8a079f9bbd2 100644
--- a/common/djangoapps/course_modes/api/v1/tests/test_views.py
+++ b/common/djangoapps/course_modes/rest_api/v1/tests/test_views.py
@@ -5,7 +5,6 @@ Tests for the course modes API.
 
 import json
 import unittest
-from itertools import product
 
 import ddt
 from django.conf import settings
@@ -15,7 +14,7 @@ from rest_framework import status
 from rest_framework.test import APITestCase
 from six import text_type
 
-from course_modes.api.v1.views import CourseModesView
+from course_modes.rest_api.v1.views import CourseModesView
 from course_modes.models import CourseMode
 from course_modes.tests.factories import CourseModeFactory
 from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
@@ -79,7 +78,7 @@ class CourseModesViewTestBase(AuthAndScopesTestMixin):
         """
         Required method to implement AuthAndScopesTestMixin.
         """
-        pass
+        pass  # pylint: disable=unnecessary-pass
 
     @ddt.data(*JWT_AUTH_TYPES)
     def test_jwt_on_behalf_of_user(self, auth_type):
diff --git a/common/djangoapps/course_modes/api/v1/urls.py b/common/djangoapps/course_modes/rest_api/v1/urls.py
similarity index 92%
rename from common/djangoapps/course_modes/api/v1/urls.py
rename to common/djangoapps/course_modes/rest_api/v1/urls.py
index 8933b3a4ac6..7dd28e36fdd 100644
--- a/common/djangoapps/course_modes/api/v1/urls.py
+++ b/common/djangoapps/course_modes/rest_api/v1/urls.py
@@ -6,7 +6,7 @@ URL definitions for the course_modes v1 API.
 from django.conf import settings
 from django.conf.urls import url
 
-from course_modes.api.v1 import views
+from course_modes.rest_api.v1 import views
 
 app_name = 'v1'
 
diff --git a/common/djangoapps/course_modes/api/v1/views.py b/common/djangoapps/course_modes/rest_api/v1/views.py
similarity index 99%
rename from common/djangoapps/course_modes/api/v1/views.py
rename to common/djangoapps/course_modes/rest_api/v1/views.py
index 056bc103324..e8d963e986d 100644
--- a/common/djangoapps/course_modes/api/v1/views.py
+++ b/common/djangoapps/course_modes/rest_api/v1/views.py
@@ -14,7 +14,7 @@ from rest_framework import status
 from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView
 from rest_framework.response import Response
 
-from course_modes.api.serializers import CourseModeSerializer
+from course_modes.rest_api.serializers import CourseModeSerializer
 from course_modes.models import CourseMode
 from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
 from openedx.core.lib.api.parsers import MergePatchParser
diff --git a/lms/urls.py b/lms/urls.py
index 9e2c8b85673..93a00d1be3b 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -156,8 +156,9 @@ urlpatterns = [
 
     # Multiple course modes and identity verification
     url(r'^course_modes/', include('course_modes.urls')),
-    url(r'^api/course_modes/', include(('course_modes.api.urls', 'common.djangoapps.course_mods'),
+    url(r'^api/course_modes/', include(('course_modes.rest_api.urls', 'common.djangoapps.course_mods'),
                                        namespace='course_modes_api')),
+
     url(r'^verify_student/', include('verify_student.urls')),
 
     # URLs for managing dark launches of languages
diff --git a/openedx/core/djangoapps/catalog/api.py b/openedx/core/djangoapps/catalog/api.py
new file mode 100644
index 00000000000..74d6b548040
--- /dev/null
+++ b/openedx/core/djangoapps/catalog/api.py
@@ -0,0 +1,21 @@
+"""
+Python APIs exposed by the catalog app to other in-process apps.
+"""
+
+from .utils import get_programs_by_type_slug as _get_programs_by_type_slug
+
+
+def get_programs_by_type(site, program_type_slug):
+    """
+    Retrieves a list of programs for the site whose type's slug matches the parameter.
+    Slug is used is used as a consistent value to check since ProgramType.name is
+    a field that gets translated.
+
+    Params:
+        site (Site): The corresponding Site object to fetch programs for.
+        program_type_slug (string): The type slug that matching programs must have.
+
+    Returns:
+        A list of programs (dicts) for the given site with the given type slug
+    """
+    return _get_programs_by_type_slug(site, program_type_slug)
diff --git a/openedx/core/djangoapps/catalog/cache.py b/openedx/core/djangoapps/catalog/cache.py
index 25fa7a2515a..8f3a302870f 100644
--- a/openedx/core/djangoapps/catalog/cache.py
+++ b/openedx/core/djangoapps/catalog/cache.py
@@ -22,5 +22,9 @@ CATALOG_COURSE_PROGRAMS_CACHE_KEY_TPL = 'catalog-course-programs-{course_uuid}'
 # that live in the same environment).
 PROGRAMS_BY_TYPE_CACHE_KEY_TPL = 'programs-by-type-{site_id}-{program_type}'
 
+# Site-aware cache key template used to locate an item containing
+# a list of all program UUIDs with a certain program slug
+PROGRAMS_BY_TYPE_SLUG_CACHE_KEY_TPL = 'programs-by-type-slug-{site_id}-{program_slug}'
+
 # Template used to create cache keys for organization to program uuids.
 PROGRAMS_BY_ORGANIZATION_CACHE_KEY_TPL = 'organization-programs-{org_key}'
diff --git a/openedx/core/djangoapps/catalog/management/commands/cache_programs.py b/openedx/core/djangoapps/catalog/management/commands/cache_programs.py
index b8c78b371f8..b16796fa068 100644
--- a/openedx/core/djangoapps/catalog/management/commands/cache_programs.py
+++ b/openedx/core/djangoapps/catalog/management/commands/cache_programs.py
@@ -18,6 +18,7 @@ from openedx.core.djangoapps.catalog.cache import (
     PROGRAM_CACHE_KEY_TPL,
     PROGRAMS_BY_ORGANIZATION_CACHE_KEY_TPL,
     PROGRAMS_BY_TYPE_CACHE_KEY_TPL,
+    PROGRAMS_BY_TYPE_SLUG_CACHE_KEY_TPL,
     SITE_PATHWAY_IDS_CACHE_KEY_TPL,
     SITE_PROGRAM_UUIDS_CACHE_KEY_TPL
 )
@@ -64,6 +65,7 @@ class Command(BaseCommand):
         courses = {}
         catalog_courses = {}
         programs_by_type = {}
+        programs_by_type_slug = {}
         organizations = {}
         for site in Site.objects.all():
             site_config = getattr(site, 'configuration', None)
@@ -93,6 +95,7 @@ class Command(BaseCommand):
             courses.update(self.get_courses(new_programs))
             catalog_courses.update(self.get_catalog_courses(new_programs))
             programs_by_type.update(self.get_programs_by_type(site, new_programs))
+            programs_by_type_slug.update(self.get_programs_by_type_slug(site, new_programs))
             organizations.update(self.get_programs_by_organization(new_programs))
 
             logger.info(u'Caching UUIDs for {total} programs for site {site_name}.'.format(
@@ -123,6 +126,9 @@ class Command(BaseCommand):
         logger.info(text_type('Caching program UUIDs by {} program types.'.format(len(programs_by_type))))
         cache.set_many(programs_by_type, None)
 
+        logger.info(text_type('Caching program UUIDs by {} program type slugs.'.format(len(programs_by_type_slug))))
+        cache.set_many(programs_by_type_slug, None)
+
         logger.info(u'Caching programs uuids for {} organizations'.format(len(organizations)))
         cache.set_many(organizations, None)
 
@@ -263,6 +269,18 @@ class Command(BaseCommand):
             programs_by_type[cache_key].append(program['uuid'])
         return programs_by_type
 
+    def get_programs_by_type_slug(self, site, programs):
+        """
+        Returns a dictionary mapping site-aware cache keys corresponding to program types
+        to lists of program uuids with that type.
+        """
+        programs_by_type_slug = defaultdict(list)
+        for program in programs.values():
+            program_slug = program.get('type_attrs', {}).get('slug')
+            cache_key = PROGRAMS_BY_TYPE_SLUG_CACHE_KEY_TPL.format(site_id=site.id, program_slug=program_slug)
+            programs_by_type_slug[cache_key].append(program['uuid'])
+        return programs_by_type_slug
+
     def get_programs_by_organization(self, programs):
         """
         Returns a dictionary mapping organization keys to lists of program uuids authored by that org
diff --git a/openedx/core/djangoapps/catalog/management/commands/tests/test_cache_programs.py b/openedx/core/djangoapps/catalog/management/commands/tests/test_cache_programs.py
index e1c6bd9216a..e7ceef8e533 100644
--- a/openedx/core/djangoapps/catalog/management/commands/tests/test_cache_programs.py
+++ b/openedx/core/djangoapps/catalog/management/commands/tests/test_cache_programs.py
@@ -14,6 +14,7 @@ from openedx.core.djangoapps.catalog.cache import (
     PATHWAY_CACHE_KEY_TPL,
     PROGRAM_CACHE_KEY_TPL,
     PROGRAMS_BY_TYPE_CACHE_KEY_TPL,
+    PROGRAMS_BY_TYPE_SLUG_CACHE_KEY_TPL,
     SITE_PATHWAY_IDS_CACHE_KEY_TPL,
     SITE_PROGRAM_UUIDS_CACHE_KEY_TPL
 )
@@ -199,10 +200,15 @@ class TestCachePrograms(CatalogIntegrationMixin, CacheIsolationTestCase, SiteMix
         # program UUIDS by program type and a cached list of UUIDs by authoring organization
         for program in self.programs:
             program_type = normalize_program_type(program.get('type', 'None'))
+            program_type_slug = program.get('type_attrs', {}).get('slug')
             program_type_cache_key = PROGRAMS_BY_TYPE_CACHE_KEY_TPL.format(
                 site_id=self.site.id, program_type=program_type
             )
+            program_type_slug_cache_key = PROGRAMS_BY_TYPE_SLUG_CACHE_KEY_TPL.format(
+                site_id=self.site.id, program_slug=program_type_slug
+            )
             self.assertIn(program['uuid'], cache.get(program_type_cache_key))
+            self.assertIn(program['uuid'], cache.get(program_type_slug_cache_key))
 
             for organization in program['authoring_organizations']:
                 organization_cache_key = PROGRAMS_BY_ORGANIZATION_CACHE_KEY_TPL.format(
diff --git a/openedx/core/djangoapps/catalog/tests/test_utils.py b/openedx/core/djangoapps/catalog/tests/test_utils.py
index 7886b7148c7..f0216de2da5 100644
--- a/openedx/core/djangoapps/catalog/tests/test_utils.py
+++ b/openedx/core/djangoapps/catalog/tests/test_utils.py
@@ -24,6 +24,7 @@ from openedx.core.djangoapps.catalog.cache import (
     PATHWAY_CACHE_KEY_TPL,
     PROGRAM_CACHE_KEY_TPL,
     PROGRAMS_BY_TYPE_CACHE_KEY_TPL,
+    PROGRAMS_BY_TYPE_SLUG_CACHE_KEY_TPL,
     SITE_PATHWAY_IDS_CACHE_KEY_TPL,
     SITE_PROGRAM_UUIDS_CACHE_KEY_TPL
 )
@@ -33,7 +34,8 @@ from openedx.core.djangoapps.catalog.tests.factories import (
     CourseRunFactory,
     PathwayFactory,
     ProgramFactory,
-    ProgramTypeFactory
+    ProgramTypeFactory,
+    ProgramTypeAttrsFactory
 )
 from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
 from openedx.core.djangoapps.catalog.utils import (
@@ -50,6 +52,7 @@ from openedx.core.djangoapps.catalog.utils import (
     get_program_types,
     get_programs,
     get_programs_by_type,
+    get_programs_by_type_slug,
     get_visible_sessions_for_entitlement,
     normalize_program_type,
 )
@@ -830,7 +833,7 @@ class TestProgramCourseRunCrawling(TestCase):
 
 @skip_unless_lms
 class TestGetProgramsByType(CacheIsolationTestCase):
-    """ Test for the ``get_programs_by_type()`` function. """
+    """ Test for the ``get_programs_by_type()`` and the ``get_programs_by_type_slug()`` functions. """
     ENABLED_CACHES = ['default']
 
     @classmethod
@@ -839,11 +842,26 @@ class TestGetProgramsByType(CacheIsolationTestCase):
         super(TestGetProgramsByType, cls).setUpClass()
         cls.site = SiteFactory()
         cls.other_site = SiteFactory()
-        cls.masters_program_1 = ProgramFactory.create(type='Masters')
-        cls.masters_program_2 = ProgramFactory.create(type='Masters')
-        cls.masters_program_other_site = ProgramFactory.create(type='Masters')
-        cls.bachelors_program = ProgramFactory.create(type='Bachelors')
-        cls.no_type_program = ProgramFactory.create(type=None)
+        cls.masters_program_1 = ProgramFactory.create(
+            type='Masters',
+            type_attrs=ProgramTypeAttrsFactory.create(slug="masters")
+        )
+        cls.masters_program_2 = ProgramFactory.create(
+            type='Masters',
+            type_attrs=ProgramTypeAttrsFactory.create(slug="masters")
+        )
+        cls.masters_program_other_site = ProgramFactory.create(
+            type='Masters',
+            type_attrs=ProgramTypeAttrsFactory.create(slug="masters")
+        )
+        cls.bachelors_program = ProgramFactory.create(
+            type='Bachelors',
+            type_attrs=ProgramTypeAttrsFactory.create(slug="bachelors")
+        )
+        cls.no_type_program = ProgramFactory.create(
+            type=None,
+            type_attrs=None
+        )
 
     def setUp(self):
         """ Loads program data into the cache before each test function. """
@@ -865,41 +883,58 @@ class TestGetProgramsByType(CacheIsolationTestCase):
         cache.set_many(cached_programs, None)
 
         programs_by_type = defaultdict(list)
+        programs_by_type_slug = defaultdict(list)
         for program in all_programs:
             program_type = normalize_program_type(program.get('type'))
+            program_type_slug = (program.get('type_attrs') or {}).get('slug')
             site_id = self.site.id
 
             if program == self.masters_program_other_site:
                 site_id = self.other_site.id
 
-            cache_key = PROGRAMS_BY_TYPE_CACHE_KEY_TPL.format(site_id=site_id, program_type=program_type)
-            programs_by_type[cache_key].append(program['uuid'])
+            program_type_cache_key = PROGRAMS_BY_TYPE_CACHE_KEY_TPL.format(
+                site_id=site_id,
+                program_type=program_type
+            )
+            program_type_slug_cache_key = PROGRAMS_BY_TYPE_SLUG_CACHE_KEY_TPL.format(
+                site_id=site_id,
+                program_slug=program_type_slug
+            )
+            programs_by_type[program_type_cache_key].append(program['uuid'])
+            programs_by_type_slug[program_type_slug_cache_key].append(program['uuid'])
 
         cache.set_many(programs_by_type, None)
+        cache.set_many(programs_by_type_slug, None)
 
     def test_get_masters_programs(self):
         expected_programs = [self.masters_program_1, self.masters_program_2]
         six.assertCountEqual(self, expected_programs, get_programs_by_type(self.site, 'masters'))
+        six.assertCountEqual(self, expected_programs, get_programs_by_type_slug(self.site, 'masters'))
 
     def test_get_bachelors_programs(self):
         expected_programs = [self.bachelors_program]
         self.assertEqual(expected_programs, get_programs_by_type(self.site, 'bachelors'))
+        self.assertEqual(expected_programs, get_programs_by_type_slug(self.site, 'bachelors'))
 
     def test_get_no_such_type_programs(self):
         expected_programs = []
         self.assertEqual(expected_programs, get_programs_by_type(self.site, 'doctorate'))
+        self.assertEqual(expected_programs, get_programs_by_type_slug(self.site, 'doctorate'))
 
     def test_get_masters_programs_other_site(self):
         expected_programs = [self.masters_program_other_site]
         self.assertEqual(expected_programs, get_programs_by_type(self.other_site, 'masters'))
+        self.assertEqual(expected_programs, get_programs_by_type_slug(self.other_site, 'masters'))
 
     def test_get_programs_null_type(self):
         expected_programs = [self.no_type_program]
         self.assertEqual(expected_programs, get_programs_by_type(self.site, None))
+        self.assertEqual(expected_programs, get_programs_by_type_slug(self.site, None))
 
     def test_get_programs_false_type(self):
         expected_programs = []
         self.assertEqual(expected_programs, get_programs_by_type(self.site, False))
+        self.assertEqual(expected_programs, get_programs_by_type_slug(self.site, False))
 
     def test_normalize_program_type(self):
         self.assertEqual('none', normalize_program_type(None))
diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py
index e87e7442842..2d4b8f8f39b 100644
--- a/openedx/core/djangoapps/catalog/utils.py
+++ b/openedx/core/djangoapps/catalog/utils.py
@@ -23,6 +23,7 @@ from openedx.core.djangoapps.catalog.cache import (
     PATHWAY_CACHE_KEY_TPL,
     PROGRAM_CACHE_KEY_TPL,
     PROGRAMS_BY_TYPE_CACHE_KEY_TPL,
+    PROGRAMS_BY_TYPE_SLUG_CACHE_KEY_TPL,
     SITE_PATHWAY_IDS_CACHE_KEY_TPL,
     SITE_PROGRAM_UUIDS_CACHE_KEY_TPL
 )
@@ -163,6 +164,29 @@ def get_programs_by_type(site, program_type):
     return get_programs_by_uuids(uuids)
 
 
+def get_programs_by_type_slug(site, program_type_slug):
+    """
+    Keyword Arguments:
+        site (Site): The corresponding Site object to fetch programs for.
+        program_type_slug (string): The type slug that matching programs must have.
+
+    Returns:
+        A list of programs for the given site with the given type slug.
+
+    Slugs are a consistent identifier whereas type (used in `get_programs_by_type`)
+    may be translated.
+    """
+    program_type_slug_cache_key = PROGRAMS_BY_TYPE_SLUG_CACHE_KEY_TPL.format(
+        site_id=site.id, program_slug=program_type_slug
+    )
+    uuids = cache.get(program_type_slug_cache_key, [])
+    if not uuids:
+        logger.warning(text_type(
+            'Failed to get program UUIDs from cache for site {} and type slug {}'.format(site.id, program_type_slug)
+        ))
+    return get_programs_by_uuids(uuids)
+
+
 def get_programs_by_uuids(uuids):
     """
     Gets a list of programs for the provided uuids
diff --git a/openedx/core/djangoapps/programs/api.py b/openedx/core/djangoapps/programs/api.py
new file mode 100644
index 00000000000..48c680d606e
--- /dev/null
+++ b/openedx/core/djangoapps/programs/api.py
@@ -0,0 +1,30 @@
+"""
+Python APIs exposed by the Programs app to other in-process apps.
+"""
+
+from .utils import is_user_enrolled_in_program_type as _is_user_enrolled_in_program_type
+
+
+def is_user_enrolled_in_program_type(user, program_type_slug, paid_modes=False, enrollments=None, entitlements=None):
+    """
+    This method will look at the learners Enrollments and Entitlements to determine
+    if a learner is enrolled in a Program of the given type.
+
+    NOTE: This method relies on the Program Cache right now. The goal is to move away from this
+    in the future.
+
+    Arguments:
+        user (User): The user we are looking for.
+        program_type_slug (str): The slug of the Program type we are looking for.
+        paid_modes (bool): Request if the user is enrolled in a Program in a paid mode, False by default.
+        enrollments (List[Dict]): Takes a serialized list of CourseEnrollments linked to the user
+        entitlements (List[CourseEntitlement]): Take a list of CourseEntitlement objects linked to the user
+
+        NOTE: Both enrollments and entitlements will be collected if they are not passed in. They are available
+        as parameters in case they were already collected, to save duplicate queries in high traffic areas.
+
+    Returns:
+        bool: True is the user is enrolled in programs of the requested type
+    """
+
+    return _is_user_enrolled_in_program_type(user, program_type_slug, paid_modes, enrollments, entitlements)
diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py
index c988ae35ff5..a9d90b72759 100644
--- a/openedx/core/djangoapps/programs/tests/test_utils.py
+++ b/openedx/core/djangoapps/programs/tests/test_utils.py
@@ -1587,23 +1587,23 @@ class TestProgramEnrollment(SharedModuleStoreTestCase):
 
     def test_user_not_in_program(self, mock_get_programs_by_type):
         mock_get_programs_by_type.return_value = [self.program]
-        self.assertFalse(is_user_enrolled_in_program_type(user=self.user, program_type=self.MICROBACHELORS))
+        self.assertFalse(is_user_enrolled_in_program_type(user=self.user, program_type_slug=self.MICROBACHELORS))
 
     def test_user_enrolled_in_mb_program(self, mock_get_programs_by_type):
         CourseEnrollmentFactory.create(user=self.user, course_id=self.course_run.id, mode=CourseMode.VERIFIED)
         mock_get_programs_by_type.return_value = [self.program]
-        self.assertTrue(is_user_enrolled_in_program_type(user=self.user, program_type=self.MICROBACHELORS))
+        self.assertTrue(is_user_enrolled_in_program_type(user=self.user, program_type_slug=self.MICROBACHELORS))
 
     def test_user_enrolled_unpaid_in_program(self, mock_get_programs_by_type):
         CourseEnrollmentFactory.create(user=self.user, course_id=self.course_run.id, mode=CourseMode.AUDIT)
         mock_get_programs_by_type.return_value = [self.program]
-        self.assertTrue(is_user_enrolled_in_program_type(user=self.user, program_type=self.MICROBACHELORS))
+        self.assertTrue(is_user_enrolled_in_program_type(user=self.user, program_type_slug=self.MICROBACHELORS))
 
     def test_user_enrolled_unpaid_in_program_paid_only_request(self, mock_get_programs_by_type):
         CourseEnrollmentFactory.create(user=self.user, course_id=self.course_run.id, mode=CourseMode.AUDIT)
         mock_get_programs_by_type.return_value = [self.program]
         self.assertFalse(
-            is_user_enrolled_in_program_type(user=self.user, program_type=self.MICROBACHELORS, paid_modes=True)
+            is_user_enrolled_in_program_type(user=self.user, program_type_slug=self.MICROBACHELORS, paid_modes=True)
         )
 
     def test_user_with_entitlement_no_enrollment(self, mock_get_programs_by_type):
@@ -1613,4 +1613,4 @@ class TestProgramEnrollment(SharedModuleStoreTestCase):
             course_uuid=self.program['courses'][0]['uuid']
         )
         mock_get_programs_by_type.return_value = [self.program]
-        self.assertTrue(is_user_enrolled_in_program_type(user=self.user, program_type=self.MICROBACHELORS))
+        self.assertTrue(is_user_enrolled_in_program_type(user=self.user, program_type_slug=self.MICROBACHELORS))
diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py
index 0ccdaafb32b..99dfe55a38e 100644
--- a/openedx/core/djangoapps/programs/utils.py
+++ b/openedx/core/djangoapps/programs/utils.py
@@ -22,16 +22,17 @@ from pytz import utc
 from requests.exceptions import ConnectionError, Timeout
 from six.moves.urllib.parse import urljoin, urlparse, urlunparse  # pylint: disable=import-error
 
+from course_modes.api import get_paid_modes_for_course
 from course_modes.models import CourseMode
 from entitlements.api import get_active_entitlement_list_for_user
 from entitlements.models import CourseEntitlement
 from lms.djangoapps.certificates import api as certificate_api
 from lms.djangoapps.certificates.models import GeneratedCertificate
 from lms.djangoapps.commerce.utils import EcommerceService
+from openedx.core.djangoapps.catalog.api import get_programs_by_type
 from openedx.core.djangoapps.catalog.utils import (
     get_fulfillable_course_runs_for_entitlement,
     get_programs,
-    get_programs_by_type
 )
 from openedx.core.djangoapps.certificates.api import available_date_for_certificate
 from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
@@ -901,9 +902,9 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
                     self.instructors.append(instructor)
 
 
-def is_user_enrolled_in_program_type(user, program_type, paid_modes=False, enrollments=None, entitlements=None):
+def is_user_enrolled_in_program_type(user, program_type_slug, paid_modes=False, enrollments=None, entitlements=None):
     """
-    This method will Look at the learners Enrollments and Entitlements to determine
+    This method will look at the learners Enrollments and Entitlements to determine
     if a learner is enrolled in a Program of the given type.
 
     NOTE: This method relies on the Program Cache right now. The goal is to move away from this
@@ -911,15 +912,20 @@ def is_user_enrolled_in_program_type(user, program_type, paid_modes=False, enrol
 
     Arguments:
         user (User): The user we are looking for.
-        program_type (String): The Program type we are looking for.
+        program_type_slug (str): The slug of the Program type we are looking for.
         paid_modes (bool): Request if the user is enrolled in a Program in a paid mode, False by default.
+        enrollments (List[Dict]): Takes a serialized list of CourseEnrollments linked to the user
+        entitlements (List[CourseEntitlement]): Take a list of CourseEntitlement objects linked to the user
+
+        NOTE: Both enrollments and entitlements will be collected if they are not passed in. They are available
+        as parameters in case they were already collected, to save duplicate queries in high traffic areas.
 
     Returns:
-        bool: True is the user is enrolled in programs of the requested Type
+        bool: True is the user is enrolled in programs of the requested type
     """
     course_runs = set()
     course_uuids = set()
-    programs = get_programs_by_type(Site.objects.get_current(), program_type)
+    programs = get_programs_by_type(Site.objects.get_current(), program_type_slug)
     if not programs:
         return False
 
@@ -941,7 +947,7 @@ def is_user_enrolled_in_program_type(user, program_type, paid_modes=False, enrol
         course_run_id = enrollment['course_details']['course_id']
         if paid_modes:
             course_run_key = CourseKey.from_string(course_run_id)
-            paid_modes = [mode.slug for mode in CourseMode.paid_modes_for_course(course_run_key)]
+            paid_modes = [mode.slug for mode in get_paid_modes_for_course(course_run_key)]
             if enrollment['mode'] in paid_modes and course_run_id in course_runs:
                 return True
         elif course_run_id in course_runs:
-- 
GitLab