-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2040 from openedx/asheehan-edx/ENT-8524
feat: adding management command to remove expired pending group memberships
- Loading branch information
Showing
11 changed files
with
282 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,4 @@ | |
Your project description goes here. | ||
""" | ||
|
||
__version__ = "4.13.2" | ||
__version__ = "4.13.3" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
enterprise/management/commands/remove_expired_pending_group_memberships.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
""" | ||
Management command for ensuring any pending group membership, ie memberships associated with a pending enterprise user, | ||
are removed after 90 days. | ||
""" | ||
|
||
import logging | ||
from datetime import timedelta | ||
|
||
from django.core.management.base import BaseCommand | ||
|
||
from enterprise.models import EnterpriseCustomer, EnterpriseGroupMembership | ||
from enterprise.utils import localized_utcnow | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class Command(BaseCommand): | ||
""" | ||
Management command for ensuring any pending group membership, ie memberships associated with a pending enterprise | ||
user, are removed after 90 days. Optionally supply a ``--enterprise_customer`` arg to only run this command on | ||
a singular customer. | ||
Example usage: | ||
$ ./manage.py remove_expired_pending_group_memberships | ||
""" | ||
help = 'Removes pending group memberships if they are older than 90 days.' | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument("-e", "--enterprise_customer") | ||
|
||
def handle(self, *args, **options): | ||
queryset = EnterpriseGroupMembership.objects.all() | ||
if enterprise_arg := options.get("enterprise_customer"): | ||
try: | ||
enterprise_customer = EnterpriseCustomer.objects.get(uuid=enterprise_arg) | ||
queryset = queryset.filter(group__enterprise_customer=enterprise_customer) | ||
except EnterpriseCustomer.DoesNotExist as exc: | ||
log.exception(f'Enterprise Customer: {enterprise_arg} not found') | ||
raise exc | ||
expired_memberships = queryset.filter( | ||
enterprise_customer_user=None, | ||
pending_enterprise_customer_user__isnull=False, | ||
created__lte=localized_utcnow() - timedelta(days=90) | ||
) | ||
for membership in expired_memberships: | ||
membership.pending_enterprise_customer_user.delete() | ||
expired_memberships.delete() |
23 changes: 23 additions & 0 deletions
23
enterprise/migrations/0202_enterprisegroup_applies_to_all_contexts_and_more.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Generated by Django 4.2.10 on 2024-03-01 17:35 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('enterprise', '0201_auto_20240227_2227'), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name='enterprisegroup', | ||
name='applies_to_all_contexts', | ||
field=models.BooleanField(default=False, help_text='When enabled, all learners connected to the org will be considered a member.', verbose_name='Set group membership to the entire org of learners.'), | ||
), | ||
migrations.AddField( | ||
model_name='historicalenterprisegroup', | ||
name='applies_to_all_contexts', | ||
field=models.BooleanField(default=False, help_text='When enabled, all learners connected to the org will be considered a member.', verbose_name='Set group membership to the entire org of learners.'), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
tests/test_enterprise/management/test_remove_expired_pending_group_memberships.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
""" | ||
Tests for the django management command `remove_expired_pending_group_memberships`. | ||
""" | ||
from datetime import timedelta | ||
|
||
from pytest import mark | ||
|
||
from django.core.management import call_command | ||
from django.test import TestCase | ||
|
||
from enterprise import models | ||
from enterprise.utils import localized_utcnow | ||
from test_utils import factories | ||
|
||
|
||
@mark.django_db | ||
class RemoveExpiredPendingGroupMembershipsCommandTests(TestCase): | ||
""" | ||
Test command `remove_expired_pending_group_memberships`. | ||
""" | ||
command = 'remove_expired_pending_group_memberships' | ||
|
||
def test_specifying_a_customer_limits_command_scope(self): | ||
""" | ||
Test that if the command is passed an optional ``--enterprise_customer`` arg, it will limit the scope of | ||
queryable objects to just that customer's memberships | ||
""" | ||
# Target membership that should be removed because it has a pending user and is over 90 days old | ||
group_to_remove_from = factories.EnterpriseGroupFactory() | ||
membership_to_remove = factories.EnterpriseGroupMembershipFactory( | ||
group=group_to_remove_from, | ||
enterprise_customer_user=None, | ||
) | ||
membership_to_remove.created = localized_utcnow() - timedelta(days=91) | ||
membership_to_remove.save() | ||
|
||
# A membership that is older than 90 days but connected to a different customer | ||
membership_to_keep = factories.EnterpriseGroupMembershipFactory( | ||
enterprise_customer_user=None, | ||
) | ||
membership_to_keep.created = localized_utcnow() - timedelta(days=91) | ||
membership_to_keep.save() | ||
|
||
call_command(self.command, enterprise_customer=str(group_to_remove_from.enterprise_customer.uuid)) | ||
|
||
assert not models.EnterpriseGroupMembership.all_objects.filter(pk=membership_to_remove.pk) | ||
assert not models.PendingEnterpriseCustomerUser.objects.filter( | ||
pk=membership_to_remove.pending_enterprise_customer_user.pk | ||
) | ||
|
||
assert models.EnterpriseGroupMembership.all_objects.filter(pk=membership_to_keep.pk) | ||
assert models.PendingEnterpriseCustomerUser.objects.filter( | ||
pk=membership_to_keep.pending_enterprise_customer_user.pk | ||
) | ||
|
||
# Sanity check | ||
call_command(self.command) | ||
assert not models.EnterpriseGroupMembership.all_objects.filter(pk=membership_to_keep.pk) | ||
assert not models.PendingEnterpriseCustomerUser.objects.filter( | ||
pk=membership_to_keep.pending_enterprise_customer_user.pk | ||
) | ||
|
||
def test_removing_old_records(self): | ||
""" | ||
Test that the command properly hard deletes membership records and pending enterprise customer user records | ||
""" | ||
# Target membership that should be removed because it has a pending user and is over 90 days old | ||
membership_to_remove = factories.EnterpriseGroupMembershipFactory( | ||
enterprise_customer_user=None, | ||
) | ||
membership_to_remove.created = localized_utcnow() - timedelta(days=91) | ||
membership_to_remove.save() | ||
|
||
# A membership that is older than 90 days but has a realized enterprise customer user | ||
old_membership_to_keep = factories.EnterpriseGroupMembershipFactory( | ||
pending_enterprise_customer_user=None, | ||
) | ||
old_membership_to_keep.created = localized_utcnow() - timedelta(days=91) | ||
old_membership_to_keep.save() | ||
|
||
# A membership that has a pending user but has not reached the 90 days cutoff | ||
new_pending_membership = factories.EnterpriseGroupMembershipFactory( | ||
enterprise_customer_user=None, | ||
) | ||
new_pending_membership.created = localized_utcnow() | ||
|
||
# Sanity check, a membership that is younger than 90 days and has a realized enterprise customer user | ||
membership = factories.EnterpriseGroupMembershipFactory( | ||
pending_enterprise_customer_user=None, | ||
) | ||
membership.created = localized_utcnow() | ||
membership.save() | ||
|
||
call_command(self.command) | ||
|
||
# Assert that memberships and pending customers are removed | ||
assert not models.EnterpriseGroupMembership.all_objects.filter(pk=membership_to_remove.pk) | ||
assert not models.PendingEnterpriseCustomerUser.objects.filter( | ||
pk=membership_to_remove.pending_enterprise_customer_user.pk | ||
) | ||
|
||
# Assert that expected memberships and users are kept | ||
assert models.EnterpriseGroupMembership.all_objects.filter(pk=old_membership_to_keep.pk) | ||
assert models.EnterpriseCustomerUser.objects.filter(pk=old_membership_to_keep.enterprise_customer_user.pk) | ||
|
||
assert models.EnterpriseGroupMembership.all_objects.filter(pk=new_pending_membership.pk) | ||
assert models.PendingEnterpriseCustomerUser.objects.filter( | ||
pk=new_pending_membership.pending_enterprise_customer_user.pk | ||
) | ||
|
||
assert models.EnterpriseGroupMembership.all_objects.filter(pk=membership.pk) | ||
assert models.EnterpriseCustomerUser.objects.filter(pk=membership.enterprise_customer_user.pk) |