Skip to content

Commit

Permalink
Merge pull request #526 from openedx/ammar/update-license-assign-view…
Browse files Browse the repository at this point in the history
…-to-set-license-source

feat: update license assign view to set source for assigned licenses
  • Loading branch information
muhammad-ammar authored Sep 25, 2023
2 parents 9e8d68b + ab8da8d commit 4b0eb48
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 4 deletions.
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

0 comments on commit 4b0eb48

Please sign in to comment.