From cede5511dfca995159a27f3017f8c1f7b0eb5783 Mon Sep 17 00:00:00 2001 From: Alex Dusenbery Date: Mon, 8 Jan 2024 09:53:24 -0500 Subject: [PATCH] feat: allow for transfer of all licenses in `LicenseTransferJob`. ENT-8197 | Optionally allow license transfer jobs to transfer all licenses from the old plan to the new plan, regardless of license status. --- license_manager/apps/subscriptions/forms.py | 13 +++++- .../migrations/0063_transfer_all_licenses.py | 33 ++++++++++++++ license_manager/apps/subscriptions/models.py | 30 +++++++++---- .../apps/subscriptions/tests/test_models.py | 45 +++++++++++++++++-- 4 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 license_manager/apps/subscriptions/migrations/0063_transfer_all_licenses.py diff --git a/license_manager/apps/subscriptions/forms.py b/license_manager/apps/subscriptions/forms.py index 30fe3e24..e6324795 100644 --- a/license_manager/apps/subscriptions/forms.py +++ b/license_manager/apps/subscriptions/forms.py @@ -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 diff --git a/license_manager/apps/subscriptions/migrations/0063_transfer_all_licenses.py b/license_manager/apps/subscriptions/migrations/0063_transfer_all_licenses.py new file mode 100644 index 00000000..c9c5411b --- /dev/null +++ b/license_manager/apps/subscriptions/migrations/0063_transfer_all_licenses.py @@ -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), + ), + ] diff --git a/license_manager/apps/subscriptions/models.py b/license_manager/apps/subscriptions/models.py index 2d21ffb7..321f9e47 100644 --- a/license_manager/apps/subscriptions/models.py +++ b/license_manager/apps/subscriptions/models.py @@ -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( @@ -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: @@ -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): """ diff --git a/license_manager/apps/subscriptions/tests/test_models.py b/license_manager/apps/subscriptions/tests/test_models.py index 3e941dcc..ae76f8e0 100644 --- a/license_manager/apps/subscriptions/tests/test_models.py +++ b/license_manager/apps/subscriptions/tests/test_models.py @@ -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, @@ -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