Skip to content

Commit

Permalink
Merge pull request #408 from openedx/kiram15/ENT-8410
Browse files Browse the repository at this point in the history
feat: adding the PolicyGroupAssociation table
  • Loading branch information
kiram15 authored Feb 21, 2024
2 parents 60e810c + f1a8686 commit 74afde6
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ def test_update_views(self, is_patch, request_payload):
def test_update_views_fields_disallowed_for_update(self, request_payload):
"""
Test that the update and partial_update views can NOT modify fields
of a policy record that are not included in the update request serializer fields defintion.
of a policy record that are not included in the update request serializer fields definition.
"""
# Set the JWT-based auth to an operator.
self.set_jwt_cookie([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.10 on 2024-02-15 21:00

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('content_assignments', '0017_migrate_automatic_cancellation_to_expired'),
]

operations = [
migrations.AlterField(
model_name='historicallearnercontentassignmentaction',
name='action_type',
field=models.CharField(choices=[('learner_linked', 'Learner linked to customer'), ('notified', 'Learner notified of assignment'), ('reminded', 'Learner reminded about assignment'), ('redeemed', 'Learner redeemed the assigned content'), ('cancelled', 'Learner assignment cancelled'), ('cancelled_acknowledged', 'Learner assignment cancellation acknowledged by learner'), ('expired', 'Learner assignment expired'), ('expired_acknowledged', 'Learner assignment expiration acknowledged by learner')], db_index=True, help_text='The type of action take on the related assignment record.', max_length=255),
),
migrations.AlterField(
model_name='learnercontentassignmentaction',
name='action_type',
field=models.CharField(choices=[('learner_linked', 'Learner linked to customer'), ('notified', 'Learner notified of assignment'), ('reminded', 'Learner reminded about assignment'), ('redeemed', 'Learner redeemed the assigned content'), ('cancelled', 'Learner assignment cancelled'), ('cancelled_acknowledged', 'Learner assignment cancellation acknowledged by learner'), ('expired', 'Learner assignment expired'), ('expired_acknowledged', 'Learner assignment expiration acknowledged by learner')], db_index=True, help_text='The type of action take on the related assignment record.', max_length=255),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.10 on 2024-02-15 21:00

from django.db import migrations, models
import django.db.models.deletion
import django_extensions.db.fields
import uuid


class Migration(migrations.Migration):

dependencies = [
('subsidy_access_policy', '0021_subsidyaccesspolicy_retired'),
]

operations = [
migrations.CreateModel(
name='PolicyGroupAssociation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('enterprise_group_uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='The uuid that uniquely identifies the associated group.', unique=True)),
('subsidy_access_policy', models.ForeignKey(help_text='The SubsidyAccessPolicy that this group is tied to.', on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='subsidy_access_policy.subsidyaccesspolicy')),
],
options={
'unique_together': {('subsidy_access_policy', 'enterprise_group_uuid')},
},
),
]
29 changes: 29 additions & 0 deletions enterprise_access/apps/subsidy_access_policy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1426,3 +1426,32 @@ def allocate(self, learner_emails, content_key, content_price_cents):
content_key,
content_price_cents,
)


class PolicyGroupAssociation(TimeStampedModel):
"""
This model ties together a policy (SubsidyAccessPolicy) and a group (EnterpriseGroup in edx-enterprise).
.. no_pii: This model has no PII
"""

class Meta:
unique_together = [
('subsidy_access_policy', 'enterprise_group_uuid'),
]

subsidy_access_policy = models.ForeignKey(
SubsidyAccessPolicy,
related_name="groups",
on_delete=models.CASCADE,
null=False,
help_text="The SubsidyAccessPolicy that this group is tied to.",
)

enterprise_group_uuid = models.UUIDField(
default=uuid4,
editable=False,
unique=True,
null=False,
help_text='The uuid that uniquely identifies the associated group.',
)
15 changes: 14 additions & 1 deletion enterprise_access/apps/subsidy_access_policy/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from enterprise_access.apps.subsidy_access_policy.models import (
AssignedLearnerCreditAccessPolicy,
PerLearnerEnrollmentCreditAccessPolicy,
PerLearnerSpendCreditAccessPolicy
PerLearnerSpendCreditAccessPolicy,
PolicyGroupAssociation
)

FAKER = Faker()
Expand Down Expand Up @@ -64,3 +65,15 @@ class Meta:
spend_limit = factory.LazyAttribute(lambda _: FAKER.pyint())
per_learner_spend_limit = None
per_learner_enrollment_limit = None


class PolicyGroupAssociationFactory(factory.django.DjangoModelFactory):
"""
Test factory for the `PolicyGroupAssociation` model.
"""

class Meta:
model = PolicyGroupAssociation

enterprise_group_uuid = factory.LazyFunction(uuid4)
subsidy_access_policy = factory.SubFactory(SubsidyAccessPolicyFactory)
73 changes: 56 additions & 17 deletions enterprise_access/apps/subsidy_access_policy/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
from enterprise_access.apps.subsidy_access_policy.tests.factories import (
AssignedLearnerCreditAccessPolicyFactory,
PerLearnerEnrollmentCapLearnerCreditAccessPolicyFactory,
PerLearnerSpendCapLearnerCreditAccessPolicyFactory
PerLearnerSpendCapLearnerCreditAccessPolicyFactory,
PolicyGroupAssociationFactory
)
from enterprise_access.cache_utils import request_cache

Expand Down Expand Up @@ -695,34 +696,34 @@ def setUp(self):
self.policy_three = PerLearnerEnrollmentCapLearnerCreditAccessPolicyFactory.create()
self.policy_four = PerLearnerSpendCapLearnerCreditAccessPolicyFactory.create()

policy_one_subsity_patcher = patch.object(
policy_one_subsidy_patcher = patch.object(
self.policy_one, 'subsidy_record'
)
self.mock_policy_one_subsidy_record = policy_one_subsity_patcher.start()
self.mock_policy_one_subsidy_record = policy_one_subsidy_patcher.start()
self.mock_policy_one_subsidy_record.return_value = self.mock_subsidy_one

policy_two_subsity_patcher = patch.object(
policy_two_subsidy_patcher = patch.object(
self.policy_two, 'subsidy_record'
)
self.mock_policy_two_subsidy_record = policy_two_subsity_patcher.start()
self.mock_policy_two_subsidy_record = policy_two_subsidy_patcher.start()
self.mock_policy_two_subsidy_record.return_value = self.mock_subsidy_two

policy_three_subsity_patcher = patch.object(
policy_three_subsidy_patcher = patch.object(
self.policy_three, 'subsidy_record'
)
self.mock_policy_three_subsidy_record = policy_three_subsity_patcher.start()
self.mock_policy_three_subsidy_record = policy_three_subsidy_patcher.start()
self.mock_policy_three_subsidy_record.return_value = self.mock_subsidy_three

policy_four_subsity_patcher = patch.object(
policy_four_subsidy_patcher = patch.object(
self.policy_four, 'subsidy_record'
)
self.mock_policy_four_subsidy_record = policy_four_subsity_patcher.start()
self.mock_policy_four_subsidy_record = policy_four_subsidy_patcher.start()
self.mock_policy_four_subsidy_record.return_value = self.mock_subsidy_four

self.addCleanup(policy_one_subsity_patcher.stop)
self.addCleanup(policy_two_subsity_patcher.stop)
self.addCleanup(policy_three_subsity_patcher.stop)
self.addCleanup(policy_four_subsity_patcher.stop)
self.addCleanup(policy_one_subsidy_patcher.stop)
self.addCleanup(policy_two_subsidy_patcher.stop)
self.addCleanup(policy_three_subsidy_patcher.stop)
self.addCleanup(policy_four_subsidy_patcher.stop)

def test_setup(self):
"""
Expand Down Expand Up @@ -752,7 +753,7 @@ def test_resolve_two_policies_by_balance(self):
@override_settings(MULTI_POLICY_RESOLUTION_ENABLED=True)
def test_resolve_two_policies_by_expiration(self):
"""
Test resolve given a two policies with different balances, differet expiration
Test resolve given a two policies with different balances, different expiration
the sooner expiration policy should be returned.
"""
policies = [self.policy_one, self.policy_three]
Expand All @@ -765,7 +766,7 @@ def test_resolve_two_policies_by_type_priority(self):
but different type-priority.
"""
policies = [self.policy_four, self.policy_one]
# artificially set the priority attribute higher on one of the policies (lower priority takes precident)
# artificially set the priority attribute higher on one of the policies (lower priority takes precedent)
with patch.object(PerLearnerSpendCreditAccessPolicy, 'priority', new_callable=PropertyMock) as mock:
mock.return_value = 100
assert SubsidyAccessPolicy.resolve_policy(policies) == self.policy_one
Expand Down Expand Up @@ -1079,7 +1080,7 @@ def test_redeem(
assert redeemed_action.action_type == AssignmentActions.REDEEMED
assert not redeemed_action.error_reason
if fail_subsidy_create_transaction:
# sad path should generate a failed redeememd action with populated error_reason and traceback.
# sad path should generate a failed redeemed action with populated error_reason and traceback.
redeemed_action = assignment.actions.last()
assert redeemed_action.action_type == AssignmentActions.REDEEMED
assert redeemed_action.error_reason == AssignmentActionErrors.ENROLLMENT_ERROR
Expand Down Expand Up @@ -1234,7 +1235,7 @@ def test_can_allocate_happy_path(self):
self.mock_assignments_api.get_allocated_quantity_for_configuration.return_value = -1000

# Request a price just slightly different from the canonical price
# the subidy remaining balance and the spend limit are both 10,000
# the subsidy remaining balance and the spend limit are both 10,000
# ((7 * 1000) + 1000 + 1000) < 10000
can_allocate, _ = self.active_policy.can_allocate(7, self.course_key, 1000)

Expand Down Expand Up @@ -1269,3 +1270,41 @@ def test_can_allocate_invalid_price(self, real_price, requested_price):
}
with self.assertRaisesRegex(PriceValidationError, 'outside of acceptable interval'):
self.active_policy.can_allocate(1, self.course_key, requested_price)


@ddt.ddt
class PolicyGroupAssociationTests(MockPolicyDependenciesMixin, TestCase):
""" Tests specific to the policy group association model. """

lms_user_id = 12345
group_uuid = uuid4()

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.access_policy = AssignedLearnerCreditAccessPolicyFactory()

def tearDown(self):
"""
Clears any cached data for the test policy instances between test runs.
"""
super().tearDown()
request_cache(namespace=REQUEST_CACHE_NAMESPACE).clear()

def test_save(self):
"""
Test that the model-level validation of this model works as expected.
Should be saved with a unique combination of SubsidyAccessPolicy
and group uuid (enterprise_customer_uuid).
"""

policy = PolicyGroupAssociationFactory(
enterprise_group_uuid=self.group_uuid,
subsidy_access_policy=self.access_policy,
)

policy.save()
policy.refresh_from_db()

self.assertEqual(policy.enterprise_group_uuid, self.group_uuid)
self.assertIsNotNone(policy.subsidy_access_policy)

0 comments on commit 74afde6

Please sign in to comment.