diff --git a/common/djangoapps/entitlements/management/commands/expire_and_create_entitlements.py b/common/djangoapps/entitlements/management/commands/expire_and_create_entitlements.py index 8f6ae14133..1bb128959f 100644 --- a/common/djangoapps/entitlements/management/commands/expire_and_create_entitlements.py +++ b/common/djangoapps/entitlements/management/commands/expire_and_create_entitlements.py @@ -1,18 +1,30 @@ # lint-amnesty, pylint: disable=django-not-configured """ -Management command for expiring old entitlements. +Management command for expiring entitlements older than 1 year / 18 months. """ - - import logging +from datetime import date +from dateutil.relativedelta import relativedelta +from math import ceil from textwrap import dedent from django.core.management import BaseCommand from common.djangoapps.entitlements.tasks import expire_and_create_entitlements +from common.djangoapps.entitlements.models import CourseEntitlement logger = logging.getLogger(__name__) # pylint: disable=invalid-name +#course uuids for which entitlements should be expired after 18 months. +MIT_SUPPLY_CHAIN_COURSES = [ + '0d9b47982e3d486aa3189a7035bbda77', + '09532745c837467b9078093b8e1265a8', + '324970b703a444d7b39e10bbda6f119f', + '5f1c55b4354e4155af4a76450953e10d', + 'ed927a1a4a95415ba865c3d722ac549c', + '6513ed9c112a495182ad7036cbe52831', +] + class Command(BaseCommand): """ @@ -47,11 +59,23 @@ class Command(BaseCommand): ) def handle(self, *args, **options): + logger.info('Looking for entitlements which may be expirable.') - total = max(1, options.get('count')) + current_date = date.today() + expiration_period = current_date - relativedelta(years=1) + exceptional_expiration_period = current_date - relativedelta(years=1, months=6) + normal_entitlements = CourseEntitlement.objects.filter( + expired_at__isnull=True, created__lte=expiration_period).exclude(course_uuid__in=MIT_SUPPLY_CHAIN_COURSES) + exceptional_entitlements = CourseEntitlement.objects.filter( + expired_at__isnull=True, created__lte=exceptional_expiration_period, course_uuid__in=MIT_SUPPLY_CHAIN_COURSES) + + entitlements = normal_entitlements | exceptional_entitlements + logger.info('Total entitlements that have reached expiration period are %d ', entitlements) + + entitlements_to_expire = max(1, options.get('count')) batch_size = max(1, options.get('batch_size')) - num_batches = ((total - 1) / batch_size + 1) if total > 0 else 0 + num_batches = ceil(entitlements_to_expire / batch_size) if entitlements else 0 if options.get('commit'): logger.info('Enqueuing %d entitlement expiration tasks.', num_batches) @@ -62,9 +86,9 @@ class Command(BaseCommand): ) return - while total > 0: - total = total - batch_size - no_of_entitlements = min(total, batch_size) - expire_and_create_entitlements.delay(no_of_entitlements) + for batch_num in range(num_batches): + start = batch_num * batch_size + end = min(start + batch_size, entitlements_to_expire) + expire_and_create_entitlements.delay(entitlements[start:end]) logger.info('Done. Successfully enqueued %d tasks.', num_batches) diff --git a/common/djangoapps/entitlements/tasks.py b/common/djangoapps/entitlements/tasks.py index b193e06e00..208da917ba 100644 --- a/common/djangoapps/entitlements/tasks.py +++ b/common/djangoapps/entitlements/tasks.py @@ -9,7 +9,7 @@ from celery.utils.log import get_task_logger from django.conf import settings # lint-amnesty, pylint: disable=unused-import from edx_django_utils.monitoring import set_code_owner_attribute -from common.djangoapps.entitlements.models import CourseEntitlement +from common.djangoapps.entitlements.models import CourseEntitlement, CourseEntitlementSupportDetail LOGGER = get_task_logger(__name__) @@ -18,15 +18,6 @@ LOGGER = get_task_logger(__name__) # time of 2047 seconds (about 30 minutes). Setting this to None could yield # unwanted behavior: infinite retries. MAX_RETRIES = 11 -#course uuids for which entitlements should be expired after 18 months. -MIT_SUPPLY_CHAIN_COURSES = [ - '0d9b47982e3d486aa3189a7035bbda77', - '09532745c837467b9078093b8e1265a8', - '324970b703a444d7b39e10bbda6f119f', - '5f1c55b4354e4155af4a76450953e10d', - 'ed927a1a4a95415ba865c3d722ac549c', - '6513ed9c112a495182ad7036cbe52831', -] @shared_task(bind=True, ignore_result=True) @@ -76,13 +67,18 @@ def expire_old_entitlements(self, start, end, logid='...'): @shared_task(bind=True, ignore_result=True) @set_code_owner_attribute -def expire_and_create_entitlements(self, no_of_entitlements): +def expire_and_create_entitlements(self, entitlements): """ - This task is designed to be called to process and expire bundle of entitlements - that are older than one year on in exceptional case 18 months. + Expire entitlements older than one year. + + Exception: if the entitlement is for a course in a list of exceptional courses, + expire those entitlements if they're older than 18 months instead. + + Then create a copy of the expired entitlement to renew it for another year + / 18 months. Args: - None + no_of_entitlements (int): Limit the operation to this number of entitlements. Returns: None @@ -90,31 +86,30 @@ def expire_and_create_entitlements(self, no_of_entitlements): """ LOGGER.info('Running task expire_and_create_entitlements') - current_date = date.today() - expiration_period = current_date - relativedelta(years=1) - exceptional_expiration_period = current_date - relativedelta(years=1, months=6) - normal_entitlements = CourseEntitlement.objects.filter( - expired_at__isnull=True, created__lte=expiration_period).exclude(course_uuid__in=MIT_SUPPLY_CHAIN_COURSES) - exceptional_entitlements = CourseEntitlement.objects.filter( - expired_at__isnull=True, created__lte=exceptional_expiration_period, course_uuid__in=MIT_SUPPLY_CHAIN_COURSES) - - entitlements = normal_entitlements | exceptional_entitlements - countdown = 2 ** self.request.retries - try: - for entitlement in entitlements[:no_of_entitlements]: - + for entitlement in entitlements: + LOGGER.info('Started expiring entitlement with id %d', entitlement.id) entitlement.expire_entitlement() - LOGGER.info('Expired entitlement with id %d ', entitlement.id) + LOGGER.info('Expired entitlement with id %d as expiration period has reached', entitlement.id) + + # Creating new entitlement with old entitlement's data entitlement.pk = None + entitlement.id = None + entitlement._state.adding = True entitlement.expired_at = None entitlement.modified = None + entitlement.refund_locked = True entitlement.save() - LOGGER.info('created new entitlement with id %d ', entitlement.id) + + support_detail = { + 'action': 'CREATE', + 'comments': 'REV-3574', + 'entitlement': entitlement, + } + CourseEntitlementSupportDetail.objects.create(**support_detail) + LOGGER.info('created new entitlement with id %d in a correspondence of above expired entitlement', entitlement.id) except Exception as exc: - LOGGER.exception('Failed to expire entitlements ',) - # The call above is idempotent, so retry at will - raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES) + LOGGER.exception('Failed to expire entitlements that reached their expiration period',) - LOGGER.info('Successfully completed the task expire_and_create_entitlements after examining %d entries', entitlements.count()) # lint-amnesty, pylint: disable=line-too-long + LOGGER.info('Successfully completed the task expire_and_create_entitlements after examining %d entries', entitlements.count()) # lint-amnesty, pylint: disable=line-too-long \ No newline at end of file