Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
51936499
Commit
51936499
authored
Sep 07, 2017
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update Schedules when start date changes on non-live courses
parent
05437483
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
289 additions
and
122 deletions
+289
-122
lms/djangoapps/certificates/tests/test_signals.py
+10
-14
lms/djangoapps/certificates/tests/test_webview_views.py
+2
-1
openedx/core/djangoapps/content/course_overviews/signals.py
+28
-1
openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py
+1
-51
openedx/core/djangoapps/content/course_overviews/tests/test_signals.py
+91
-0
openedx/core/djangoapps/models/course_details.py
+0
-7
openedx/core/djangoapps/schedules/models.py
+0
-2
openedx/core/djangoapps/schedules/signals.py
+63
-29
openedx/core/djangoapps/schedules/tasks.py
+29
-1
openedx/core/djangoapps/schedules/tests/test_signals.py
+56
-5
openedx/core/djangoapps/signals/handlers.py
+6
-10
openedx/core/djangoapps/signals/signals.py
+3
-1
No files found.
lms/djangoapps/certificates/tests/test_signals.py
View file @
51936499
...
...
@@ -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
))
...
...
lms/djangoapps/certificates/tests/test_webview_views.py
View file @
51936499
...
...
@@ -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
),
...
...
openedx/core/djangoapps/content/course_overviews/signals.py
View file @
51936499
...
...
@@ -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
,
)
openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py
View file @
51936499
...
...
@@ -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
):
"""
...
...
openedx/core/djangoapps/content/course_overviews/tests/test_signals.py
0 → 100644
View file @
51936499
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
)
openedx/core/djangoapps/models/course_details.py
View file @
51936499
...
...
@@ -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
...
...
openedx/core/djangoapps/schedules/models.py
View file @
51936499
from
collections
import
namedtuple
from
django.db
import
models
from
django.utils.translation
import
ugettext_lazy
as
_
from
django_extensions.db.models
import
TimeStampedModel
...
...
openedx/core/djangoapps/schedules/signals.py
View file @
51936499
...
...
@@ -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
openedx/core/djangoapps/schedules/tasks.py
View file @
51936499
...
...
@@ -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
):
...
...
openedx/core/djangoapps/schedules/tests/test_signals.py
View file @
51936499
...
...
@@ -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
)
openedx/core/djangoapps/signals/handlers.py
View file @
51936499
...
...
@@ -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_CHANGE
D
,
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
)
openedx/core/djangoapps/signals/signals.py
View file @
51936499
...
...
@@ -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"
])
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment