feat: add ability for notify_credentials to revoke program certs

[APER-2504]

This is a companion to PR #32458. This updates the `notify_credentials` management command and adds an additional argument/switch (`--revoke_program_certs`).

If included, this option will be converted to a boolean and passed as a script option. Eventually, the `send_notifications` function (updated in the previously mentioned PR) will determine if we should fire a signal that checks if any program certs need to revoked.
This commit is contained in:
Justin Hynes
2023-06-14 18:21:27 +00:00
parent e607bb1208
commit 2beaa1d260
4 changed files with 43 additions and 36 deletions

View File

@@ -1,19 +1,14 @@
"""
A few places in the LMS want to notify the Credentials service when certain events
happen (like certificates being awarded or grades changing). To do this, they
listen for a signal. Sometimes we want to rebuild the data on these apps
regardless of an actual change in the database, either to recover from a bug or
to bootstrap a new feature we're rolling out for the first time.
A few places in the LMS want to notify the Credentials service when certain events happen (like certificates being
awarded or grades changing). To do this, they listen for a signal. Sometimes we want to rebuild the data on these apps
regardless of an actual change in the database, either to recover from a bug or to bootstrap a new feature we're
rolling out for the first time.
This management command will manually trigger the receivers we care about.
(We don't want to trigger all receivers for these signals, since these are busy
signals.)
This management command will manually trigger the receivers we care about. (We don't want to trigger all receivers
for these signals, since these are busy signals.)
"""
import logging
import shlex
import sys # lint-amnesty, pylint: disable=unused-import
from datetime import datetime, timedelta
import dateutil.parser
@@ -137,6 +132,11 @@ class Command(BaseCommand):
nargs='+',
help='Run the command for the given user or list of users',
)
parser.add_argument(
'--revoke_program_certs',
action='store_true',
help="If true, system will check if any program certificates need to be revoked from learners"
)
def get_args_from_database(self):
""" Returns an options dictionary from the current NotifyCredentialsConfig model. """
@@ -159,17 +159,12 @@ class Command(BaseCommand):
options['start_date'] = options['end_date'] - timedelta(hours=4)
log.info(
"notify_credentials starting, dry-run=%s, site=%s, delay=%d seconds, page_size=%d, "
"from=%s, to=%s, notify_programs=%s, user_ids=%s, execution=%s",
options['dry_run'],
options['site'],
options['delay'],
options['page_size'],
options['start_date'] if options['start_date'] else 'NA',
options['end_date'] if options['end_date'] else 'NA',
options['notify_programs'],
options['user_ids'],
'auto' if options['auto'] else 'manual',
f"notify_credentials starting, dry-run={options['dry_run']}, site={options['site']}, "
f"delay={options['delay']} seconds, page_size={options['page_size']}, "
f"from={options['start_date'] if options['start_date'] else 'NA'}, "
f"to={options['end_date'] if options['end_date'] else 'NA'}, notify_programs={options['notify_programs']}, "
f"user_ids={options['user_ids']}, execution={'auto' if options['auto'] else 'manual'}, "
f"revoke_program_certs={options['revoke_program_certs']}"
)
program_course_run_keys = self._get_course_run_keys_for_programs(options["program_uuids"])

View File

