Skip to content

Commit

Permalink
feat: allow for transfer of all licenses in LicenseTransferJob.
Browse files Browse the repository at this point in the history
ENT-8197 | Optionally allow license transfer jobs to transfer all
licenses from the old plan to the new plan, regardless of license status.
  • Loading branch information
iloveagent57 committed Jan 9, 2024
1 parent b773ec1 commit cede551
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 13 deletions.
13 changes: 12 additions & 1 deletion license_manager/apps/subscriptions/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,18 @@ def is_valid(self):
class LicenseTransferJobAdminForm(forms.ModelForm):
class Meta:
model = LicenseTransferJob
fields = '__all__'
fields = [
'customer_agreement',
'old_subscription_plan',
'new_subscription_plan',
'notes',
'is_dry_run',
'transfer_all',
'delimiter',
'license_uuids_raw',
'completed_at',
'processed_results',
]
# Use django-autocomplete-light to filter the available
# subscription_plan choices to only those related to
# the selected customer agreement. Works for both
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 4.2.8 on 2024-01-09 15:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('subscriptions', '0062_add_license_transfer_job'),
]

operations = [
migrations.AddField(
model_name='historicallicensetransferjob',
name='transfer_all',
field=models.BooleanField(default=False, help_text='Set to true to transfer ALL licenses from old to new plan, regardless of status.'),
),
migrations.AddField(
model_name='licensetransferjob',
name='transfer_all',
field=models.BooleanField(default=False, help_text='Set to true to transfer ALL licenses from old to new plan, regardless of status.'),
),
migrations.AlterField(
model_name='historicallicensetransferjob',
name='license_uuids_raw',
field=models.TextField(blank=True, help_text='Delimitted (with newlines by default) list of license_uuids to transfer', null=True),
),
migrations.AlterField(
model_name='licensetransferjob',
name='license_uuids_raw',
field=models.TextField(blank=True, help_text='Delimitted (with newlines by default) list of license_uuids to transfer', null=True),
),
]
30 changes: 21 additions & 9 deletions license_manager/apps/subscriptions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1373,9 +1373,13 @@ class LicenseTransferJob(TimeStampedModel):
null=False,
default='newline',
)
transfer_all = models.BooleanField(
default=False,
help_text=_("Set to true to transfer ALL licenses from old to new plan, regardless of status."),
)
license_uuids_raw = models.TextField(
null=False,
blank=False,
null=True,
blank=True,
help_text=_("Delimitted (with newlines by default) list of license_uuids to transfer"),
)
processed_results = models.JSONField(
Expand Down Expand Up @@ -1411,6 +1415,10 @@ def clean(self):
raise ValidationError(
'LicenseTransferJob: Old and new subscription plans must have same customer_agreement.'
)
if not self.transfer_all and not self.license_uuids_raw:
raise ValidationError(
'LicenseTransferJob: Must specify either transfer_all or license_uuids_raw.'
)

def get_customer_agreement(self):
try:
Expand All @@ -1428,14 +1436,18 @@ def get_licenses_to_transfer(self):
"""
Yields successive chunked querysets of License records to transfer.
The licenses are from self.old_subscription_plan and will
only be in the (activated, assigned) statuses.
only be in the (activated, assigned) statuses, unless ``transfer_all``
is True, in which case **all** licenses will be included.
"""
for license_uuid_chunk in chunks(self.get_license_uuids(), self.CHUNK_SIZE):
yield License.objects.filter(
subscription_plan=self.old_subscription_plan,
status__in=[ACTIVATED, ASSIGNED],
uuid__in=license_uuid_chunk,
)
if self.transfer_all:
yield License.objects.filter(subscription_plan=self.old_subscription_plan)
else:
for license_uuid_chunk in chunks(self.get_license_uuids(), self.CHUNK_SIZE):
yield License.objects.filter(
subscription_plan=self.old_subscription_plan,
status__in=[ACTIVATED, ASSIGNED],
uuid__in=license_uuid_chunk,
)

def process(self):
"""
Expand Down
45 changes: 42 additions & 3 deletions license_manager/apps/subscriptions/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,19 +484,18 @@ def tearDown(self):
super().tearDown()
License.objects.all().delete()

def _create_transfer_job(self, license_uuids_raw, **kwargs):
def _create_transfer_job(self, **kwargs):
return LicenseTransferJob.objects.create(
customer_agreement=self.customer_agreement,
old_subscription_plan=self.old_plan,
new_subscription_plan=self.new_plan,
license_uuids_raw=license_uuids_raw,
**kwargs,
)

def test_get_licenses_to_transfer(self):
"""
Tests that we only operate on activated or assigned licenses from the old plan
of a transfer job.
of a transfer job when `transfer_all` is not selected.
"""
old_assigned_licenses = LicenseFactory.create_batch(
3, subscription_plan=self.old_plan, assigned_date=localized_utcnow(), status=ASSIGNED,
Expand Down Expand Up @@ -528,6 +527,46 @@ def test_get_licenses_to_transfer(self):
}
self.assertEqual(expected_licenses, actual_licenses)

def test_transfer_all(self):
"""
Tests that we transfer all licenses when `transfer_all` is selected.
"""
old_assigned_licenses = LicenseFactory.create_batch(
3, subscription_plan=self.old_plan, assigned_date=localized_utcnow(), status=ASSIGNED,
)
old_activated_licenses = LicenseFactory.create_batch(
3, subscription_plan=self.old_plan, assigned_date=localized_utcnow(), status=ACTIVATED,
)
# old unassigned licenses
old_unassigned_licenses = LicenseFactory.create_batch(
3, subscription_plan=self.old_plan,
)
# new_licenses
LicenseFactory.create_batch(
3, subscription_plan=self.new_plan, assigned_date=localized_utcnow(), status=ACTIVATED,
)

job = self._create_transfer_job(transfer_all=True)

expected_licenses = {
_license.uuid: _license
for _license in old_assigned_licenses + old_activated_licenses + old_unassigned_licenses
}
actual_licenses = {
_license.uuid: _license
for license_batch in job.get_licenses_to_transfer()
for _license in license_batch
}
self.assertEqual(expected_licenses, actual_licenses)

job.process()

self.assertEqual(self.old_plan.licenses.all().count(), 0)
self.assertEqual(self.new_plan.licenses.all().count(), 12)
for _license in expected_licenses.values():
_license.refresh_from_db()
self.assertEqual(_license.subscription_plan, self.new_plan)

def test_transfer_dry_run_processing(self):
"""
Tests that a dry-run process doesn't actually modify the
Expand Down

0 comments on commit cede551

Please sign in to comment.