From ac43d5faacffbc14210743c18ae68d18ca5a26c2 Mon Sep 17 00:00:00 2001 From: Dillon Dumesnil <ddumesnil@edx.org> Date: Mon, 22 Oct 2018 11:27:36 -0400 Subject: [PATCH] Adding email opt in to create entitlement --- .../entitlements/api/v1/tests/test_views.py | 42 ++++++ .../djangoapps/entitlements/api/v1/views.py | 11 +- .../djangoapps/catalog/tests/test_utils.py | 28 +++- openedx/core/djangoapps/catalog/utils.py | 138 ++++++++++-------- .../user_api/preferences/tests/test_api.py | 4 +- 5 files changed, 155 insertions(+), 68 deletions(-) diff --git a/common/djangoapps/entitlements/api/v1/tests/test_views.py b/common/djangoapps/entitlements/api/v1/tests/test_views.py index a52c248a37e..eb7a34248c9 100644 --- a/common/djangoapps/entitlements/api/v1/tests/test_views.py +++ b/common/djangoapps/entitlements/api/v1/tests/test_views.py @@ -20,6 +20,7 @@ from course_modes.tests.factories import CourseModeFactory from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory +from openedx.core.djangoapps.user_api.models import UserOrgTag from student.models import CourseEnrollment from student.tests.factories import (TEST_PASSWORD, CourseEnrollmentFactory, UserFactory) @@ -309,6 +310,47 @@ class EntitlementViewSetTest(ModuleStoreTestCase): ) assert course_entitlement.policy == policy + @patch("entitlements.api.v1.views.get_owners_for_course") + def test_email_opt_in_single_org(self, mock_get_owners): + course_uuid = uuid.uuid4() + entitlement_data = self._get_data_set(self.user, str(course_uuid)) + entitlement_data['email_opt_in'] = True + + org = u'particularly' + mock_get_owners.return_value = [{'key': org}] + + response = self.client.post( + self.entitlements_list_url, + data=json.dumps(entitlement_data), + content_type='application/json', + ) + assert response.status_code == 201 + + result_obj = UserOrgTag.objects.get(user=self.user, org=org, key='email-optin') + self.assertEqual(result_obj.value, u"True") + + @patch("entitlements.api.v1.views.get_owners_for_course") + def test_email_opt_in_multiple_orgs(self, mock_get_owners): + course_uuid = uuid.uuid4() + entitlement_data = self._get_data_set(self.user, str(course_uuid)) + entitlement_data['email_opt_in'] = True + + org_1 = u'particularly' + org_2 = u'underwood' + mock_get_owners.return_value = [{'key': org_1}, {'key': org_2}] + + response = self.client.post( + self.entitlements_list_url, + data=json.dumps(entitlement_data), + content_type='application/json', + ) + assert response.status_code == 201 + + result_obj = UserOrgTag.objects.get(user=self.user, org=org_1, key='email-optin') + self.assertEqual(result_obj.value, u"True") + result_obj = UserOrgTag.objects.get(user=self.user, org=org_2, key='email-optin') + self.assertEqual(result_obj.value, u"True") + def test_add_entitlement_with_support_detail(self): """ Verify that an EntitlementSupportDetail entry is made when the request includes support interaction information. diff --git a/common/djangoapps/entitlements/api/v1/views.py b/common/djangoapps/entitlements/api/v1/views.py index 654df32a19a..c8bf87203fa 100644 --- a/common/djangoapps/entitlements/api/v1/views.py +++ b/common/djangoapps/entitlements/api/v1/views.py @@ -19,9 +19,10 @@ from entitlements.api.v1.permissions import IsAdminOrSupportOrAuthenticatedReadO from entitlements.api.v1.serializers import CourseEntitlementSerializer from entitlements.models import CourseEntitlement, CourseEntitlementPolicy, CourseEntitlementSupportDetail from entitlements.utils import is_course_run_entitlement_fulfillable -from openedx.core.djangoapps.catalog.utils import get_course_runs_for_course +from openedx.core.djangoapps.catalog.utils import get_course_runs_for_course, get_owners_for_course from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf +from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in from student.models import AlreadyEnrolledError, CourseEnrollment, CourseEnrollmentException log = logging.getLogger(__name__) @@ -158,6 +159,8 @@ class EntitlementViewSet(viewsets.ModelViewSet): def create(self, request, *args, **kwargs): support_details = request.data.pop('support_details', []) + email_opt_in = request.data.pop('email_opt_in', False) + serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) @@ -165,6 +168,12 @@ class EntitlementViewSet(viewsets.ModelViewSet): entitlement = serializer.instance set_entitlement_policy(entitlement, request.site) + # The owners for a course are the organizations that own the course. By taking owner.key, + # we are able to pass in the organization key for email_opt_in + owners = get_owners_for_course(entitlement.course_uuid) + for owner in owners: + update_email_opt_in(entitlement.user, owner['key'], email_opt_in) + if support_details: for support_detail in support_details: support_detail['entitlement'] = entitlement diff --git a/openedx/core/djangoapps/catalog/tests/test_utils.py b/openedx/core/djangoapps/catalog/tests/test_utils.py index 0d447cbc13c..a0a5bdaed18 100644 --- a/openedx/core/djangoapps/catalog/tests/test_utils.py +++ b/openedx/core/djangoapps/catalog/tests/test_utils.py @@ -35,9 +35,10 @@ from openedx.core.djangoapps.catalog.utils import ( get_course_runs, get_course_runs_for_course, get_course_run_details, - get_pathways, get_currency_data, get_localized_price_text, + get_owners_for_course, + get_pathways, get_program_types, get_programs, get_visible_sessions_for_entitlement @@ -464,6 +465,31 @@ class TestGetCourseRuns(CatalogIntegrationMixin, TestCase): self.assertEqual(data, catalog_course_runs) +@skip_unless_lms +@mock.patch(UTILS_MODULE + '.get_edx_api_data') +class TestGetCourseOwners(CatalogIntegrationMixin, TestCase): + """ + Tests covering retrieval of course runs from the catalog service. + """ + def setUp(self): + super(TestGetCourseOwners, self).setUp() + + self.catalog_integration = self.create_catalog_integration(cache_ttl=1) + self.user = UserFactory(username=self.catalog_integration.service_username) + + def test_get_course_owners_by_course(self, mock_get_edx_api_data): + """ + Test retrieval of course runs. + """ + catalog_course_runs = CourseRunFactory.create_batch(10) + catalog_course = CourseFactory(course_runs=catalog_course_runs) + mock_get_edx_api_data.return_value = catalog_course + + data = get_owners_for_course(course_uuid=str(catalog_course['uuid'])) + self.assertTrue(mock_get_edx_api_data.called) + self.assertEqual(data, catalog_course['owners']) + + @skip_unless_lms @mock.patch(UTILS_MODULE + '.get_edx_api_data') class TestSessionEntitlement(CatalogIntegrationMixin, TestCase): diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py index a823153f2cc..f4530a38ca9 100644 --- a/openedx/core/djangoapps/catalog/utils.py +++ b/openedx/core/djangoapps/catalog/utils.py @@ -37,6 +37,45 @@ def create_catalog_api_client(user, site=None): return EdxRestApiClient(url, jwt=jwt) +def check_catalog_integration_and_get_user(error_message_field): + """ + Checks that catalog integration is enabled, and if so, attempts to get and + return the service user. + + Parameters: + error_message_field (str): The field that will be attempted to be + retrieved when calling the api client. Used for the error message. + + Returns: + Tuple of: + The catalog integration service user if it exists, else None + The catalog integration Object + Note: (This is necessary for future calls of functions using this method) + """ + catalog_integration = CatalogIntegration.current() + + if catalog_integration.is_enabled(): + try: + user = catalog_integration.get_service_user() + except ObjectDoesNotExist: + logger.error( + 'Catalog service user with username [{username}] does not exist. ' + '{field} will not be retrieved.'.format( + username=catalog_integration.service_username, + field=error_message_field, + ) + ) + return None, catalog_integration + return user, catalog_integration + else: + logger.error( + 'Unable to retrieve details about {field} because Catalog Integration is not enabled'.format( + field=error_message_field, + ) + ) + return None, catalog_integration + + def get_programs(site, uuid=None): """Read programs from the cache. @@ -101,13 +140,8 @@ def get_program_types(name=None): list of dict, representing program types. dict, if a specific program type is requested. """ - catalog_integration = CatalogIntegration.current() - if catalog_integration.enabled: - try: - user = catalog_integration.get_service_user() - except ObjectDoesNotExist: - return [] - + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Program types') + if user: api = create_catalog_api_client(user) cache_key = '{base}.program_types'.format(base=catalog_integration.CACHE_KEY) @@ -186,13 +220,8 @@ def get_currency_data(): list of dict, representing program types. dict, if a specific program type is requested. """ - catalog_integration = CatalogIntegration.current() - if catalog_integration.enabled: - try: - user = catalog_integration.get_service_user() - except ObjectDoesNotExist: - return [] - + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Currency data') + if user: api = create_catalog_api_client(user) cache_key = '{base}.currency'.format(base=catalog_integration.CACHE_KEY) @@ -289,18 +318,9 @@ def get_course_runs(): Returns: list of dict with each record representing a course run. """ - catalog_integration = CatalogIntegration.current() course_runs = [] - if catalog_integration.enabled: - try: - user = catalog_integration.get_service_user() - except ObjectDoesNotExist: - logger.error( - 'Catalog service user with username [%s] does not exist. Course runs will not be retrieved.', - catalog_integration.service_username, - ) - return course_runs - + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Course runs') + if user: api = create_catalog_api_client(user) querystring = { @@ -314,18 +334,30 @@ def get_course_runs(): def get_course_runs_for_course(course_uuid): - catalog_integration = CatalogIntegration.current() + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Course runs') + if user: + api = create_catalog_api_client(user) + cache_key = '{base}.course.{uuid}.course_runs'.format( + base=catalog_integration.CACHE_KEY, + uuid=course_uuid + ) + data = get_edx_api_data( + catalog_integration, + 'courses', + resource_id=course_uuid, + api=api, + cache_key=cache_key if catalog_integration.is_cache_enabled else None, + long_term_cache=True, + many=False + ) + return data.get('course_runs', []) + else: + return [] - if catalog_integration.is_enabled(): - try: - user = catalog_integration.get_service_user() - except ObjectDoesNotExist: - logger.error( - 'Catalog service user with username [%s] does not exist. Course runs will not be retrieved.', - catalog_integration.service_username, - ) - return [] +def get_owners_for_course(course_uuid): + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Owners') + if user: api = create_catalog_api_client(user) cache_key = '{base}.course.{uuid}.course_runs'.format( base=catalog_integration.CACHE_KEY, @@ -340,7 +372,7 @@ def get_course_runs_for_course(course_uuid): long_term_cache=True, many=False ) - return data.get('course_runs', []) + return data.get('owners', []) else: return [] @@ -356,18 +388,8 @@ def get_course_uuid_for_course(course_run_key): Returns: UUID: Course UUID and None if it was not retrieved. """ - catalog_integration = CatalogIntegration.current() - - if catalog_integration.is_enabled(): - try: - user = catalog_integration.get_service_user() - except ObjectDoesNotExist: - logger.error( - 'Catalog service user with username [%s] does not exist. Course UUID will not be retrieved.', - catalog_integration.service_username, - ) - return [] - + user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Course UUID') + if user: api = create_catalog_api_client(user) run_cache_key = '{base}.course_run.{course_run_key}'.format( @@ -483,27 +505,15 @@ def get_course_run_details(course_run_key, fields): Returns: dict with language, start date, end date, and max_effort details about specified course run """ - catalog_integration = CatalogIntegration.current() course_run_details = dict() - if catalog_integration.enabled: - try: - user = catalog_integration.get_service_user() - except ObjectDoesNotExist: - msg = 'Catalog service user {} does not exist. Data for course_run {} will not be retrieved'.format( - catalog_integration.service_username, - course_run_key - ) - logger.error(msg) - return course_run_details + user, catalog_integration = check_catalog_integration_and_get_user( + error_message_field='Data for course_run {}'.format(course_run_key) + ) + if user: api = create_catalog_api_client(user) cache_key = '{base}.course_runs'.format(base=catalog_integration.CACHE_KEY) course_run_details = get_edx_api_data(catalog_integration, 'course_runs', api, resource_id=course_run_key, cache_key=cache_key, many=False, traverse_pagination=False, fields=fields) - else: - msg = 'Unable to retrieve details about course_run {} because Catalog Integration is not enabled'.format( - course_run_key - ) - logger.error(msg) return course_run_details diff --git a/openedx/core/djangoapps/user_api/preferences/tests/test_api.py b/openedx/core/djangoapps/user_api/preferences/tests/test_api.py index 5df93fcc8c3..47e64e95c5a 100644 --- a/openedx/core/djangoapps/user_api/preferences/tests/test_api.py +++ b/openedx/core/djangoapps/user_api/preferences/tests/test_api.py @@ -332,9 +332,9 @@ class UpdateEmailOptInTests(ModuleStoreTestCase): """ Test cases to cover API-driven email list opt-in update workflows """ - USERNAME = u'frank-underwood' + USERNAME = u'claire-underwood' PASSWORD = u'ṕáśśẃőŕd' - EMAIL = u'frank+underwood@example.com' + EMAIL = u'claire+underwood@example.com' shard = 2 @ddt.data( -- GitLab