def test_create_pending_enterprise_users_http_error( self, mock_oauth_client): """ Verify the ``create_pending_enterprise_users`` method does not raise an exception for successful requests. """ # Mock out the response from the lms mock_oauth_client.return_value.post.return_value = MockResponse( {'detail': 'Bad Request'}, 400, content=b'error response', ) user_emails = [ '*****@*****.**', '*****@*****.**', '*****@*****.**', ] enterprise_client = EnterpriseApiClient() with self.assertRaises(requests.exceptions.HTTPError): response = enterprise_client.create_pending_enterprise_users( self.uuid, user_emails) mock_oauth_client.return_value.post.assert_called_once_with( enterprise_client.pending_enterprise_learner_endpoint, json=[{ 'enterprise_customer': self.uuid, 'user_email': user_email } for user_email in user_emails], ) assert response.status_code == 400 assert response.content == 'error response'
def activation_email_task(custom_template_text, email_recipient_list, subscription_uuid): """ Sends license activation email(s) asynchronously, and creates pending enterprise users to link the email recipients to the subscription's enterprise. Arguments: custom_template_text (dict): Dictionary containing `greeting` and `closing` keys to be used for customizing the email template. email_recipient_list (list of str): List of recipients to send the emails to. subscription_uuid (str): UUID (string representation) of the subscription that the recipients are associated with or will be associated with. """ subscription_plan = SubscriptionPlan.objects.get(uuid=subscription_uuid) pending_licenses = subscription_plan.licenses.filter(user_email__in=email_recipient_list).order_by('uuid') enterprise_api_client = EnterpriseApiClient() enterprise_customer = enterprise_api_client.get_enterprise_customer_data(subscription_plan.enterprise_customer_uuid) enterprise_slug = enterprise_customer.get('slug') enterprise_name = enterprise_customer.get('name') enterprise_sender_alias = enterprise_customer.get('sender_alias', 'edX Support Team') try: send_activation_emails( custom_template_text, pending_licenses, enterprise_slug, enterprise_name, enterprise_sender_alias ) except SMTPException: msg = 'License manager activation email sending received an exception for enterprise: {}.'.format( enterprise_name ) logger.error(msg, exc_info=True) return
def send_auto_applied_license_email_task(enterprise_customer_uuid, user_email): """ Asynchronously sends onboarding email to learner. Intended for use following automatic license activation. Uses Braze client to send email via Braze campaign. """ try: # Get some info about the enterprise customer enterprise_api_client = EnterpriseApiClient() enterprise_customer = enterprise_api_client.get_enterprise_customer_data( enterprise_customer_uuid) enterprise_slug = enterprise_customer.get('slug') enterprise_name = enterprise_customer.get('name') learner_portal_search_enabled = enterprise_customer.get( 'enable_integrated_customer_learner_portal_search') identity_provider = enterprise_customer.get('identity_provider') enterprise_sender_alias = get_enterprise_sender_alias( enterprise_customer) enterprise_contact_email = enterprise_customer.get('contact_email') except Exception: # pylint: disable=broad-except message = ( f'Error getting data about the enterprise_customer {enterprise_customer_uuid}. ' f'Onboarding email to {user_email} for auto applied license failed.' ) logger.error(message, exc_info=True) return # Determine which email campaign to use if identity_provider and learner_portal_search_enabled is False: braze_campaign_id = settings.AUTOAPPLY_NO_LEARNER_PORTAL_CAMPAIGN else: braze_campaign_id = settings.AUTOAPPLY_WITH_LEARNER_PORTAL_CAMPAIGN # Form data we want to hand to the campaign's email template braze_trigger_properties = { 'enterprise_customer_slug': enterprise_slug, 'enterprise_customer_name': enterprise_name, 'enterprise_sender_alias': enterprise_sender_alias, 'enterprise_contact_email': enterprise_contact_email, } recipient = _aliased_recipient_object_from_email(user_email) try: # Hit the Braze api to send the email braze_client_instance = BrazeApiClient() braze_client_instance.create_braze_alias( [user_email], ENTERPRISE_BRAZE_ALIAS_LABEL, ) braze_client_instance.send_campaign_message( braze_campaign_id, recipients=[recipient], trigger_properties=braze_trigger_properties, ) except BrazeClientError: message = ( 'Error hitting Braze API. ' f'Onboarding email to {user_email} for auto applied license failed.' ) logger.error(message, exc_info=True)
def send_reminder_email_task(custom_template_text, email_recipient_list, subscription_uuid): """ Sends license activation reminder email(s) asynchronously. Arguments: custom_template_text (dict): Dictionary containing `greeting` and `closing` keys to be used for customizing the email template. email_recipient_list (list of str): List of recipients to send the emails to. subscription_uuid (str): UUID (string representation) of the subscription that the recipients are associated with or will be associated with. """ subscription_plan = SubscriptionPlan.objects.get(uuid=subscription_uuid) pending_licenses = subscription_plan.licenses.filter( user_email__in=email_recipient_list).order_by('uuid') enterprise_api_client = EnterpriseApiClient() enterprise_slug = enterprise_api_client.get_enterprise_slug( subscription_plan.enterprise_customer_uuid) enterprise_name = enterprise_api_client.get_enterprise_name( subscription_plan.enterprise_customer_uuid) try: send_activation_emails(custom_template_text, pending_licenses, enterprise_slug, enterprise_name, is_reminder=True) except Exception: # pylint: disable=broad-except msg = 'License manager reminder email sending received an exception for enterprise: {}.'.format( enterprise_name) logger.error(msg, exc_info=True) # Return without updating the last_remind_date for licenses return License.set_date_fields_to_now(pending_licenses, ['last_remind_date'])
def revoke_course_enrollments_for_user_task(user_id, enterprise_id): """ Sends revoking the user's enterprise licensed course enrollments asynchronously Arguments: user_id (str): The ID of the user who had an enterprise license revoked enterprise_id (str): The ID of the enterprise to revoke course enrollments for """ try: enterprise_api_client = EnterpriseApiClient() enterprise_api_client.revoke_course_enrollments_for_user( user_id=user_id, enterprise_id=enterprise_id) logger.info( 'Revocation of course enrollments SUCCEEDED for user [{user_id}], enterprise [{enterprise_id}]' .format( user_id=user_id, enterprise_id=enterprise_id, )) except Exception: # pylint: disable=broad-except logger.error( 'Revocation of course enrollments FAILED for user [{user_id}], enterprise [{enterprise_id}]' .format( user_id=user_id, enterprise_id=enterprise_id, ), exc_info=True, )
def _validate_enterprise_customer_uuid(self): """ Verifies that a customer with the given enterprise_customer_uuid exists """ enterprise_customer_uuid = self.instance.enterprise_customer_uuid try: customer_data = EnterpriseApiClient().get_enterprise_customer_data( enterprise_customer_uuid) self.instance.enterprise_customer_slug = customer_data.get('slug') self.instance.enterprise_customer_name = customer_data.get('name') return True except HTTPError as ex: logger.exception( f'Could not validate enterprise_customer_uuid {enterprise_customer_uuid}.' ) if ex.response.status_code == status.HTTP_404_NOT_FOUND: self.add_error( 'enterprise_customer_uuid', f'An enterprise customer with uuid: {enterprise_customer_uuid} does not exist.', ) else: self.add_error( 'enterprise_customer_uuid', f'Could not verify the given UUID: {ex}. Please try again.', ) return False
def send_onboarding_email(enterprise_customer_uuid, user_email): """ Sends onboarding email to learner. Intended for use following license activation. Arguments: enterprise_customer_uuid (UUID): unique identifier of the EnterpriseCustomer that is linked to the SubscriptionPlan associated with the activated license user_email (str): email of the learner whose license has just been activated """ enterprise_customer = EnterpriseApiClient().get_enterprise_customer_data( enterprise_customer_uuid) enterprise_name = enterprise_customer.get('name') enterprise_slug = enterprise_customer.get('slug') enterprise_sender_alias = get_enterprise_sender_alias(enterprise_customer) context = { 'subject': ONBOARDING_EMAIL_SUBJECT, 'template_name': ONBOARDING_EMAIL_TEMPLATE, 'ENTERPRISE_NAME': enterprise_name, 'ENTERPRISE_SLUG': enterprise_slug, 'HELP_CENTER_URL': settings.SUPPORT_SITE_URL, 'LEARNER_PORTAL_LINK': get_learner_portal_url(enterprise_slug), 'MOBILE_STORE_URLS': settings.MOBILE_STORE_URLS, 'RECIPIENT_EMAIL': user_email, 'SOCIAL_MEDIA_FOOTER_URLS': settings.SOCIAL_MEDIA_FOOTER_URLS, } email = _message_from_context_and_template(context, enterprise_sender_alias) email.send()
def activation_task(custom_template_text, email_recipient_list, subscription_uuid): """ Sends license activation email(s) asynchronously, and creates pending enterprise users to link the email recipients to the subscription's enterprise. Arguments: custom_template_text (dict): Dictionary containing `greeting` and `closing` keys to be used for customizing the email template. email_recipient_list (list of str): List of recipients to send the emails to. subscription_uuid (str): UUID (string representation) of the subscription that the recipients are associated with or will be associated with. """ subscription_plan = SubscriptionPlan.objects.get(uuid=subscription_uuid) pending_licenses = subscription_plan.licenses.filter( user_email__in=email_recipient_list).order_by('uuid') enterprise_api_client = EnterpriseApiClient() enterprise_slug = enterprise_api_client.get_enterprise_slug( subscription_plan.enterprise_customer_uuid) send_activation_emails(custom_template_text, pending_licenses, enterprise_slug) License.set_date_fields_to_now(pending_licenses, ['last_remind_date', 'assigned_date']) for email_recipient in email_recipient_list: enterprise_api_client.create_pending_enterprise_user( subscription_plan.enterprise_customer_uuid, email_recipient, )
def send_reminder_email_task(custom_template_text, email_recipient_list, subscription_uuid): """ Sends license activation reminder email(s) asynchronously. Arguments: custom_template_text (dict): Dictionary containing `greeting` and `closing` keys to be used for customizing the email template. email_recipient_list (list of str): List of recipients to send the emails to. subscription_uuid (str): UUID (string representation) of the subscription that the recipients are associated with or will be associated with. """ subscription_plan = SubscriptionPlan.objects.get(uuid=subscription_uuid) pending_licenses = subscription_plan.licenses.filter( user_email__in=email_recipient_list).order_by('uuid') enterprise_api_client = EnterpriseApiClient() enterprise_customer = enterprise_api_client.get_enterprise_customer_data( subscription_plan.enterprise_customer_uuid) enterprise_slug = enterprise_customer.get('slug') enterprise_name = enterprise_customer.get('name') enterprise_sender_alias = get_enterprise_sender_alias(enterprise_customer) enterprise_contact_email = enterprise_customer.get('contact_email') # We need to send these emails individually, because each email's text must be # generated for every single user/activation_key for pending_license in pending_licenses: user_email = pending_license.user_email license_activation_key = str(pending_license.activation_key) braze_campaign_id = settings.BRAZE_REMIND_EMAIL_CAMPAIGN braze_trigger_properties = { 'TEMPLATE_GREETING': custom_template_text['greeting'], 'TEMPLATE_CLOSING': custom_template_text['closing'], 'license_activation_key': license_activation_key, 'enterprise_customer_slug': enterprise_slug, 'enterprise_customer_name': enterprise_name, 'enterprise_sender_alias': enterprise_sender_alias, 'enterprise_contact_email': enterprise_contact_email, } recipient = _aliased_recipient_object_from_email(user_email) try: braze_client_instance = BrazeApiClient() braze_client_instance.create_braze_alias( [user_email], ENTERPRISE_BRAZE_ALIAS_LABEL, ) braze_client_instance.send_campaign_message( braze_campaign_id, recipients=[recipient], trigger_properties=braze_trigger_properties, ) except BrazeClientError as exc: message = ('Error hitting Braze API. ' f'reminder email to {user_email} for license failed.') logger.exception(message) raise exc License.set_date_fields_to_now(pending_licenses, ['last_remind_date'])
def _send_bulk_enrollment_results_email( bulk_enrollment_job, campaign_id, ): """ Sends email with properties required to detail the results of a bulk enrollment job. Arguments: bulk_enrollment_job (BulkEnrollmentJob): the completed bulk enrollment job campaign_id: (str): The Braze campaign identifier """ try: enterprise_api_client = EnterpriseApiClient() enterprise_customer = enterprise_api_client.get_enterprise_customer_data( bulk_enrollment_job.enterprise_customer_uuid, ) admin_users = enterprise_api_client.get_enterprise_admin_users( bulk_enrollment_job.enterprise_customer_uuid, ) # https://web.archive.org/web/20211122135949/https://www.braze.com/docs/api/objects_filters/recipient_object/ recipients = [] for user in admin_users: if int(user['id']) != bulk_enrollment_job.lms_user_id: continue # must use a mix of send_to_existing_only: false + enternal_id w/ attributes to send to new braze profiles recipient = { 'send_to_existing_only': False, 'external_user_id': str(user['id']), 'attributes': { 'email': user['email'], } } recipients.append(recipient) break braze_client = BrazeApiClient() braze_client.send_campaign_message(campaign_id, recipients=recipients, trigger_properties={ 'enterprise_customer_slug': enterprise_customer.get('slug'), 'enterprise_customer_name': enterprise_customer.get('name'), 'bulk_enrollment_job_uuid': str(bulk_enrollment_job.uuid), }) msg = ( f'success _send_bulk_enrollment_results_email for bulk_enrollment_job_uuid={bulk_enrollment_job.uuid} ' 'braze_campaign_id={campaign_id} lms_user_id={bulk_enrollment_job.lms_user_id}' ) logger.info(msg) except Exception as ex: msg = ( f'failed _send_bulk_enrollment_results_email for bulk_enrollment_job_uuid={bulk_enrollment_job.uuid} ' 'braze_campaign_id={campaign_id} lms_user_id={bulk_enrollment_job.lms_user_id}' ) logger.error(msg, exc_info=True) raise ex
def test_create_pending_enterprise_user_logs(self, mock_oauth_client, mock_logger): """ Verify the ``create_pending_enterprise_user`` method logs an error for a status code of >=400. """ # Mock out the response from the lms mock_oauth_client().post.return_value = MockResponse({'detail': 'Bad Request'}, 400) EnterpriseApiClient().create_pending_enterprise_user(self.uuid, self.user_email) mock_logger.error.assert_called_once()
def _get_admin_users_for_enterprise(enterprise_customer_uuid): api_client = EnterpriseApiClient() admin_users = api_client.get_enterprise_admin_users( enterprise_customer_uuid) return [{ 'lms_user_id': admin_user['id'], 'ecu_id': admin_user['ecu_id'], 'email': admin_user['email'] } for admin_user in admin_users]
def test_create_pending_enterprise_user_successful(self, mock_oauth_client, mock_logger): """ Verify the ``create_pending_enterprise_user`` method does not retry or log an error on success """ # Mock out the response from the lms mock_oauth_client().post.return_value = MockResponse( {'detail': 'Good Request'}, 201) EnterpriseApiClient().create_pending_enterprise_user( self.uuid, self.user_email) assert mock_oauth_client().post.call_count == 1 mock_logger.error.assert_not_called()
def test_revoke_course_enrollments_for_user_with_error(self, mock_oauth_client, mock_logger): """ Verify the ``update_course_enrollment_mode_for_user`` method logs an error for a status code of >=400. """ # Mock out the response from the lms mock_oauth_client().post.return_value = MockResponse({'detail': 'Bad Request'}, 400) mock_oauth_client().post.return_value.content = 'error response' EnterpriseApiClient().revoke_course_enrollments_for_user( user_id=self.user_id, enterprise_id=self.uuid, ) mock_logger.error.assert_called_once()
def link_learners_to_enterprise_task(learner_emails, enterprise_customer_uuid): """ Links learners to an enterprise asynchronously. Arguments: learner_emails (list): list containing the list of learner emails to link to the enterprise enterprise_customer_uuid (str): UUID (string representation) of the enterprise to link learns to """ enterprise_api_client = EnterpriseApiClient() for learner_email in learner_emails: enterprise_api_client.create_pending_enterprise_user( enterprise_customer_uuid, learner_email, )
def test_create_pending_enterprise_user_rate_limited(self, mock_oauth_client): """ Verify the ``create_pending_enterprise_user`` method retries on a 429 response code. """ rate_limited_response = MockResponse({'detail': 'Rate limited'}, 429) # Mock out a few rate-limited response and one good from the lms mock_oauth_client().post.side_effect = [ rate_limited_response, rate_limited_response, rate_limited_response, MockResponse({'detail': 'Good Request'}, 201), ] EnterpriseApiClient().create_pending_enterprise_user(self.uuid, self.user_email) assert mock_oauth_client().post.call_count == 4
def link_learners_to_enterprise_task(learner_emails, enterprise_customer_uuid): """ Links learners to an enterprise asynchronously. Arguments: learner_emails (list): list email addresses to link to the given enterprise. enterprise_customer_uuid (str): UUID (string representation) of the enterprise to link learns to. """ enterprise_api_client = EnterpriseApiClient() for user_email_batch in chunks(learner_emails, PENDING_ACCOUNT_CREATION_BATCH_SIZE): enterprise_api_client.create_pending_enterprise_users( enterprise_customer_uuid, user_email_batch, )
def test_create_pending_enterprise_user_rate_with_error_retries( self, mock_oauth_client): """ Verify the ``create_pending_enterprise_user`` method retries on a non 429 error code """ error_response = MockResponse({'detail': '500 Internal Server'}, 500) # Mock out a few rate-limited response and one good from the lms mock_oauth_client().post.side_effect = [ error_response, error_response, MockResponse({'detail': 'Good Request'}, 201), ] EnterpriseApiClient().create_pending_enterprise_user( self.uuid, self.user_email) assert mock_oauth_client().post.call_count == 3
def sync_agreement_with_enterprise_customer(customer_agreement): """ Syncs any updates made to the enterprise customer slug or name as returned by the ``EnterpriseApiClient`` with the specified ``CustomerAgreement``. """ try: customer_data = EnterpriseApiClient().get_enterprise_customer_data( customer_agreement.enterprise_customer_uuid, ) customer_agreement.enterprise_customer_slug = customer_data.get('slug') customer_agreement.enterprise_customer_name = customer_data.get('name') customer_agreement.save() except HTTPError as exc: error_message = ( 'Could not fetch customer fields from the enterprise API: {}'.format(exc) ) raise CustomerAgreementError(error_message) from exc
def license_expiration_task(license_uuids): """ Sends terminating the licensed course enrollments for the submitted license_uuids asynchronously Arguments: license_uuids (list of str): The UUIDs of the expired licenses """ try: enterprise_api_client = EnterpriseApiClient() enterprise_api_client.bulk_licensed_enrollments_expiration(expired_license_uuids=license_uuids) except Exception: # pylint: disable=broad-except logger.error( "Expiration of course enrollments FAILED for licenses [{license_uuids}]".format( license_uuids=license_uuids, ), exc_info=True, )
def get_enterprise_customer(self, enterprise_customer_slug): """ Returns an enterprise customer """ logger.info('\nFetching an enterprise customer {} name ...'.format(enterprise_customer_slug)) try: enterprise_api_client = EnterpriseApiClient() # Query endpoint by slug for easy dev CLI experience endpoint = '{}?slug={}'.format(enterprise_api_client.enterprise_customer_endpoint, str(enterprise_customer_slug)) response = enterprise_api_client.client.get(endpoint).json() if response.get('count'): return response.get('results')[0] return None except IndexError: logger.error('No enterprise customer found.') return None
def create_relationships(apps, schema_editor): """ Create new CustomerAgreements from all existing SubscriptionPlans with enterprise customers. """ CustomerAgreement = apps.get_model('subscriptions', 'CustomerAgreement') SubscriptionPlan = apps.get_model('subscriptions', 'SubscriptionPlan') subscriptions_with_customers = SubscriptionPlan.objects.exclude(enterprise_customer_uuid=None) for plan in subscriptions_with_customers: customer_uuid = plan.enterprise_customer_uuid enterprise_slug = EnterpriseApiClient().get_enterprise_customer_data(customer_uuid).get('slug') customer_agreement, _ = CustomerAgreement.objects.get_or_create( enterprise_customer_uuid=customer_uuid, defaults={ 'enterprise_customer_slug': enterprise_slug, } ) plan.customer_agreement = customer_agreement plan.save()
def send_revocation_cap_notification_email_task(subscription_uuid): """ Sends revocation cap email notification to ECS asynchronously. Arguments: subscription_uuid (str): UUID (string representation) of the subscription that has reached its recovation cap. """ subscription_plan = SubscriptionPlan.objects.get(uuid=subscription_uuid) enterprise_api_client = EnterpriseApiClient() enterprise_customer = enterprise_api_client.get_enterprise_customer_data( subscription_plan.enterprise_customer_uuid) enterprise_name = enterprise_customer.get('name') now = localized_utcnow() revocation_date = datetime.strftime(now, "%B %d, %Y, %I:%M%p %Z") braze_campaign_id = settings.BRAZE_REVOKE_CAP_EMAIL_CAMPAIGN braze_trigger_properties = { 'SUBSCRIPTION_TITLE': subscription_plan.title, 'NUM_REVOCATIONS_APPLIED': subscription_plan.num_revocations_applied, 'ENTERPRISE_NAME': enterprise_name, 'REVOKED_LIMIT_REACHED_DATE': revocation_date, } recipient = _aliased_recipient_object_from_email( settings.CUSTOMER_SUCCESS_EMAIL_ADDRESS) try: braze_client_instance = BrazeApiClient() braze_client_instance.create_braze_alias( [settings.CUSTOMER_SUCCESS_EMAIL_ADDRESS], ENTERPRISE_BRAZE_ALIAS_LABEL, ) braze_client_instance.send_campaign_message( braze_campaign_id, recipients=[recipient], trigger_properties=braze_trigger_properties, ) except BrazeClientError as exc: message = 'Revocation cap notification email sending received an exception.' logger.exception(message) raise exc
def send_revocation_cap_notification_email_task(subscription_uuid): """ Sends revocation cap email notification to ECS asynchronously. Arguments: subscription_uuid (str): UUID (string representation) of the subscription that has reached its recovation cap. """ subscription_plan = SubscriptionPlan.objects.get(uuid=subscription_uuid) enterprise_api_client = EnterpriseApiClient() enterprise_name = enterprise_api_client.get_enterprise_name( subscription_plan.enterprise_customer_uuid) try: send_revocation_cap_notification_email( subscription_plan, enterprise_name, ) except Exception: # pylint: disable=broad-except logger.error( 'Revocation cap notification email sending received an exception.', exc_info=True)
def send_revocation_cap_notification_email_task(subscription_uuid): """ Sends revocation cap email notification to ECS asynchronously. Arguments: subscription_uuid (str): UUID (string representation) of the subscription that has reached its recovation cap. """ subscription_plan = SubscriptionPlan.objects.get(uuid=subscription_uuid) enterprise_api_client = EnterpriseApiClient() enterprise_customer = enterprise_api_client.get_enterprise_customer_data(subscription_plan.enterprise_customer_uuid) enterprise_name = enterprise_customer.get('name') enterprise_sender_alias = enterprise_customer.get('sender_alias', 'edX Support Team') try: send_revocation_cap_notification_email( subscription_plan, enterprise_name, enterprise_sender_alias, ) except SMTPException: logger.error('Revocation cap notification email sending received an exception.', exc_info=True)
def test_create_pending_enterprise_user_rate_with_error_retries_and_rate_limiting( self, mock_oauth_client): """ Verify the ``create_pending_enterprise_user`` method has 3 error retries if a rate limited error occurs """ error_response = MockResponse({'detail': '500 Internal Server'}, 500) rate_limited_response = MockResponse({'detail': 'Rate limited'}, 429) # Mock out a few rate-limited response and one good from the lms mock_oauth_client().post.side_effect = [ error_response, error_response, rate_limited_response, error_response, error_response, error_response, error_response, MockResponse({'detail': 'Good Request'}, 201), ] EnterpriseApiClient().create_pending_enterprise_user( self.uuid, self.user_email) assert mock_oauth_client().post.call_count == 6
def license_expiration_task(license_uuids, ignore_enrollments_modified_after=None): """ Sends terminating the licensed course enrollments for the submitted license_uuids asynchronously Arguments: license_uuids (list of str): The UUIDs of the expired licenses """ try: enterprise_api_client = EnterpriseApiClient() enterprise_api_client.bulk_licensed_enrollments_expiration( expired_license_uuids=license_uuids, ignore_enrollments_modified_after=ignore_enrollments_modified_after ) logger.info( "Expiration of course enrollments SUCCEEDED for licenses [{license_uuids}]" .format(license_uuids=license_uuids, )) except Exception as exc: logger.error( "Expiration of course enrollments FAILED for licenses [{license_uuids}]" .format(license_uuids=license_uuids, ), exc_info=True, ) raise exc
def send_post_activation_email_task(enterprise_customer_uuid, user_email): """ Asynchronously sends post license activation email to learner. """ enterprise_customer = EnterpriseApiClient().get_enterprise_customer_data( enterprise_customer_uuid) enterprise_name = enterprise_customer.get('name') enterprise_slug = enterprise_customer.get('slug') enterprise_sender_alias = get_enterprise_sender_alias(enterprise_customer) enterprise_contact_email = enterprise_customer.get('contact_email') braze_campaign_id = settings.BRAZE_ACTIVATION_EMAIL_CAMPAIGN braze_trigger_properties = { 'enterprise_customer_slug': enterprise_slug, 'enterprise_customer_name': enterprise_name, 'enterprise_sender_alias': enterprise_sender_alias, 'enterprise_contact_email': enterprise_contact_email, } recipient = _aliased_recipient_object_from_email(user_email) try: braze_client_instance = BrazeApiClient() braze_client_instance.create_braze_alias( [user_email], ENTERPRISE_BRAZE_ALIAS_LABEL, ) braze_client_instance.send_campaign_message( braze_campaign_id, recipients=[recipient], trigger_properties=braze_trigger_properties, ) except BrazeClientError as exc: message = ('Error hitting Braze API. ' f'Onboarding email to {user_email} for license failed.') logger.exception(message) raise exc
def enterprise_enrollment_license_subsidy_task( bulk_enrollment_job_uuid, enterprise_customer_uuid, learner_emails, course_run_keys, notify_learners, subscription_uuid, ): """ Enroll a list of enterprise learners into a list of course runs with or without notifying them. Optionally, filter license check by a specific subscription. Arguments: bulk_enrollment_job_uuid (str): UUID (string representation) for a BulkEnrollmentJob created by the enqueuing process for logging and progress tracking table updates. enterprise_customer_uuid (str): UUID (string representation) the enterprise customer id learner_emails (list(str)): email addresses of the learners to enroll course_run_keys (list(str)): course keys of the courses to enroll the learners into notify_learners (bool): whether or not to send notifications of their enrollment to the learners subscription_uuid (str): UUID (string representation) of the specific enterprise subscription to use when validating learner licenses """ # AED 2022-01-24 - I don't have enough context to unwind this sanely. # Declaring bankruptcy for now. # pylint: disable=too-many-nested-blocks try: logger.info('starting enterprise_enrollment_license_subsidy_task for ' f'bulk_enrollment_job_uuid={bulk_enrollment_job_uuid} ' f'enterprise_customer_uuid={enterprise_customer_uuid}') # collect/return results (rather than just write to the CSV) to help testability results = [] bulk_enrollment_job = BulkEnrollmentJob.objects.get( uuid=bulk_enrollment_job_uuid) customer_agreement = CustomerAgreement.objects.get( enterprise_customer_uuid=enterprise_customer_uuid) # this is to avoid hitting timeouts on the enterprise enroll api # take course keys 25 at a time, for each course key chunk, take learners 25 at a time for course_run_key_batch in chunks(course_run_keys, 25): logger.debug( "enterprise_customer_uuid={} course_run_key_batch size: {}". format(enterprise_customer_uuid, len(course_run_key_batch))) for learner_enrollment_batch in chunks(learner_emails, 25): logger.debug( "enterprise_customer_uuid={} learner_enrollment_batch size: {}" .format(enterprise_customer_uuid, len(learner_enrollment_batch))) missing_subscriptions, licensed_enrollment_info = utils.check_missing_licenses( customer_agreement, learner_enrollment_batch, course_run_key_batch, subscription_uuid=subscription_uuid, ) if missing_subscriptions: for failed_email, course_keys in missing_subscriptions.items( ): for course_key in course_keys: results.append([ failed_email, course_key, 'failed', 'missing subscription' ]) if licensed_enrollment_info: options = { 'licenses_info': licensed_enrollment_info, 'notify': notify_learners } enrollment_result = EnterpriseApiClient( ).bulk_enroll_enterprise_learners( str(enterprise_customer_uuid), options).json() for success in enrollment_result['successes']: results.append([ success.get('email'), success.get('course_run_key'), 'success', '' ]) for pending in enrollment_result['pending']: results.append([ pending.get('email'), pending.get('course_run_key'), 'pending', 'pending license activation' ]) for failure in enrollment_result['failures']: results.append([ failure.get('email'), failure.get('course_run_key'), 'failed', '' ]) if enrollment_result.get('invalid_email_addresses'): for result_email in enrollment_result[ 'invalid_email_addresses']: for course_key in course_run_key_batch: results.append([ result_email, course_key, 'failed', 'invalid email address' ]) with NamedTemporaryFile(mode='w', delete=False) as result_file: result_writer = csv.writer(result_file) result_writer.writerow( ['email address', 'course key', 'enrollment status', 'notes']) for result in results: result_writer.writerow(result) result_file.close() if hasattr(settings, "BULK_ENROLL_JOB_AWS_BUCKET" ) and settings.BULK_ENROLL_JOB_AWS_BUCKET: bulk_enrollment_job.upload_results(result_file.name) if hasattr(settings, "BULK_ENROLL_RESULT_CAMPAIGN" ) and settings.BULK_ENROLL_RESULT_CAMPAIGN: _send_bulk_enrollment_results_email( bulk_enrollment_job=bulk_enrollment_job, campaign_id=settings.BULK_ENROLL_RESULT_CAMPAIGN, ) return results except Exception as ex: msg = ('failed enterprise_enrollment_license_subsidy_task for ' f'bulk_enrollment_job_uuid={bulk_enrollment_job_uuid} ' f'enterprise_customer_uuid={enterprise_customer_uuid}') logger.error(msg, exc_info=True) raise ex
def post(self, request): """ Returns the enterprise bulk enrollment API response after validating that each user requesting to be enrolled has a valid subscription for each of the requested courses. Expected params: - notify (bool): Whether or not learners should be notified of their enrollments. - course_run_keys (list of strings): An array of course run keys in which all provided learners will be enrolled in Example: course_run_keys: ['course-v1:edX+DemoX+Demo_Course', 'course-v2:edX+The+Second+DemoX+Demo_Course', ... ] - emails (string): A single string of multiple learner emails separated with a `\n` (new line) character Example: emails: '[email protected]\[email protected]\[email protected]' - enterprise_customer_uuid (string): the uuid of the associated enterprise customer provided as a query params. Expected Return Values: Success cases: - All learners have licenses and are enrolled - {}, 201 Partial failure cases: License verification and bulk enterprise enrollment happen non-transactionally, meaning that a subset of learners failing one step will not stop others from continuing the enrollment flow. As such, partial failures will be reported in the following ways: Fails license verification: response includes: {'failed_license_checks': [<users who do not have valid licenses>]} Fails Enrollment: response includes {'failed_enrollments': [<users who were not able to be enrolled>] Fails Validation (something goes wrong with requesting enrollments): response includes: {'bulk_enrollment_errors': [<errors returned by the bulk enrollment endpoint>]} """ param_validation = self._validate_request_params() if param_validation: return Response(param_validation, status=status.HTTP_400_BAD_REQUEST) results = {} customer_agreement = utils.get_customer_agreement_from_request_enterprise_uuid( request) missing_subscriptions, licensed_enrollment_info = self._check_missing_licenses( customer_agreement) if missing_subscriptions: msg = 'One or more of the learners entered do not have a valid subscription for your requested courses. ' \ 'Learners: {}'.format(missing_subscriptions) results['failed_license_checks'] = missing_subscriptions logger.error(msg) if licensed_enrollment_info: options = { 'licenses_info': licensed_enrollment_info, 'notify': self.requested_notify_learners } enrollment_response = EnterpriseApiClient( ).bulk_enroll_enterprise_learners(self.requested_enterprise_id, options) # Check for bulk enrollment errors if enrollment_response.status_code >= 400 and enrollment_response.status_code != 409: status_code = status.HTTP_400_BAD_REQUEST results['bulk_enrollment_errors'] = [] try: response_json = enrollment_response.json() except JSONDecodeError: # Catch uncaught exceptions from enterprise results['bulk_enrollment_errors'].append( enrollment_response.reason) else: msg = 'Encountered a validation error when requesting bulk enrollment. Endpoint returned with ' \ 'error: {}'.format(response_json) logger.error(msg) # check for non field specific errors if response_json.get('non_field_errors'): results['bulk_enrollment_errors'].append( response_json['non_field_errors']) # check for param field specific validation errors for param in options: if response_json.get(param): results['bulk_enrollment_errors'].append( response_json.get(param)) else: enrollment_result = enrollment_response.json() if enrollment_result.get('failures'): results['failed_enrollments'] = enrollment_result[ 'failures'] if enrollment_result.get('failures') or missing_subscriptions: status_code = status.HTTP_409_CONFLICT else: status_code = status.HTTP_201_CREATED else: status_code = status.HTTP_404_NOT_FOUND return Response(results, status=status_code)