diff --git a/common/djangoapps/course_modes/api.py b/common/djangoapps/course_modes/api.py new file mode 100644 index 0000000000000000000000000000000000000000..2dce97f412c533439c2bf65d897080f0f999f9a6 --- /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 e4cf046defbb601002c0900aae9d2467f2701ae2..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..349efb8a3f25d78d3c87db8d37ee048edd3065d9 --- /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 da5b881b80acae894e85ba9e1afc6bc9aa6ff28a..8a079f9bbd260d6208891fd00b014d74c0d6e9d9 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 8933b3a4ac616cc2b2785df9a51db48cf0830d66..7dd28e36fdd266271c908fbf49362bd2a4de254d 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 056bc103324a67659d3093340b7b127bd66a2174..e8d963e986ded243949f15377d7989f57b203dd8 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 9e2c8b856733067f86b21a2a021cbcc1e2a1af55..93a00d1be3b5da4a1459a763d908ede8ac095418 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 0000000000000000000000000000000000000000..74d6b5480403632d02ad184fbc94ae1f95a2423f --- /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 25fa7a2515a32b0f1fe34901927ac897c99a54c0..8f3a302870f80ba853ac561fbbab4d88cfb656df 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 b8c78b371f8152d54020ff06ac8821ee6d88cd30..b16796fa068cd0a24539e341a1c3444b9deb335d 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 e1c6bd9216a1e0c58cd9400542f1d18d2df18326..e7ceef8e533ffbc8ca774bdfe0b3e126aae0cde5 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 7886b7148c7fb6ee28b81fb223c8b0f65751e7fa..f0216de2da5615c021295511e8e387792cd806b9 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 e87e7442842859f603cf949a32c489ee0e8f48cf..2d4b8f8f39bb5cd428373e380b5b2be97685bb37 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 0000000000000000000000000000000000000000..48c680d606eb447660ac8cdcd491dca2fb86de1f --- /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 c988ae35ff5fda33cdd895850d0632b075b1b41e..a9d90b727599abab6e61bca09bfe4a3fb3c17a1f 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 0ccdaafb32bb90f1deb638a33dd4c06286eec0fb..99dfe55a38e5e5892f271aba8762b52037cef7e4 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: