Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update license assign view to set source for assigned licenses #526

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion license_manager/apps/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from django.core.validators import MinLengthValidator
from rest_framework import serializers
from rest_framework.fields import SerializerMethodField

from license_manager.apps.subscriptions.constants import ACTIVATED, ASSIGNED
from license_manager.apps.subscriptions.constants import (
ACTIVATED,
ASSIGNED,
SALESFORCE_ID_LENGTH,
)
from license_manager.apps.subscriptions.models import (
CustomerAgreement,
License,
Expand Down Expand Up @@ -361,8 +366,32 @@ class LicenseAdminAssignActionSerializer(CustomTextWithMultipleEmailsSerializer)
"""

notify_users = serializers.BooleanField(required=False)
user_sfids = serializers.ListField(
child=serializers.CharField(
allow_blank=True,
allow_null=True,
write_only=True,
validators=[MinLengthValidator(SALESFORCE_ID_LENGTH)]
),
allow_empty=False,
required=False,
error_messages={"empty": "No Salesforce Ids provided."}
)

class Meta:
fields = CustomTextWithMultipleEmailsSerializer.Meta.fields + [
'notify_users',
]

def validate(self, attrs):
user_emails = attrs.get('user_emails')
user_sfids = attrs.get('user_sfids')

if user_sfids:
# if saleforce ids list is present then its length must be equal to number of user emails
if len(user_emails) != len(user_sfids):
raise serializers.ValidationError(
'Number of Salesforce IDs did not match number of provided user emails.'
)

return super().validate(attrs)
66 changes: 66 additions & 0 deletions license_manager/apps/api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from license_manager.apps.subscriptions.exceptions import LicenseRevocationError
from license_manager.apps.subscriptions.models import (
License,
SubscriptionLicenseSource,
SubscriptionsFeatureRole,
SubscriptionsRoleAssignment,
)
Expand Down Expand Up @@ -1507,6 +1508,71 @@ def test_assign(self, use_superuser, mock_send_assignment_email_task, mock_link_
self.subscription_plan.customer_agreement.enterprise_customer_uuid
)

@mock.patch('license_manager.apps.api.v1.views.link_learners_to_enterprise_task.si')
@mock.patch('license_manager.apps.api.v1.views.send_assignment_email_task.si')
@ddt.data(True, False)
def test_assign_with_salesforce_ids(self, use_superuser, *arge, **kwargs): # pylint: disable=unused-argument
"""
Verify the assign endpoint assigns licenses and correct lincese sources are created.
"""
self._setup_request_jwt(user=self.super_user if use_superuser else self.user)
self._create_available_licenses()
user_emails = ['[email protected]', self.test_email, '[email protected]', '[email protected]']
user_sfids = ['0000qse56709sdfd08', '0000abc34MS67a8907', '', None]
response = self.api_client.post(
self.assign_url,
{'greeting': self.greeting, 'closing': self.closing, 'user_emails': user_emails, 'user_sfids': user_sfids},
)
assert response.status_code == status.HTTP_200_OK
self._assert_licenses_assigned(user_emails)

# verify that correct salesforce ids are assigned
emails_and_sfids = dict(zip(user_emails, user_sfids))
for email, salesforce_id in emails_and_sfids.items():
assigned_license = self.subscription_plan.licenses.get(user_email=email, status=constants.ASSIGNED)

if salesforce_id:
assert assigned_license.source.source_id == salesforce_id
else:
with self.assertRaises(ObjectDoesNotExist):
SubscriptionLicenseSource.objects.get(license=assigned_license)

@ddt.data(True, False)
def test_assign_with_less_salesforce_ids(self, use_superuser):
"""
Verify the assign endpoint raises correct error when user_sfids list has less items then user_emails.
"""
self._setup_request_jwt(user=self.super_user if use_superuser else self.user)
user_emails = ['[email protected]', '[email protected]']
user_sfids = ['0010abc34MS67a8907']
response = self.api_client.post(
self.assign_url,
{'greeting': self.greeting, 'closing': self.closing, 'user_emails': user_emails, 'user_sfids': user_sfids},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json() == {
'non_field_errors': [
'Number of Salesforce IDs did not match number of provided user emails.'
]
}
assert SubscriptionLicenseSource.objects.count() == 0

@ddt.data(True, False)
def test_assign_with_empty_salesforce_ids(self, use_superuser):
"""
Verify the assign endpoint raises correct error when user_sfids is present but empty.
"""
self._setup_request_jwt(user=self.super_user if use_superuser else self.user)
user_emails = ['[email protected]']
user_sfids = []
response = self.api_client.post(
self.assign_url,
{'greeting': self.greeting, 'closing': self.closing, 'user_emails': user_emails, 'user_sfids': user_sfids},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json() == {'user_sfids': ['No Salesforce Ids provided.']}
assert SubscriptionLicenseSource.objects.count() == 0

@mock.patch('license_manager.apps.api.v1.views.link_learners_to_enterprise_task.si')
@mock.patch('license_manager.apps.api.v1.views.send_assignment_email_task.si')
@ddt.data(True, False)
Expand Down
48 changes: 46 additions & 2 deletions license_manager/apps/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
from license_manager.apps.subscriptions.models import (
CustomerAgreement,
License,
SubscriptionLicenseSource,
SubscriptionLicenseSourceType,
SubscriptionPlan,
SubscriptionsRoleAssignment,
)
Expand Down Expand Up @@ -726,11 +728,36 @@ def _assign_new_licenses(self, subscription_plan, user_emails):
)
return licenses

def _set_source_for_assigned_licenses(self, assigned_licenses, emails_and_sfids):
"""
Set source for each assigned license.
"""
license_source = SubscriptionLicenseSourceType.get_source_type(SubscriptionLicenseSourceType.AMT)
source_objects = []
for assigned_license in assigned_licenses:
sf_opportunity_id = emails_and_sfids.get(assigned_license.user_email)
if sf_opportunity_id:
source = SubscriptionLicenseSource(
license=assigned_license,
source_id=sf_opportunity_id,
source_type=license_source
)
source_objects.append(source)

SubscriptionLicenseSource.objects.bulk_create(
source_objects,
batch_size=constants.LICENSE_SOURCE_BULK_OPERATION_BATCH_SIZE
)

@action(detail=False, methods=['post'])
def assign(self, request, subscription_uuid=None): # pylint: disable=unused-argument
"""
Given a list of emails, assigns a license to those user emails and sends an activation email.

Endpoint will also receive `user_sfids`, an optional list of salesforce ids. If present then
for each assigned license a source object will be created to later identify the source of a
license assignment.

Assignment is intended to be a fully atomic and linearizable operation. We utilize
a cache-based lock to ensure that only one assignment operation can be executed at a
time for the given subscription plan.
Expand All @@ -743,6 +770,10 @@ def assign(self, request, subscription_uuid=None): # pylint: disable=unused-arg
'user_emails': [
'[email protected]',
'[email protected]'
],
'user_sfids': [
'001OE000001f26OXZP',
'001OE000001a25WXYZ'
]
}
"""
Expand All @@ -769,8 +800,19 @@ def _assign(self, request, subscription_plan):
# 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', [])})
emails_and_sfids = []
# remove duplicate emails and convert to lowercase
if 'user_sfids' in request.data:
user_emails = map(str.lower, request.data.get('user_emails'))
user_sfids = request.data.get('user_sfids')
# remove whitespaces if there are any
user_sfids = [sfid and sfid.strip() for sfid in user_sfids]

emails_and_sfids = dict(zip(user_emails, user_sfids))
user_emails = list(emails_and_sfids.keys())
else:
# 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', [])})

user_emails, already_associated_emails = self._trim_already_associated_emails(
subscription_plan,
Expand All @@ -795,6 +837,8 @@ def _assign(self, request, subscription_plan):
assigned_licenses = self._assign_new_licenses(
subscription_plan, user_emails,
)
if emails_and_sfids:
self._set_source_for_assigned_licenses(assigned_licenses, emails_and_sfids)
except DatabaseError:
error_message = 'Database error occurred while assigning licenses, no assignments were completed'
logger.exception(error_message)
Expand Down
21 changes: 20 additions & 1 deletion license_manager/apps/subscriptions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ class LicenseAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
'activation_key',
'get_renewed_to',
'get_renewed_from',
'auto_applied'
'auto_applied',
'source_id',
'source_type',
]
exclude = ['history', 'renewed_to']
list_display = (
Expand Down Expand Up @@ -79,8 +81,25 @@ class LicenseAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
def get_queryset(self, request):
return super().get_queryset(request).select_related(
'subscription_plan',
'source'
)

@admin.display(description='Source ID')
def source_id(self, instance):
"""Return source id of license if a source exists"""
try:
return instance.source.source_id
except License.source.RelatedObjectDoesNotExist: # pylint: disable=no-member
return ''

@admin.display(description='Source Type')
def source_type(self, instance):
"""Return source type of license if a source exists"""
try:
return instance.source.source_type.slug
except License.source.RelatedObjectDoesNotExist: # pylint: disable=no-member
return ''

@admin.display(
description='Subscription Plan'
)
Expand Down
1 change: 1 addition & 0 deletions license_manager/apps/subscriptions/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class SegmentEvents:
# Bulk operation constants
LICENSE_BULK_OPERATION_BATCH_SIZE = 100
PENDING_ACCOUNT_CREATION_BATCH_SIZE = 100
LICENSE_SOURCE_BULK_OPERATION_BATCH_SIZE = 100

# Num distinct catalog query validation batch size
VALIDATE_NUM_CATALOG_QUERIES_BATCH_SIZE = 100
Expand Down
Loading