Files
edx-platform/scripts/user_retirement/retirement_bulk_status_update.py
Muhammad Farhan Khan 65ea55c8aa Move user retirement scripts code from the tubular repo (#34063)
* refactor: Migragte user retirement scripts code from the tubular repo
2024-02-22 11:09:00 -05:00

147 lines
5.0 KiB
Python
Executable File

#! /usr/bin/env python3
"""
Command-line script to bulk update retirement states in LMS
"""
import logging
import sys
from datetime import datetime
from functools import partial
from os import path
import click
from six import text_type
# Add top-level project path to sys.path before importing scripts code
sys.path.append(path.abspath(path.join(path.dirname(__file__), '../..')))
# pylint: disable=wrong-import-position
from scripts.user_retirement.utils.helpers import _config_or_exit, _fail, _fail_exception, _log, _setup_lms_api_or_exit
SCRIPT_SHORTNAME = 'Bulk Status'
# Return codes for various fail cases
ERR_NO_CONFIG = -1
ERR_BAD_CONFIG = -2
ERR_FETCHING = -3
ERR_UPDATING = -4
ERR_SETUP_FAILED = -5
LOG = partial(_log, SCRIPT_SHORTNAME)
FAIL = partial(_fail, SCRIPT_SHORTNAME)
FAIL_EXCEPTION = partial(_fail_exception, SCRIPT_SHORTNAME)
CONFIG_OR_EXIT = partial(_config_or_exit, FAIL_EXCEPTION, ERR_BAD_CONFIG)
SETUP_LMS_OR_EXIT = partial(_setup_lms_api_or_exit, FAIL, ERR_SETUP_FAILED)
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
def validate_dates(_, __, value):
"""
Click input validator for date options.
- Validates string format
- Transforms the string into a datetime.Date object
- Validates the date is less than or equal to today
- Returns the Date, or raises a click.BadParameter
"""
try:
date = datetime.strptime(value, '%Y-%m-%d').date()
if date > datetime.now().date():
raise ValueError()
return date
except ValueError:
raise click.BadParameter('Dates need to be in the format of YYYY-MM-DD and today or earlier.')
def _fetch_learners_to_update_or_exit(config, start_date, end_date, initial_state):
"""
Makes the call to fetch learners to be bulk updated, returns the list of learners
or exits.
"""
LOG('Fetching users in state {} created from {} to {}'.format(initial_state, start_date, end_date))
try:
return config['LMS'].get_learners_by_date_and_status(initial_state, start_date, end_date)
except Exception as exc: # pylint: disable=broad-except
FAIL_EXCEPTION(ERR_FETCHING, 'Unexpected error occurred fetching users to update!', exc)
def _update_learners_or_exit(config, learners, new_state=None, rewind_state=False):
"""
Iterates the list of learners, setting each to the new state. On any error
it will exit the script. If rewind_state is set to True then the learner
will be reset to their previous state.
"""
if (not new_state and not rewind_state) or (rewind_state and new_state):
FAIL(ERR_BAD_CONFIG, "You must specify either the boolean rewind_state or a new state to set learners to.")
LOG('Updating {} learners to {}'.format(len(learners), new_state))
try:
for learner in learners:
if rewind_state:
new_state = learner['last_state']['state_name']
config['LMS'].update_learner_retirement_state(
learner['original_username'],
new_state,
'Force updated via retirement_bulk_status_update script',
force=True
)
except Exception as exc: # pylint: disable=broad-except
FAIL_EXCEPTION(ERR_UPDATING, 'Unexpected error occurred updating users!', exc)
@click.command("update_statuses")
@click.option(
'--config_file',
help='YAML file that contains retirement-related configuration for this environment.'
)
@click.option(
'--initial_state',
help='Find learners in this retirement state. Use the state name ex: PENDING, COMPLETE'
)
@click.option(
'--new_state',
help='Set any found learners to this new state. Use the state name ex: PENDING, COMPLETE',
default=None
)
@click.option(
'--start_date',
callback=validate_dates,
help='(YYYY-MM-DD) Earliest creation date for retirements to act on.'
)
@click.option(
'--end_date',
callback=validate_dates,
help='(YYYY-MM-DD) Latest creation date for retirements to act on.'
)
@click.option(
'--rewind-state',
help='Rewinds to the last_state for learners. Useful for resetting ERRORED users',
default=False,
is_flag=True
)
def update_statuses(config_file, initial_state, new_state, start_date, end_date, rewind_state):
"""
Bulk-updates user retirement statuses which are in the specified state -and- retirement was
requested between a start date and end date.
"""
try:
LOG('Starting bulk update script: Config: {}'.format(config_file))
if not config_file:
FAIL(ERR_NO_CONFIG, 'No config file passed in.')
config = CONFIG_OR_EXIT(config_file)
SETUP_LMS_OR_EXIT(config)
learners = _fetch_learners_to_update_or_exit(config, start_date, end_date, initial_state)
_update_learners_or_exit(config, learners, new_state, rewind_state)
LOG('Bulk update complete')
except Exception as exc:
print(text_type(exc))
raise
if __name__ == '__main__':
# pylint: disable=unexpected-keyword-arg, no-value-for-parameter
update_statuses(auto_envvar_prefix='RETIREMENT')