diff --git a/openedx/core/djangoapps/content/course_overviews/admin.py b/openedx/core/djangoapps/content/course_overviews/admin.py index 45d5ad5939..f53c3049ca 100644 --- a/openedx/core/djangoapps/content/course_overviews/admin.py +++ b/openedx/core/djangoapps/content/course_overviews/admin.py @@ -8,7 +8,7 @@ from __future__ import absolute_import from config_models.admin import ConfigurationModelAdmin from django.contrib import admin -from .models import CourseOverview, CourseOverviewImageConfig, CourseOverviewImageSet +from .models import CourseOverview, CourseOverviewImageConfig, CourseOverviewImageSet, SimulateCoursePublishConfig class CourseOverviewAdmin(admin.ModelAdmin): @@ -73,6 +73,11 @@ class CourseOverviewImageSetAdmin(admin.ModelAdmin): fields = ('course_overview_id', 'small_url', 'large_url') +class SimulateCoursePublishConfigAdmin(ConfigurationModelAdmin): + pass + + admin.site.register(CourseOverview, CourseOverviewAdmin) admin.site.register(CourseOverviewImageConfig, CourseOverviewImageConfigAdmin) admin.site.register(CourseOverviewImageSet, CourseOverviewImageSetAdmin) +admin.site.register(SimulateCoursePublishConfig, SimulateCoursePublishConfigAdmin) diff --git a/openedx/core/djangoapps/content/course_overviews/management/commands/simulate_publish.py b/openedx/core/djangoapps/content/course_overviews/management/commands/simulate_publish.py index bcd25d4a05..7df4196eb7 100644 --- a/openedx/core/djangoapps/content/course_overviews/management/commands/simulate_publish.py +++ b/openedx/core/djangoapps/content/course_overviews/management/commands/simulate_publish.py @@ -21,10 +21,10 @@ import textwrap import time import six -from django.core.management.base import BaseCommand +from django.core.management.base import BaseCommand, CommandError from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey - +from openedx.core.djangoapps.content.course_overviews.models import SimulateCoursePublishConfig from lms.djangoapps.ccx.tasks import course_published_handler as ccx_receiver_fn from xmodule.modulestore.django import SignalHandler, modulestore @@ -42,10 +42,10 @@ class Command(BaseCommand): $ ./manage.py lms --settings=devstack_docker simulate_publish --delay 10 # Find all available listeners - $ ./manage.py lms --settings=devstack_docker simulate_publish --show_listeners + $ ./manage.py lms --settings=devstack_docker simulate_publish --show_receivers # Send the publish signal to two courses and two listeners - $ ./manage.py lms --settings=devstack_docker simulate_publish --listeners \ + $ ./manage.py lms --settings=devstack_docker simulate_publish --receivers \ openedx.core.djangoapps.content.course_overviews.signals._listen_for_course_publish \ openedx.core.djangoapps.bookmarks.signals.trigger_update_xblocks_cache_task \ --courses course-v1:edX+DemoX+Demo_Course edX/MODULESTORE_100/2018 @@ -157,8 +157,30 @@ class Command(BaseCommand): u"with this flag, so that CCX receivers are omitted." ) ), + parser.add_argument( + '--args-from-database', + action='store_true', + help='Use arguments from the SimulateCoursePublishConfig model instead of the command line.', + ), + + def get_args_from_database(self): + """ Returns an options dictionary from the current SimulateCoursePublishConfig model. """ + + config = SimulateCoursePublishConfig.current() + if not config.enabled: + raise CommandError('SimulateCourseConfigPublish 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', 'simulate_publish') + 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() + if options['show_receivers']: return self.print_show_receivers() diff --git a/openedx/core/djangoapps/content/course_overviews/management/commands/tests/test_simulate_publish.py b/openedx/core/djangoapps/content/course_overviews/management/commands/tests/test_simulate_publish.py index 134000e5f0..492f11166f 100644 --- a/openedx/core/djangoapps/content/course_overviews/management/commands/tests/test_simulate_publish.py +++ b/openedx/core/djangoapps/content/course_overviews/management/commands/tests/test_simulate_publish.py @@ -5,15 +5,21 @@ from __future__ import absolute_import import six +from django.core.management import call_command +from django.core.management.base import CommandError +from testfixtures import LogCapture + import lms.djangoapps.ccx.tasks import openedx.core.djangoapps.content.course_overviews.signals from openedx.core.djangoapps.content.course_overviews.management.commands.simulate_publish import Command, name_from_fn -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview, SimulateCoursePublishConfig from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import SwitchedSignal from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory +LOGGER_NAME = 'simulate_publish' + class TestSimulatePublish(SharedModuleStoreTestCase): """Test simulate_publish, our fake course-publish signal command.""" @@ -97,6 +103,7 @@ class TestSimulatePublish(SharedModuleStoreTestCase): delay=0, force_lms=False, skip_ccx=False, + args_from_database=False ) default_options.update(kwargs) return default_options @@ -147,3 +154,35 @@ class TestSimulatePublish(SharedModuleStoreTestCase): def sample_receiver_2(self, sender, course_key, **kwargs): # pylint: disable=unused-argument """Custom receiver for testing.""" self.received_2.append(course_key) + + def test_args_from_database(self): + """Test management command arguments injected from config model.""" + # Nothing in the database, should default to disabled + with self.assertRaisesRegex(CommandError, 'SimulateCourseConfigPublish is disabled.*'): + call_command('simulate_publish', '--args-from-database') + + # Add a config + config = SimulateCoursePublishConfig.current() + config.arguments = '--delay 20 --dry-run' + config.enabled = True + config.save() + + with LogCapture(LOGGER_NAME) as log: + call_command('simulate_publish') + + log.check_present( + ( + LOGGER_NAME, 'INFO', + u"simulate_publish starting, dry-run={}, delay={} seconds".format('False', '0') + ), + ) + + with LogCapture(LOGGER_NAME) as log: + call_command('simulate_publish', '--args-from-database') + + log.check_present( + ( + LOGGER_NAME, 'INFO', + u"simulate_publish starting, dry-run={}, delay={} seconds".format('True', '20') + ), + ) diff --git a/openedx/core/djangoapps/content/course_overviews/migrations/0016_simulatecoursepublishconfig.py b/openedx/core/djangoapps/content/course_overviews/migrations/0016_simulatecoursepublishconfig.py new file mode 100644 index 0000000000..af75c33d87 --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/migrations/0016_simulatecoursepublishconfig.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.23 on 2019-09-16 10:45 +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), + ('course_overviews', '0015_historicalcourseoverview'), + ] + + operations = [ + migrations.CreateModel( + name='SimulateCoursePublishConfig', + 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=b'', help_text=b'Useful for manually running a Jenkins job. Specify like "--delay 10 --listeners A B C --courses X Y Z".')), + ('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': 'simulate_publish argument', + }, + ), + ] diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index 624015010e..d8b05c8b7c 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -896,3 +896,25 @@ class CourseOverviewImageConfig(ConfigurationModel): return u"CourseOverviewImageConfig(enabled={}, small={}, large={})".format( self.enabled, self.small, self.large ) + + +class SimulateCoursePublishConfig(ConfigurationModel): + """ + Manages configuration for a run of the simulate_publish management command. + + .. no_pii: + """ + + class Meta(object): + app_label = 'course_overviews' + verbose_name = 'simulate_publish argument' + + arguments = models.TextField( + blank=True, + help_text='Useful for manually running a Jenkins job. Specify like "--delay 10 --receivers A B C \ + --courses X Y Z".', + default='', + ) + + def __unicode__(self): + return six.text_type(self.arguments)