Skip to content

Commit

Permalink
feat: editable desired_num_licenses, license-creation on plans as action
Browse files Browse the repository at this point in the history
Creates a simple django admin action to make the actual license count equal
the desired license count via SubscriptionPlanAdmin. The logic to create actual
licenses via subscription plan admin now takes the presence of last_freeze_timestamp into account.
No longer try to rectify desired-vs-actual on save of subscription plans via admin.
Makes desired_num_license editable via admin, so that our EstimatedCountLicensePagintion
doesn't completely lie when we manually remove licenses from a plan.
ENT-8905
  • Loading branch information
iloveagent57 committed May 8, 2024
1 parent 679fd53 commit 720e561
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 20 deletions.
50 changes: 38 additions & 12 deletions license_manager/apps/subscriptions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,6 @@ class SubscriptionPlanAdmin(DjangoQLSearchMixin, SimpleHistoryAdmin):
read_only_fields = [
'num_revocations_remaining',
'num_licenses',
'desired_num_licenses',
'expiration_processed',
'customer_agreement',
'last_freeze_timestamp',
Expand All @@ -204,6 +203,7 @@ class SubscriptionPlanAdmin(DjangoQLSearchMixin, SimpleHistoryAdmin):
# Writable fields appear higher on the page.
writable_fields = [
'title',
'desired_num_licenses',
'start_date',
'expiration_date',
'enterprise_catalog_uuid',
Expand Down Expand Up @@ -289,7 +289,10 @@ def get_fields(self, request, obj=None):

autocomplete_fields = ['customer_agreement']

actions = ['process_unused_licenses_post_freeze']
actions = [
'process_unused_licenses_post_freeze',
'create_actual_licenses_action',
]

def get_queryset(self, request):
return super().get_queryset(request).select_related(
Expand Down Expand Up @@ -342,6 +345,36 @@ def process_unused_licenses_post_freeze(self, request, queryset):
except UnprocessableSubscriptionPlanFreezeError as exc:
messages.add_message(request, messages.ERROR, exc)

@admin.action(
description='Create actual licenses to match desired number'
)
def create_actual_licenses_action(self, request, queryset):
"""
Django action to make the actual number of License records associated with this
plan match the *desired* number of licenses for the plan.
"""
for subscription_plan in queryset:
self._create_actual_licenses(subscription_plan)

messages.add_message(
request, messages.SUCCESS, 'Successfully created license records for selected Subscription Plans.',
)

def _create_actual_licenses(self, obj):
"""
Provision any additional licenses if necessary, assuming that the plan
has not been "frozen"
"""
if obj.desired_num_licenses and not obj.last_freeze_timestamp:
license_count_gap = obj.desired_num_licenses - obj.num_licenses
if license_count_gap > 0:
if license_count_gap <= PROVISION_LICENSES_BATCH_SIZE:
# We can handle just one batch synchronously.
SubscriptionPlan.increase_num_licenses(obj, license_count_gap)
else:
# Multiple batches of licenses will need to be created, so provision them asynchronously.
provision_licenses_task.delay(subscription_plan_uuid=obj.uuid)

def save_model(self, request, obj, form, change):
# Record change reason for simple history
obj._change_reason = form.cleaned_data.get('change_reason') # pylint: disable=protected-access
Expand All @@ -357,16 +390,9 @@ def save_model(self, request, obj, form, change):

super().save_model(request, obj, form, change)

# Finally, provision any additional licenses if necessary.
if obj.desired_num_licenses:
license_count_gap = obj.desired_num_licenses - obj.num_licenses
if license_count_gap > 0:
if license_count_gap <= PROVISION_LICENSES_BATCH_SIZE:
# We can handle just one batch synchronously.
SubscriptionPlan.increase_num_licenses(obj, license_count_gap)
else:
# Multiple batches of licenses will need to be created, so provision them asynchronously.
provision_licenses_task.delay(subscription_plan_uuid=obj.uuid)
# Finally, if we're creating the model instance, go ahead and create the related license records.
if not change:
self._create_actual_licenses(obj)


@admin.register(CustomerAgreement)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.9 on 2024-05-08 14:27

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('subscriptions', '0066_license_subscription_plan_status_idx'),
]

operations = [
migrations.AlterField(
model_name='historicalsubscriptionplan',
name='desired_num_licenses',
field=models.PositiveIntegerField(blank=True, help_text='Total number of licenses that should exist for this SubscriptionPlan. The total license count (provisioned asynchronously) will reach the desired amount eventually. Empty (NULL) means no attempts will be made to asynchronously provision licenses.', null=True, verbose_name='Desired Number of Licenses'),
),
migrations.AlterField(
model_name='subscriptionplan',
name='desired_num_licenses',
field=models.PositiveIntegerField(blank=True, help_text='Total number of licenses that should exist for this SubscriptionPlan. The total license count (provisioned asynchronously) will reach the desired amount eventually. Empty (NULL) means no attempts will be made to asynchronously provision licenses.', null=True, verbose_name='Desired Number of Licenses'),
),
]
1 change: 0 additions & 1 deletion license_manager/apps/subscriptions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,6 @@ class SubscriptionPlan(TimeStampedModel):
desired_num_licenses = models.PositiveIntegerField(
blank=True,
null=True,
editable=False,
verbose_name="Desired Number of Licenses",
help_text=(
"Total number of licenses that should exist for this SubscriptionPlan. "
Expand Down
39 changes: 32 additions & 7 deletions license_manager/apps/subscriptions/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,28 @@
from unittest import mock

import pytest
from django.contrib import messages
from django.contrib.admin.sites import AdminSite
from django.test import RequestFactory

from license_manager.apps.subscriptions.admin import (
CustomerAgreementAdmin,
SubscriptionPlanAdmin,
)
from license_manager.apps.subscriptions.forms import SubscriptionPlanForm
from license_manager.apps.subscriptions.models import (
CustomerAgreement,
SubscriptionPlan,
)
from license_manager.apps.subscriptions.tests.factories import (
CustomerAgreementFactory,
PlanTypeFactory,
SubscriptionPlanFactory,
UserFactory,
)
from license_manager.apps.subscriptions.tests.utils import (
make_bound_customer_agreement_form,
make_bound_subscription_form,
)
from license_manager.apps.subscriptions.utils import localized_utcnow


@pytest.mark.django_db
Expand All @@ -44,9 +44,10 @@ def test_licenses_subscription_creation():


@pytest.mark.django_db
def test_licenses_subscription_modification():
@mock.patch('license_manager.apps.subscriptions.admin.messages.add_message')
def test_subscription_licenses_create_action(mock_add_message):
"""
Verify that creating a SubscriptionPlan creates its associated Licenses after it is created.
Verify that running the create licenses action will create licenses.
"""
# Setup an existing plan
customer_agreement = CustomerAgreementFactory()
Expand All @@ -63,15 +64,39 @@ def test_licenses_subscription_modification():

# doesn't really matter what we put for num_licenses in here, save_model
# will read the desired number of license from the existing object on save.
form = make_bound_subscription_form(num_licenses=1)
form = make_bound_subscription_form(num_licenses=10)
form.save()

# save the form as a modify instead of create
subscription_admin.save_model(request, subscription_plan, form, True)
subscription_plan.refresh_from_db()
# Desired number of licenses won't actually be created until we run the action
assert subscription_plan.licenses.count() == 0

# Now run the action...
subscription_admin.create_actual_licenses_action(
request,
SubscriptionPlan.objects.filter(uuid=subscription_plan.uuid),
)
subscription_plan.refresh_from_db()
# Actual number of licenses should now equal the desired number (ten)
assert subscription_plan.licenses.count() == 10

mock_add_message.assert_called_once_with(
request, messages.SUCCESS, 'Successfully created license records for selected Subscription Plans.',
)

# The save_model() method should determine that licenses need to be created,
# and then create them (synchronously in this case, since its a small number of licenses).
# check that freezing the plan means running the create licenses action has no effect
subscription_plan.last_freeze_timestamp = localized_utcnow()
subscription_plan.desired_num_licenses = 5000
subscription_plan.save()

subscription_admin.create_actual_licenses_action(
request,
SubscriptionPlan.objects.filter(uuid=subscription_plan.uuid),
)
subscription_plan.refresh_from_db()
# Actual number of licenses should STILL equal 10, because the plan is frozen
assert subscription_plan.licenses.count() == 10


Expand Down

0 comments on commit 720e561

Please sign in to comment.