Commit 51936499 by Nimisha Asthagiri

Update Schedules when start date changes on non-live courses

parent 05437483
......@@ -5,12 +5,12 @@ and disabling for instructor-paced courses.
import mock
from certificates import api as certs_api
from certificates.models import \
CertificateGenerationConfiguration, \
CertificateWhitelist, \
GeneratedCertificate, \
CertificateStatuses
from openedx.core.djangoapps.signals.handlers import _listen_for_course_pacing_changed
from certificates.models import (
CertificateGenerationConfiguration,
CertificateWhitelist,
GeneratedCertificate,
CertificateStatuses,
)
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from lms.djangoapps.grades.tests.utils import mock_passing_grade
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
......@@ -25,12 +25,11 @@ class SelfGeneratedCertsSignalTest(ModuleStoreTestCase):
"""
Tests for enabling/disabling self-generated certificates according to course-pacing.
"""
ENABLED_SIGNALS = ['course_published']
def setUp(self):
super(SelfGeneratedCertsSignalTest, self).setUp()
SelfPacedConfiguration(enabled=True).save()
self.course = CourseFactory.create(self_paced=True)
# Enable the feature
CertificateGenerationConfiguration.objects.create(enabled=True)
def test_cert_generation_flag_on_pacing_toggle(self):
......@@ -38,18 +37,15 @@ class SelfGeneratedCertsSignalTest(ModuleStoreTestCase):
Verify that signal enables or disables self-generated certificates
according to course-pacing.
"""
#self-generation of cert disables by default
self.course = CourseFactory.create(self_paced=False, emit_signals=True)
self.assertFalse(certs_api.cert_generation_enabled(self.course.id))
_listen_for_course_pacing_changed('store', self.course.id, self.course.self_paced)
#verify that self-generation of cert is enabled for self-paced course
self.course.self_paced = True
self.store.update_item(self.course, self.user.id)
self.assertTrue(certs_api.cert_generation_enabled(self.course.id))
self.course.self_paced = False
self.store.update_item(self.course, self.user.id)
_listen_for_course_pacing_changed('store', self.course.id, self.course.self_paced)
# verify that self-generation of cert is disabled for instructor-paced course
self.assertFalse(certs_api.cert_generation_enabled(self.course.id))
......
......@@ -12,8 +12,8 @@ from django.conf import settings
from django.core.urlresolvers import reverse
from django.test.client import Client, RequestFactory
from django.test.utils import override_settings
from freezegun import freeze_time
from util.date_utils import strftime_localized
from django.utils.translation import ugettext as _
from mock import patch
from nose.plugins.attrib import attr
......@@ -795,6 +795,7 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
self.assertIn('course_title_0', response.content)
self.assertIn('Signatory_Title 0', response.content)
@freeze_time('2017-09-10 00:00:00Z')
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
@ddt.data(
(datetime.datetime.now() - datetime.timedelta(days=1), True),
......
......@@ -4,6 +4,7 @@ Signal handler for invalidating cached course overviews
from django.dispatch.dispatcher import receiver
from .models import CourseOverview
from openedx.core.djangoapps.signals.signals import COURSE_PACING_CHANGED, COURSE_START_DATE_CHANGED
from xmodule.modulestore.django import SignalHandler
......@@ -13,7 +14,9 @@ def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable
Catches the signal that a course has been published in Studio and
updates the corresponding CourseOverview cache entry.
"""
CourseOverview.load_from_module_store(course_key)
previous_course_overview = CourseOverview.get_from_ids_if_exists([course_key]).get(course_key)
updated_course_overview = CourseOverview.load_from_module_store(course_key)
_check_for_course_changes(previous_course_overview, updated_course_overview)
@receiver(SignalHandler.course_deleted)
......@@ -27,3 +30,27 @@ def _listen_for_course_delete(sender, course_key, **kwargs): # pylint: disable=
from cms.djangoapps.contentstore.courseware_index import CourseAboutSearchIndexer
# Delete course entry from Course About Search_index
CourseAboutSearchIndexer.remove_deleted_items(course_key)
def _check_for_course_changes(previous_course_overview, updated_course_overview):
if previous_course_overview:
_check_for_course_date_changes(previous_course_overview, updated_course_overview)
_check_for_pacing_changes(previous_course_overview, updated_course_overview)
def _check_for_course_date_changes(previous_course_overview, updated_course_overview):
if previous_course_overview.start != updated_course_overview.start:
COURSE_START_DATE_CHANGED.send(
sender=None,
updated_course_overview=updated_course_overview,
previous_start_date=previous_course_overview.start,
)
def _check_for_pacing_changes(previous_course_overview, updated_course_overview):
if previous_course_overview.self_paced != updated_course_overview.self_paced:
COURSE_PACING_CHANGED.send(
sender=None,
updated_course_overview=updated_course_overview,
previous_self_paced=previous_course_overview.self_paced,
)
......@@ -33,7 +33,7 @@ from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls, check_mongo_calls_range
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls_range
from ..models import CourseOverview, CourseOverviewImageSet, CourseOverviewImageConfig
......@@ -270,56 +270,6 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
course = CourseFactory.create(default_store=modulestore_type, run="TestRun", **kwargs)
self.check_course_overview_against_course(course)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_overview_cache_invalidation(self, modulestore_type):
"""
Tests that when a course is published or deleted, the corresponding
course_overview is removed from the cache.
Arguments:
modulestore_type (ModuleStoreEnum.Type): type of store to create the
course in.
"""
with self.store.default_store(modulestore_type):
# Create a course where mobile_available is True.
course = CourseFactory.create(mobile_available=True, default_store=modulestore_type)
course_overview_1 = CourseOverview.get_from_id(course.id)
self.assertTrue(course_overview_1.mobile_available)
# Set mobile_available to False and update the course.
# This fires a course_published signal, which should be caught in signals.py, which should in turn
# delete the corresponding CourseOverview from the cache.
course.mobile_available = False
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
self.store.update_item(course, ModuleStoreEnum.UserID.test)
# Make sure that when we load the CourseOverview again, mobile_available is updated.
course_overview_2 = CourseOverview.get_from_id(course.id)
self.assertFalse(course_overview_2.mobile_available)
# Verify that when the course is deleted, the corresponding CourseOverview is deleted as well.
with self.assertRaises(CourseOverview.DoesNotExist):
self.store.delete_course(course.id, ModuleStoreEnum.UserID.test)
CourseOverview.get_from_id(course.id)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_overview_caching(self, modulestore_type):
"""
Tests that CourseOverview structures are actually getting cached.
Arguments:
modulestore_type (ModuleStoreEnum.Type): type of store to create the
course in.
"""
# Creating a new course will trigger a publish event and the course will be cached
course = CourseFactory.create(default_store=modulestore_type, emit_signals=True)
# The cache will be hit and mongo will not be queried
with check_mongo_calls(0):
CourseOverview.get_from_id(course.id)
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
def test_get_non_existent_course(self, modulestore_type):
"""
......
import datetime
import ddt
from mock import patch
from nose.plugins.attrib import attr
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
from ..models import CourseOverview
@ddt.ddt
@attr(shard=3)
class CourseOverviewSignalsTestCase(ModuleStoreTestCase):
"""
Tests for CourseOverview signals.
"""
ENABLED_SIGNALS = ['course_deleted', 'course_published']
TODAY = datetime.datetime.utcnow()
NEXT_WEEK = TODAY + datetime.timedelta(days=7)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_caching(self, modulestore_type):
"""
Tests that CourseOverview structures are actually getting cached.
Arguments:
modulestore_type (ModuleStoreEnum.Type): type of store to create the
course in.
"""
# Creating a new course will trigger a publish event and the course will be cached
course = CourseFactory.create(default_store=modulestore_type, emit_signals=True)
# The cache will be hit and mongo will not be queried
with check_mongo_calls(0):
CourseOverview.get_from_id(course.id)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_cache_invalidation(self, modulestore_type):
"""
Tests that when a course is published or deleted, the corresponding
course_overview is removed from the cache.
Arguments:
modulestore_type (ModuleStoreEnum.Type): type of store to create the
course in.
"""
with self.store.default_store(modulestore_type):
# Create a course where mobile_available is True.
course = CourseFactory.create(mobile_available=True, default_store=modulestore_type)
course_overview_1 = CourseOverview.get_from_id(course.id)
self.assertTrue(course_overview_1.mobile_available)
# Set mobile_available to False and update the course.
# This fires a course_published signal, which should be caught in signals.py, which should in turn
# delete the corresponding CourseOverview from the cache.
course.mobile_available = False
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
self.store.update_item(course, ModuleStoreEnum.UserID.test)
# Make sure that when we load the CourseOverview again, mobile_available is updated.
course_overview_2 = CourseOverview.get_from_id(course.id)
self.assertFalse(course_overview_2.mobile_available)
# Verify that when the course is deleted, the corresponding CourseOverview is deleted as well.
with self.assertRaises(CourseOverview.DoesNotExist):
self.store.delete_course(course.id, ModuleStoreEnum.UserID.test)
CourseOverview.get_from_id(course.id)
def assert_changed_signal_sent(self, field_name, initial_value, changed_value, mock_signal):
course = CourseFactory.create(emit_signals=True, **{field_name: initial_value})
# changing display name doesn't fire the signal
course.display_name = course.display_name + u'changed'
self.store.update_item(course, ModuleStoreEnum.UserID.test)
self.assertFalse(mock_signal.called)
# changing the given field fires the signal
setattr(course, field_name, changed_value)
self.store.update_item(course, ModuleStoreEnum.UserID.test)
self.assertTrue(mock_signal.called)
@patch('openedx.core.djangoapps.signals.signals.COURSE_START_DATE_CHANGED.send')
def test_start_changed(self, mock_signal):
self.assert_changed_signal_sent('start', self.TODAY, self.NEXT_WEEK, mock_signal)
@patch('openedx.core.djangoapps.signals.signals.COURSE_PACING_CHANGED.send')
def test_pacing_changed(self, mock_signal):
self.assert_changed_signal_sent('self_paced', True, False, mock_signal)
......@@ -9,7 +9,6 @@ from django.conf import settings
from xmodule.fields import Date
from xmodule.modulestore.exceptions import ItemNotFoundError
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.signals.signals import COURSE_PACING_CHANGE
from openedx.core.lib.courses import course_image_url
from xmodule.modulestore.django import modulestore
......@@ -190,7 +189,6 @@ class CourseDetails(object):
descriptor = module_store.get_course(course_key)
dirty = False
is_pacing_changed = False
# In the descriptor's setter, the date is converted to JSON
# using Date's to_json method. Calling to_json on something that
......@@ -283,15 +281,10 @@ class CourseDetails(object):
and jsondict['self_paced'] != descriptor.self_paced):
descriptor.self_paced = jsondict['self_paced']
dirty = True
is_pacing_changed = True
if dirty:
module_store.update_item(descriptor, user.id)
# fires a signal indicating that the course pacing has changed
if is_pacing_changed:
COURSE_PACING_CHANGE.send(sender=None, course_key=course_key, course_self_paced=descriptor.self_paced)
# NOTE: below auto writes to the db w/o verifying that any of
# the fields actually changed to make faster, could compare
# against db or could have client send over a list of which
......
from collections import namedtuple
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django_extensions.db.models import TimeStampedModel
......
......@@ -3,13 +3,18 @@ import logging
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from course_modes.models import CourseMode
from courseware.models import DynamicUpgradeDeadlineConfiguration, CourseDynamicUpgradeDeadlineConfiguration
from edx_ace.utils import date
from openedx.core.djangoapps.signals.signals import COURSE_START_DATE_CHANGED
from openedx.core.djangoapps.theming.helpers import get_current_site
from openedx.core.djangoapps.waffle_utils import WaffleFlagNamespace, CourseWaffleFlag
from student.models import CourseEnrollment
from .models import Schedule, ScheduleConfig
from .tasks import update_course_schedules
log = logging.getLogger(__name__)
......@@ -45,39 +50,11 @@ def create_schedule(sender, **kwargs):
log.debug('Schedules: Creation only enabled for self-paced courses')
return
delta = None
global_config = DynamicUpgradeDeadlineConfiguration.current()
if global_config.enabled:
# Use the default from this model whether or not the feature is enabled
delta = global_config.deadline_days
# Check if the course has a deadline override
course_config = CourseDynamicUpgradeDeadlineConfiguration.current(enrollment.course_id)
if course_config.enabled:
delta = course_config.deadline_days
upgrade_deadline = None
# This represents the first date at which the learner can access the content. This will be the latter of
# either the enrollment date or the course's start date.
content_availability_date = max(enrollment.created, enrollment.course_overview.start)
if delta is not None:
upgrade_deadline = content_availability_date + datetime.timedelta(days=delta)
course_upgrade_deadline = None
try:
verified_mode = CourseMode.verified_mode_for_course(enrollment.course_id)
except CourseMode.DoesNotExist:
pass
else:
if verified_mode:
course_upgrade_deadline = verified_mode.expiration_datetime
if course_upgrade_deadline is not None and upgrade_deadline is not None:
# The content availability-based deadline should never occur after the verified mode's
# expiration date, if one is set.
upgrade_deadline = min(upgrade_deadline, course_upgrade_deadline)
upgrade_deadline = _calculate_upgrade_deadline(enrollment.course_id, content_availability_date)
Schedule.objects.create(
enrollment=enrollment,
......@@ -87,3 +64,60 @@ def create_schedule(sender, **kwargs):
log.debug('Schedules: created a new schedule starting at %s with an upgrade deadline of %s',
content_availability_date, upgrade_deadline)
@receiver(COURSE_START_DATE_CHANGED, dispatch_uid="update_schedules_on_course_start_changed")
def update_schedules_on_course_start_changed(sender, updated_course_overview, previous_start_date, **kwargs):
"""
Updates all course schedules if course hasn't started yet.
"""
if previous_start_date > timezone.now():
upgrade_deadline = _calculate_upgrade_deadline(
updated_course_overview.id,
content_availability_date=updated_course_overview.start,
)
update_course_schedules.apply_async(
kwargs=dict(
course_id=unicode(updated_course_overview.id),
new_start_date_str=date.serialize(updated_course_overview.start),
new_upgrade_deadline_str=date.serialize(upgrade_deadline),
),
)
def _calculate_upgrade_deadline(course_id, content_availability_date):
upgrade_deadline = None
delta = _get_upgrade_deadline_delta_setting(course_id)
if delta is not None:
upgrade_deadline = content_availability_date + datetime.timedelta(days=delta)
if upgrade_deadline is not None:
# The content availability-based deadline should never occur
# after the verified mode's expiration date, if one is set.
try:
verified_mode = CourseMode.verified_mode_for_course(course_id)
except CourseMode.DoesNotExist:
pass
else:
if verified_mode:
course_mode_upgrade_deadline = verified_mode.expiration_datetime
if course_mode_upgrade_deadline is not None:
upgrade_deadline = min(upgrade_deadline, course_mode_upgrade_deadline)
return upgrade_deadline
def _get_upgrade_deadline_delta_setting(course_id):
delta = None
global_config = DynamicUpgradeDeadlineConfiguration.current()
if global_config.enabled:
# Use the default from this model whether or not the feature is enabled
delta = global_config.deadline_days
# Check if the course has a deadline
course_config = CourseDynamicUpgradeDeadlineConfiguration.current(course_id)
if course_config.enabled:
delta = course_config.deadline_days
return delta
......@@ -5,19 +5,47 @@ from urlparse import urlparse
from celery.task import task
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db.utils import DatabaseError
from django.utils.http import urlquote
from logging import getLogger
from edx_ace import ace
from edx_ace.message import MessageType, Message
from edx_ace.recipient import Recipient
from edx_ace.utils.date import deserialize
from edxmako.shortcuts import marketing_link
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.schedules.models import Schedule, ScheduleConfig
log = getLogger(__name__)
ROUTING_KEY = getattr(settings, 'ACE_ROUTING_KEY', None)
KNOWN_RETRY_ERRORS = ( # Errors we expect occasionally that could resolve on retry
DatabaseError,
ValidationError,
)
@task(bind=True, default_retry_delay=30, routing_key=ROUTING_KEY)
def update_course_schedules(self, **kwargs):
course_key = CourseKey.from_string(kwargs['course_id'])
new_start_date = deserialize(kwargs['new_start_date_str'])
new_upgrade_deadline = deserialize(kwargs['new_upgrade_deadline_str'])
try:
Schedule.objects.filter(enrollment__course_id=course_key).update(
start=new_start_date,
upgrade_deadline=new_upgrade_deadline
)
except Exception as exc: # pylint: disable=broad-except
if not isinstance(exc, KNOWN_RETRY_ERRORS):
log.exception("Unexpected failure: task id: %s, kwargs=%s".format(self.request.id, kwargs))
raise self.retry(kwargs=kwargs, exc=exc)
class RecurringNudge(MessageType):
......
......@@ -9,7 +9,9 @@ from openedx.core.djangoapps.schedules.signals import SCHEDULE_WAFFLE_FLAG
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.models import CourseEnrollment
from student.tests.factories import CourseEnrollmentFactory
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from ..models import Schedule
......@@ -21,13 +23,13 @@ from ..tests.factories import ScheduleConfigFactory
class CreateScheduleTests(SharedModuleStoreTestCase):
def assert_schedule_created(self):
course = create_course_run(self_paced=True)
course = _create_course_run(self_paced=True)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
self.assertIsNotNone(enrollment.schedule)
self.assertIsNone(enrollment.schedule.upgrade_deadline)
def assert_schedule_not_created(self):
course = create_course_run(self_paced=True)
course = _create_course_run(self_paced=True)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
with self.assertRaises(Schedule.DoesNotExist):
enrollment.schedule
......@@ -70,19 +72,64 @@ class CreateScheduleTests(SharedModuleStoreTestCase):
site = SiteFactory.create()
mock_get_current_site.return_value = site
ScheduleConfigFactory.create(site=site, enabled=True, create_schedules=True)
course = create_course_run(self_paced=False)
course = _create_course_run(self_paced=False)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
with self.assertRaises(Schedule.DoesNotExist):
enrollment.schedule
def create_course_run(self_paced=True):
@skip_unless_lms
class UpdateScheduleTests(SharedModuleStoreTestCase):
ENABLED_SIGNALS = ['course_published']
VERIFICATION_DEADLINE_DAYS = 14
def setUp(self):
super(UpdateScheduleTests, self).setUp()
self.site = SiteFactory.create()
ScheduleConfigFactory.create(site=self.site)
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True, deadline_days=self.VERIFICATION_DEADLINE_DAYS)
def assert_schedule_dates(self, schedule, expected_start):
self.assertEquals(_strip_secs(schedule.start), _strip_secs(expected_start))
self.assertEquals(
_strip_secs(schedule.upgrade_deadline),
_strip_secs(expected_start) + datetime.timedelta(days=self.VERIFICATION_DEADLINE_DAYS),
)
@patch('openedx.core.djangoapps.schedules.signals.get_current_site')
def test_schedule_updated(self, mock_get_current_site):
mock_get_current_site.return_value = self.site
course = _create_course_run(self_paced=True, start_day_offset=5)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
self.assert_schedule_dates(enrollment.schedule, enrollment.course_overview.start)
course.start = course.start + datetime.timedelta(days=3)
self.store.update_item(course, ModuleStoreEnum.UserID.test)
enrollment = CourseEnrollment.objects.get(id=enrollment.id)
self.assert_schedule_dates(enrollment.schedule, course.start)
@patch('openedx.core.djangoapps.schedules.signals.get_current_site')
def test_schedule_not_updated(self, mock_get_current_site):
mock_get_current_site.return_value = self.site
course = _create_course_run(self_paced=True, start_day_offset=-5)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
self.assert_schedule_dates(enrollment.schedule, enrollment.created)
course.start = course.start + datetime.timedelta(days=3)
self.store.update_item(course, ModuleStoreEnum.UserID.test)
self.assert_schedule_dates(enrollment.schedule, enrollment.created)
def _create_course_run(self_paced=True, start_day_offset=-1):
""" Create a new course run and course modes.
Both audit and verified `CourseMode` objects will be created for the course run.
"""
now = datetime.datetime.now(utc)
course = CourseFactory.create(start=now + datetime.timedelta(days=-1), self_paced=self_paced)
start = now + datetime.timedelta(days=start_day_offset)
course = CourseFactory.create(start=start, self_paced=self_paced)
CourseModeFactory(
course_id=course.id,
......@@ -95,3 +142,7 @@ def create_course_run(self_paced=True):
)
return course
def _strip_secs(timestamp):
return timestamp.replace(second=0, microsecond=0)
......@@ -3,32 +3,28 @@ This module contains all general use or cross-use handlers.
"""
import logging
from celery.task import task
from django.dispatch import receiver
from certificates.models import CertificateGenerationCourseSetting
from opaque_keys.edx.keys import CourseKey
from signals import COURSE_PACING_CHANGE
from signals import COURSE_PACING_CHANGED
log = logging.getLogger(__name__)
@receiver(COURSE_PACING_CHANGE, dispatch_uid="course_pacing_changed")
def _listen_for_course_pacing_changed(sender, course_key, course_self_paced, **kwargs): # pylint: disable=unused-argument
@receiver(COURSE_PACING_CHANGED, dispatch_uid="update_cert_settings_on_pacing_change")
def _update_cert_settings_on_pacing_change(sender, updated_course_overview, **kwargs): # pylint: disable=unused-argument
"""
Catches the signal that course pacing has changed and enable/disable
the self-generated certificates according to course-pacing.
"""
toggle_self_generated_certs.delay(unicode(course_key), course_self_paced)
log.info(u'Certificate Generation Setting Toggled for {course} via pacing change'.format(
course=course_key
toggle_self_generated_certs(updated_course_overview.id, updated_course_overview.self_paced)
log.info(u'Certificate Generation Setting Toggled for {course_id} via pacing change'.format(
course_id=updated_course_overview.id
))
@task
def toggle_self_generated_certs(course_key, course_self_paced):
"""
Enable or disable self-generated certificates for a course according to pacing.
"""
course_key = CourseKey.from_string(course_key)
CertificateGenerationCourseSetting.set_enabled_for_course(course_key, course_self_paced)
......@@ -23,4 +23,6 @@ COURSE_GRADE_NOW_PASSED = Signal(
# Signal that indicates that a user has become verified
LEARNER_NOW_VERIFIED = Signal(providing_args=['user'])
COURSE_PACING_CHANGE = Signal(providing_args=["course_key", "course_self_paced"])
COURSE_PACING_CHANGED = Signal(providing_args=["updated_course_overview", "previous_self_paced"])
COURSE_START_DATE_CHANGED = Signal(providing_args=["updated_course_overview", "previous_start_date"])
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment