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:
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user