diff --git a/openedx/core/djangoapps/credentials/admin.py b/openedx/core/djangoapps/credentials/admin.py index 18c91fc06f..556f5008d1 100644 --- a/openedx/core/djangoapps/credentials/admin.py +++ b/openedx/core/djangoapps/credentials/admin.py @@ -5,11 +5,14 @@ Django admin pages for credentials support models. from config_models.admin import ConfigurationModelAdmin from django.contrib import admin -from openedx.core.djangoapps.credentials.models import CredentialsApiConfig +from openedx.core.djangoapps.credentials.models import CredentialsApiConfig, NotifyCredentialsConfig +@admin.register(CredentialsApiConfig) class CredentialsApiConfigAdmin(ConfigurationModelAdmin): pass -admin.site.register(CredentialsApiConfig, CredentialsApiConfigAdmin) +@admin.register(NotifyCredentialsConfig) +class NotifyCredentialsConfigAdmin(ConfigurationModelAdmin): + pass diff --git a/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py b/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py index 5593cde24a..61d19a1c63 100644 --- a/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py +++ b/openedx/core/djangoapps/credentials/management/commands/notify_credentials.py @@ -24,6 +24,7 @@ from pytz import UTC from lms.djangoapps.certificates.models import GeneratedCertificate from lms.djangoapps.grades.models import PersistentCourseGrade +from openedx.core.djangoapps.credentials.models import NotifyCredentialsConfig from openedx.core.djangoapps.credentials.signals import handle_cert_change, send_grade_if_interesting from openedx.core.djangoapps.programs.signals import handle_course_cert_changed from openedx.core.djangoapps.site_configuration.models import SiteConfiguration @@ -134,8 +135,28 @@ class Command(BaseCommand): default=100, help="Number of items to query at once.", ) + parser.add_argument( + '--args-from-database', + action='store_true', + help='Use arguments from the NotifyCredentialsConfig model instead of the command line.', + ) + + def get_args_from_database(self): + """ Returns an options dictionary from the current NotifyCredentialsConfig model. """ + config = NotifyCredentialsConfig.current() + if not config.enabled: + raise CommandError('NotifyCredentialsConfig is disabled, but --args-from-database was requested.') + + # We don't need fancy shell-style whitespace/quote handling - none of our arguments are complicated + argv = config.arguments.split() + + parser = self.create_parser('manage.py', 'notify_credentials') + return parser.parse_args(argv).__dict__ # we want a dictionary, not a non-iterable Namespace object def handle(self, *args, **options): + if options['args_from_database']: + options = self.get_args_from_database() + log.info( "notify_credentials starting, dry-run=%s, site=%s, delay=%d seconds", options['dry_run'], diff --git a/openedx/core/djangoapps/credentials/management/commands/tests/test_notify_credentials.py b/openedx/core/djangoapps/credentials/management/commands/tests/test_notify_credentials.py index 3bc07303d3..52749dbb65 100644 --- a/openedx/core/djangoapps/credentials/management/commands/tests/test_notify_credentials.py +++ b/openedx/core/djangoapps/credentials/management/commands/tests/test_notify_credentials.py @@ -14,6 +14,7 @@ from freezegun import freeze_time from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory from lms.djangoapps.grades.models import PersistentCourseGrade +from openedx.core.djangoapps.credentials.models import NotifyCredentialsConfig from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory from openedx.core.djangolib.testing.utils import skip_unless_lms from student.tests.factories import UserFactory @@ -131,3 +132,30 @@ class TestNotifyCredentials(TestCase): call_command(Command(), '--site', site_config.site.domain, '--start-date', '2017-01-01') self.assertEqual(mock_grade_interesting.call_count, 1) self.assertEqual(mock_cert_change.call_count, 1) + + @mock.patch(COMMAND_MODULE + '.Command.send_notifications') + def test_args_from_database(self, mock_send): + # Nothing in the database, should default to disabled + with self.assertRaisesRegex(CommandError, 'NotifyCredentialsConfig is disabled.*'): + call_command(Command(), '--start-date', '2017-01-01', '--args-from-database') + + # Add a config + config = NotifyCredentialsConfig.current() + config.arguments = '--start-date 2017-03-01' + config.enabled = True + config.save() + + # Not told to use config, should ignore it + call_command(Command(), '--start-date', '2017-01-01') + self.assertEqual(len(mock_send.call_args[0][0]), 3) + + # Told to use it, and enabled. Should use config in preference of command line + call_command(Command(), '--start-date', '2017-01-01', '--args-from-database') + self.assertEqual(len(mock_send.call_args[0][0]), 1) + + config.enabled = False + config.save() + + # Explicitly disabled + with self.assertRaisesRegex(CommandError, 'NotifyCredentialsConfig is disabled.*'): + call_command(Command(), '--start-date', '2017-01-01', '--args-from-database') diff --git a/openedx/core/djangoapps/credentials/migrations/0004_notifycredentialsconfig.py b/openedx/core/djangoapps/credentials/migrations/0004_notifycredentialsconfig.py new file mode 100644 index 0000000000..d1defe9887 --- /dev/null +++ b/openedx/core/djangoapps/credentials/migrations/0004_notifycredentialsconfig.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.15 on 2018-08-17 18:14 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('credentials', '0003_auto_20170525_1109'), + ] + + operations = [ + migrations.CreateModel( + name='NotifyCredentialsConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')), + ('enabled', models.BooleanField(default=False, verbose_name='Enabled')), + ('arguments', models.TextField(blank=True, default='', help_text='Useful for manually running a Jenkins job. Specify like "--start-date=2018 --courses A B".')), + ('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')), + ], + options={ + 'verbose_name': 'notify_credentials argument', + }, + ), + ] diff --git a/openedx/core/djangoapps/credentials/models.py b/openedx/core/djangoapps/credentials/models.py index 7fa094222c..f872e49ccf 100644 --- a/openedx/core/djangoapps/credentials/models.py +++ b/openedx/core/djangoapps/credentials/models.py @@ -108,3 +108,22 @@ class CredentialsApiConfig(ConfigurationModel): def is_cache_enabled(self): """Whether responses from the Credentials API will be cached.""" return self.cache_ttl > 0 + + +class NotifyCredentialsConfig(ConfigurationModel): + """ + Manages configuration for a run of the notify_credentials management command. + """ + + class Meta(object): + app_label = 'credentials' + verbose_name = 'notify_credentials argument' + + arguments = models.TextField( + blank=True, + help_text='Useful for manually running a Jenkins job. Specify like "--start-date=2018 --courses A B".', + default='', + ) + + def __unicode__(self): + return unicode(self.arguments)