@@ -22,6 +22,7 @@ NOTIFY_CREDENTIALS_TASK = 'openedx.core.djangoapps.credentials.tasks.v1.tasks.ha
@skip_unless_lms
@mock.patch(NOTIFY_CREDENTIALS_TASK)
class TestNotifyCredentials(TestCase):
"""
Tests the ``notify_credentials`` management command.
@@ -51,9 +52,9 @@ class TestNotifyCredentials(TestCase):
'verbose': False,
'verbosity': 1,
'skip_checks': True,
'revoke_program_certs': False,
}
@mock.patch(NOTIFY_CREDENTIALS_TASK)
def test_course_args(self, mock_task):
course_1_id = 'course-v1:edX+Test+1'
course_2_id = 'course-v1:edX+Test+2'
@@ -63,7 +64,6 @@ class TestNotifyCredentials(TestCase):
assert mock_task.called
assert mock_task.call_args[0][0] == self.expected_options
@mock.patch(NOTIFY_CREDENTIALS_TASK)
@mock.patch(
'openedx.core.djangoapps.credentials.management.commands.notify_credentials.get_programs_from_cache_by_uuid'
)
@@ -88,7 +88,6 @@ class TestNotifyCredentials(TestCase):
assert mock_task.call_args[0][0] == self.expected_options
assert mock_task.call_args[0][1].sort() == [course_1_id, course_2_id].sort()
@mock.patch(NOTIFY_CREDENTIALS_TASK)
@mock.patch(
'openedx.core.djangoapps.credentials.management.commands.notify_credentials.get_programs_from_cache_by_uuid'
)
@@ -125,7 +124,6 @@ class TestNotifyCredentials(TestCase):
assert mock_task.call_args[0][1].sort() == [course_1_id, course_2_id].sort()
@freeze_time(datetime(2017, 5, 1, 4))
@mock.patch(NOTIFY_CREDENTIALS_TASK)
def test_auto_execution(self, mock_task):
self.expected_options['auto'] = True
self.expected_options['start_date'] = '2017-05-01T00:00:00'
@@ -135,7 +133,6 @@ class TestNotifyCredentials(TestCase):
assert mock_task.called
assert mock_task.call_args[0][0] == self.expected_options
@mock.patch(NOTIFY_CREDENTIALS_TASK)
def test_date_args(self, mock_task):
self.expected_options['start_date'] = '2017-01-31T00:00:00Z'
call_command(Command(), '--start-date', '2017-01-31')
@@ -163,7 +160,6 @@ class TestNotifyCredentials(TestCase):
assert mock_task.called
assert mock_task.call_args[0][0] == self.expected_options
@mock.patch(NOTIFY_CREDENTIALS_TASK)
def test_username_arg(self, mock_task):
self.expected_options['start_date'] = '2017-02-01T00:00:00Z'
self.expected_options['end_date'] = '2017-02-02T00:00:00Z'
@@ -215,13 +211,11 @@ class TestNotifyCredentials(TestCase):
assert mock_task.call_args[0][0] == self.expected_options
mock_task.reset_mock()
@mock.patch(NOTIFY_CREDENTIALS_TASK)
def test_no_args(self, mock_task):
with self.assertRaisesRegex(CommandError, 'You must specify a filter.*'):
call_command(Command())
assert not mock_task.called
@mock.patch(NOTIFY_CREDENTIALS_TASK)
def test_dry_run(self, mock_task):
self.expected_options['start_date'] = '2017-02-01T00:00:00Z'
self.expected_options['dry_run'] = True
@@ -229,7 +223,6 @@ class TestNotifyCredentials(TestCase):
assert mock_task.called
assert mock_task.call_args[0][0] == self.expected_options
@mock.patch(NOTIFY_CREDENTIALS_TASK)
def test_hand_off(self, mock_task):
self.expected_options['start_date'] = '2017-02-01T00:00:00Z'
self.expected_options['notify_programs'] = True
@@ -237,7 +230,6 @@ class TestNotifyCredentials(TestCase):
assert mock_task.called
assert mock_task.call_args[0][0] == self.expected_options
@mock.patch(NOTIFY_CREDENTIALS_TASK)
def test_delay(self, mock_task):
self.expected_options['start_date'] = '2017-02-01T00:00:00Z'
self.expected_options['delay'] = 0.2
@@ -245,7 +237,6 @@ class TestNotifyCredentials(TestCase):
assert mock_task.called
assert mock_task.call_args[0][0] == self.expected_options
@mock.patch(NOTIFY_CREDENTIALS_TASK)
def test_page_size(self, mock_task):
self.expected_options['start_date'] = '2017-02-01T00:00:00Z'
self.expected_options['page_size'] = 2
@@ -253,7 +244,6 @@ class TestNotifyCredentials(TestCase):
assert mock_task.called
assert mock_task.call_args[0][0] == self.expected_options
@mock.patch(NOTIFY_CREDENTIALS_TASK)
def test_site(self, mock_task):
site_config = SiteConfigurationFactory.create(
site_values={'course_org_filter': ['testX']}
@@ -265,7 +255,6 @@ class TestNotifyCredentials(TestCase):
assert mock_task.called
assert mock_task.call_args[0][0] == self.expected_options
@mock.patch(NOTIFY_CREDENTIALS_TASK)
def test_args_from_database(self, mock_task):
# Nothing in the database, should default to disabled
with self.assertRaisesRegex(CommandError, 'NotifyCredentialsConfig is disabled.*'):
@@ -296,3 +285,10 @@ class TestNotifyCredentials(TestCase):
# Explicitly disabled
with self.assertRaisesRegex(CommandError, 'NotifyCredentialsConfig is disabled.*'):
call_command(Command(), '--start-date', '2017-01-01', '--args-from-database')
def test_args_revoke_program_cert(self, mock_task):
self.expected_options['user_ids'] = [str(self.user.id)]
self.expected_options['revoke_program_certs'] = True
call_command(Command(), '--user_ids', self.user.id, '--revoke_program_certs')
assert mock_task.called
assert mock_task.call_args[0][0] == self.expected_options

View File

@@ -151,7 +151,8 @@ def handle_notify_credentials(options, course_keys):
delay=options['delay'],
page_size=options['page_size'],
verbose=options['verbose'],
notify_programs=options['notify_programs']
notify_programs=options['notify_programs'],
revoke_program_certs=options['revoke_program_certs']
)

View File

@@ -92,6 +92,7 @@ class TestSendGradeToCredentialTask(TestCase):
assert mock_get_api_client.call_count == (tasks.MAX_RETRIES + 1)
@ddt.ddt
@skip_unless_lms
class TestHandleNotifyCredentialsTask(TestCase):
"""
@@ -145,6 +146,7 @@ class TestHandleNotifyCredentialsTask(TestCase):
'verbose': False,
'verbosity': 1,
'skip_checks': True,
'revoke_program_certs': False,
}
@mock.patch(TASKS_MODULE + '.send_notifications')
@@ -427,6 +429,19 @@ class TestHandleNotifyCredentialsTask(TestCase):
with pytest.raises(Exception):
tasks.handle_notify_credentials(options=self.options, course_keys=[])
@ddt.data([True], [False])
@ddt.unpack
@mock.patch(TASKS_MODULE + '.send_notifications')
def test_revoke_program_certs(self, revoke_program_certs, mock_send_notifications):
"""
This test verifies that the `revoke_program_certs` option is forwarded as expected when included in the options.
"""
self.options['revoke_program_certs'] = revoke_program_certs
tasks.handle_notify_credentials(options=self.options, course_keys=[])
assert mock_send_notifications.called
mock_call_args = mock_send_notifications.call_args_list[0]
assert mock_call_args.kwargs['revoke_program_certs'] == revoke_program_certs
@ddt.ddt
@skip_unless_lms