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 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 update_user_email_for_licenses_task(lms_user_id, new_email): """ Updates the user_email field on all licenses associated with the given lms_user_id. Arguments: lms_user_id (str): The lms_user_id associated with licenses that should be updated new_email (str): The email that will overwrite curent user_email fields """ user_licenses = License.objects.filter(lms_user_id=lms_user_id, ) for lcs in user_licenses: lcs.user_email = new_email License.bulk_update(user_licenses, ['user_email'])
def test_bulk_create(self): """ Test that bulk_create creates and saves objects, and creates an associated historical record for the creation. """ licenses = [License(subscription_plan=self.subscription_plan) for _ in range(3)] License.bulk_create(licenses) for user_license in licenses: user_license.refresh_from_db() assert UNASSIGNED == user_license.status license_history = user_license.history.all() assert 1 == len(license_history) assert self.CREATE_HISTORY_TYPE == user_license.history.earliest().history_type
def test_bulk_create(self, mock_track_license_changes): """ Test that bulk_create creates and saves objects, and creates an associated historical record for the creation, and calls the create track_event. """ licenses = [License(subscription_plan=self.subscription_plan) for _ in range(3)] License.bulk_create(licenses) for user_license in licenses: user_license.refresh_from_db() assert UNASSIGNED == user_license.status license_history = user_license.history.all() assert 1 == len(license_history) assert self.CREATE_HISTORY_TYPE == user_license.history.earliest().history_type mock_track_license_changes.assert_called_with( licenses, SegmentEvents.LICENSE_CREATED )
def test_bulk_update(self): """ Test that bulk_update saves objects, and creates an associated historical record for the update action """ licenses = [License(subscription_plan=self.subscription_plan) for _ in range(3)] License.bulk_create(licenses) for user_license in licenses: user_license.status = REVOKED License.bulk_update(licenses, ['status']) for user_license in licenses: user_license.refresh_from_db() assert REVOKED == user_license.status license_history = user_license.history.all() assert 2 == len(license_history) assert self.CREATE_HISTORY_TYPE == user_license.history.earliest().history_type assert self.UPDATE_HISTORY_TYPE == user_license.history.first().history_type
def test_for_user_and_customer_active_and_current_only(self): expected_licenses = [ self.active_current_license, ] actual_licenses = License.for_user_and_customer( user_email=self.user_email, lms_user_id=None, enterprise_customer_uuid=self.enterprise_customer_uuid, active_plans_only=True, current_plans_only=True, ) self.assertCountEqual(actual_licenses, expected_licenses)
def test_for_user_and_customer_no_kwargs(self): expected_licenses = [ self.active_current_license, self.inactive_current_license, self.non_current_active_license, self.non_current_inactive_license, ] actual_licenses = License.for_user_and_customer( user_email=self.user_email, lms_user_id=None, enterprise_customer_uuid=self.enterprise_customer_uuid, ) self.assertCountEqual(actual_licenses, expected_licenses)
def post(self, request): """ For a given email address provided in POST data, returns all licenses and associated subscription data associated with that email address. """ user_email = request.data.get('user_email') if not user_email: return Response( 'A ``user_email`` is required in the request data', status=status.HTTP_400_BAD_REQUEST, ) user_licenses = License.by_user_email(user_email) if not user_licenses: return Response(status=status.HTTP_404_NOT_FOUND, ) serialized_licenses = serializers.StaffLicenseSerializer(user_licenses, many=True) return Response( serialized_licenses.data, status=status.HTTP_200_OK, )
def assign(self, request, subscription_uuid=None): """ Given a list of emails, assigns a license to those user emails and sends an activation email. This endpoint allows assigning licenses to users who have previously had a license revoked, by removing their association to the revoked licenses and then assigning them to unassigned licenses. """ # Validate the user_emails and text sent in the data self._validate_data(request.data) # Dedupe all lowercase emails before turning back into a list for indexing user_emails = list( {email.lower() for email in request.data.get('user_emails', [])}) subscription_plan = self._get_subscription_plan() # Find any emails that have already been associated with a non-revoked license in the subscription # and remove from user_emails list already_associated_licenses = subscription_plan.licenses.filter( user_email__in=user_emails, status__in=[constants.ASSIGNED, constants.ACTIVATED], ) if already_associated_licenses: already_associated_emails = list( already_associated_licenses.values_list('user_email', flat=True)) for email in already_associated_emails: user_emails.remove(email.lower()) # Get the revoked licenses that are attempting to be assigned to revoked_licenses_for_assignment = subscription_plan.licenses.filter( status=constants.REVOKED, user_email__in=user_emails, ) # Make sure there are enough licenses that we can assign to num_user_emails = len(user_emails) num_unassigned_licenses = subscription_plan.unassigned_licenses.count() # Since we flip the status of revoked licenses when admins attempt to re-assign that learner to a new # license, we check that there are enough unassigned licenses when combined with the revoked licenses that # will have their status change num_potential_unassigned_licenses = num_unassigned_licenses + revoked_licenses_for_assignment.count( ) if num_user_emails > num_potential_unassigned_licenses: msg = ( 'There are not enough licenses that can be assigned to complete your request.' 'You attempted to assign {} licenses, but there are only {} potentially available.' ).format(num_user_emails, num_potential_unassigned_licenses) return Response(msg, status=status.HTTP_400_BAD_REQUEST) # Flip all revoked licenses that were associated with emails that we are assigning to unassigned, and clear # all the old data on the license. for revoked_license in revoked_licenses_for_assignment: revoked_license.reset_to_unassigned() License.bulk_update( revoked_licenses_for_assignment, [ 'status', 'user_email', 'lms_user_id', 'last_remind_date', 'activation_date', 'activation_key', 'assigned_date', 'revoked_date', ], ) # Get a queryset of only the number of licenses we need to assign unassigned_licenses = subscription_plan.unassigned_licenses[: num_user_emails] for unassigned_license, email in zip(unassigned_licenses, user_emails): # Assign each email to a license and mark the license as assigned unassigned_license.user_email = email unassigned_license.status = constants.ASSIGNED activation_key = str(uuid4()) unassigned_license.activation_key = activation_key unassigned_license.assigned_date = localized_utcnow() unassigned_license.last_remind_date = localized_utcnow() License.bulk_update( unassigned_licenses, [ 'user_email', 'status', 'activation_key', 'assigned_date', 'last_remind_date' ], ) # Create async chains of the pending learners and activation emails tasks with each batch of users # The task signatures are immutable, hence the `si()` - we don't want the result of the # link_learners_to_enterprise_task passed to the "child" activation_email_task. for pending_learner_batch in chunks( user_emails, constants.PENDING_ACCOUNT_CREATION_BATCH_SIZE): chain( link_learners_to_enterprise_task.si( pending_learner_batch, subscription_plan.enterprise_customer_uuid, ), activation_email_task.si( self._get_custom_text(request.data), pending_learner_batch, subscription_uuid, )).apply_async() # Pass email assignment data back to frontend for display response_data = { 'num_successful_assignments': len(user_emails), 'num_already_associated': len(already_associated_licenses) } return Response(data=response_data, status=status.HTTP_200_OK)
def handle(self, *args, **options): ready_for_retirement_date = localized_utcnow() - timedelta( DAYS_TO_RETIRE) expired_licenses_for_retirement = License.get_licenses_exceeding_purge_duration( 'subscription_plan__expiration_date', ) # Scrub all piii on licenses whose subscription expired over 90 days ago, and mark the licenses as revoked for expired_license in expired_licenses_for_retirement: original_lms_user_id = expired_license.lms_user_id # record event data BEFORE we clear the license data: event_properties = get_license_tracking_properties(expired_license) expired_license.clear_pii() expired_license.status = REVOKED expired_license.revoked_date = localized_utcnow() expired_license.save() event_properties = get_license_tracking_properties(expired_license) track_event(original_lms_user_id, SegmentEvents.LICENSE_REVOKED, event_properties) # Clear historical pii after removing pii from the license itself expired_license.clear_historical_pii() expired_license_uuids = sorted([ expired_license.uuid for expired_license in expired_licenses_for_retirement ]) message = 'Retired {} expired licenses with uuids: {}'.format( len(expired_license_uuids), expired_license_uuids) logger.info(message) revoked_licenses_for_retirement = License.get_licenses_exceeding_purge_duration( 'revoked_date', status=REVOKED, ) # Scrub all pii on the revoked licenses, but they should stay revoked and keep their other info as we currently # add an unassigned license to the subscription's license pool whenever one is revoked. for revoked_license in revoked_licenses_for_retirement: revoked_license.clear_pii() revoked_license.save() # Clear historical pii after removing pii from the license itself revoked_license.clear_historical_pii() revoked_license_uuids = sorted([ revoked_license.uuid for revoked_license in revoked_licenses_for_retirement ]) message = 'Retired {} revoked licenses with uuids: {}'.format( len(revoked_license_uuids), revoked_license_uuids) logger.info(message) # Any license that was assigned but not activated before the associated agreement's # ``unactivated_license_duration`` elapsed should have its data scrubbed. assigned_licenses_for_retirement = License.get_licenses_exceeding_purge_duration( 'assigned_date', status=ASSIGNED, ) # We place previously assigned licenses that are now retired back into the unassigned license pool, so we scrub # all data on them. for assigned_license in assigned_licenses_for_retirement: assigned_license.reset_to_unassigned() assigned_license.save() # Clear historical pii after removing pii from the license itself assigned_license.clear_historical_pii() assigned_license_uuids = sorted([ assigned_license.uuid for assigned_license in assigned_licenses_for_retirement ], ) message = 'Retired {} assigned licenses that exceeded their inactivation duration with uuids: {}'.format( len(assigned_license_uuids), assigned_license_uuids, ) logger.info(message)