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
52897d58
Commit
52897d58
authored
Oct 26, 2017
by
Tyler Hallada
Committed by
GitHub
Oct 26, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #16346 from edx/thallada/ret-org-opt-out
Org-level schedule upgrade deadline opt-out
parents
6b37218e
05dd63e8
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
208 additions
and
20 deletions
+208
-20
common/djangoapps/student/models.py
+11
-2
lms/djangoapps/courseware/admin.py
+1
-0
lms/djangoapps/courseware/migrations/0005_orgdynamicupgradedeadlineconfiguration.py
+39
-0
lms/djangoapps/courseware/models.py
+39
-4
lms/djangoapps/courseware/tests/test_date_summary.py
+89
-6
openedx/core/djangoapps/schedules/management/commands/tests/test_send_upgrade_reminder.py
+14
-6
openedx/core/djangoapps/schedules/signals.py
+15
-2
No files found.
common/djangoapps/student/models.py
View file @
52897d58
...
...
@@ -50,7 +50,11 @@ import request_cache
from
student.signals
import
UNENROLL_DONE
,
ENROLL_STATUS_CHANGE
,
REFUND_ORDER
,
ENROLLMENT_TRACK_UPDATED
from
certificates.models
import
GeneratedCertificate
from
course_modes.models
import
CourseMode
from
courseware.models
import
DynamicUpgradeDeadlineConfiguration
,
CourseDynamicUpgradeDeadlineConfiguration
from
courseware.models
import
(
CourseDynamicUpgradeDeadlineConfiguration
,
DynamicUpgradeDeadlineConfiguration
,
OrgDynamicUpgradeDeadlineConfiguration
)
from
enrollment.api
import
_default_course_mode
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
...
...
@@ -1740,7 +1744,12 @@ class CourseEnrollment(models.Model):
return
None
course_config
=
CourseDynamicUpgradeDeadlineConfiguration
.
current
(
self
.
course_id
)
if
course_config
.
enabled
and
course_config
.
opt_out
:
if
course_config
.
opted_out
():
# Course-level config should be checked first since it overrides the org-level config
return
None
org_config
=
OrgDynamicUpgradeDeadlineConfiguration
.
current
(
self
.
course_id
.
org
)
if
org_config
.
opted_out
()
and
not
course_config
.
opted_in
():
return
None
try
:
...
...
lms/djangoapps/courseware/admin.py
View file @
52897d58
...
...
@@ -8,4 +8,5 @@ admin.site.register(models.DynamicUpgradeDeadlineConfiguration, ConfigurationMod
admin
.
site
.
register
(
models
.
OfflineComputedGrade
)
admin
.
site
.
register
(
models
.
OfflineComputedGradeLog
)
admin
.
site
.
register
(
models
.
CourseDynamicUpgradeDeadlineConfiguration
,
KeyedConfigurationModelAdmin
)
admin
.
site
.
register
(
models
.
OrgDynamicUpgradeDeadlineConfiguration
,
KeyedConfigurationModelAdmin
)
admin
.
site
.
register
(
models
.
StudentModule
)
lms/djangoapps/courseware/migrations/0005_orgdynamicupgradedeadlineconfiguration.py
0 → 100644
View file @
52897d58
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
import
django.db.models.deletion
from
django.conf
import
settings
import
courseware.models
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
migrations
.
swappable_dependency
(
settings
.
AUTH_USER_MODEL
),
(
'courseware'
,
'0004_auto_20171010_1639'
),
]
operations
=
[
migrations
.
CreateModel
(
name
=
'OrgDynamicUpgradeDeadlineConfiguration'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
verbose_name
=
'ID'
,
serialize
=
False
,
auto_created
=
True
,
primary_key
=
True
)),
(
'change_date'
,
models
.
DateTimeField
(
auto_now_add
=
True
,
verbose_name
=
'Change date'
)),
(
'enabled'
,
models
.
BooleanField
(
default
=
False
,
verbose_name
=
'Enabled'
)),
(
'org_id'
,
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)),
(
'deadline_days'
,
models
.
PositiveSmallIntegerField
(
default
=
21
,
help_text
=
'Number of days a learner has to upgrade after content is made available'
)),
(
'opt_out'
,
models
.
BooleanField
(
default
=
False
,
help_text
=
'Disable the dynamic upgrade deadline for this organization.'
)),
(
'changed_by'
,
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
PROTECT
,
editable
=
False
,
to
=
settings
.
AUTH_USER_MODEL
,
null
=
True
,
verbose_name
=
'Changed by'
)),
],
options
=
{
'ordering'
:
(
'-change_date'
,),
'abstract'
:
False
,
},
bases
=
(
courseware
.
models
.
OptOutDynamicUpgradeDeadlineMixin
,
models
.
Model
),
),
migrations
.
AlterModelOptions
(
name
=
'coursedynamicupgradedeadlineconfiguration'
,
options
=
{
'ordering'
:
(
'-change_date'
,)},
),
]
lms/djangoapps/courseware/models.py
View file @
52897d58
...
...
@@ -379,24 +379,59 @@ class DynamicUpgradeDeadlineConfiguration(ConfigurationModel):
)
class
CourseDynamicUpgradeDeadlineConfiguration
(
ConfigurationModel
):
class
OptOutDynamicUpgradeDeadlineMixin
(
object
):
"""
Provides convenience methods for interpreting the enabled and opt out status.
"""
def
opted_in
(
self
):
"""Convenience function that returns True if this config model is both enabled and opt_out is False"""
return
self
.
enabled
and
not
self
.
opt_out
def
opted_out
(
self
):
"""Convenience function that returns True if this config model is both enabled and opt_out is True"""
return
self
.
enabled
and
self
.
opt_out
class
CourseDynamicUpgradeDeadlineConfiguration
(
OptOutDynamicUpgradeDeadlineMixin
,
ConfigurationModel
):
"""
Per-course run configuration for dynamic upgrade deadlines.
This model controls dynamic upgrade deadlines on a per-course run level, allowing course runs to
have different deadlines or opt out of the functionality altogether.
"""
class
Meta
(
object
):
app_label
=
'courseware'
KEY_FIELDS
=
(
'course_id'
,)
course_id
=
CourseKeyField
(
max_length
=
255
,
db_index
=
True
)
deadline_days
=
models
.
PositiveSmallIntegerField
(
default
=
21
,
help_text
=
_
(
'Number of days a learner has to upgrade after content is made available'
)
)
opt_out
=
models
.
BooleanField
(
default
=
False
,
help_text
=
_
(
'Disable the dynamic upgrade deadline for this course run.'
)
)
class
OrgDynamicUpgradeDeadlineConfiguration
(
OptOutDynamicUpgradeDeadlineMixin
,
ConfigurationModel
):
"""
Per-org configuration for dynamic upgrade deadlines.
This model controls dynamic upgrade deadlines on a per-org level, allowing organizations to
have different deadlines or opt out of the functionality altogether.
"""
KEY_FIELDS
=
(
'org_id'
,)
org_id
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)
deadline_days
=
models
.
PositiveSmallIntegerField
(
default
=
21
,
help_text
=
_
(
'Number of days a learner has to upgrade after content is made available'
)
)
opt_out
=
models
.
BooleanField
(
default
=
False
,
help_text
=
_
(
'Disable the dynamic upgrade deadline for this organization.'
)
)
lms/djangoapps/courseware/tests/test_date_summary.py
View file @
52897d58
...
...
@@ -24,10 +24,15 @@ from courseware.date_summary import (
VerifiedUpgradeDeadlineDate
,
CertificateAvailableDate
)
from
courseware.models
import
DynamicUpgradeDeadlineConfiguration
,
CourseDynamicUpgradeDeadlineConfiguration
from
courseware.models
import
(
CourseDynamicUpgradeDeadlineConfiguration
,
DynamicUpgradeDeadlineConfiguration
,
OrgDynamicUpgradeDeadlineConfiguration
)
from
lms.djangoapps.verify_student.models
import
VerificationDeadline
from
lms.djangoapps.verify_student.tests.factories
import
SoftwareSecurePhotoVerificationFactory
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.content.course_overviews.tests.factories
import
CourseOverviewFactory
from
openedx.core.djangoapps.schedules.signals
import
CREATE_SCHEDULE_WAFFLE_FLAG
from
openedx.core.djangoapps.self_paced.models
import
SelfPacedConfiguration
from
openedx.core.djangoapps.site_configuration.tests.factories
import
SiteFactory
...
...
@@ -562,6 +567,7 @@ class TestDateAlerts(SharedModuleStoreTestCase):
self
.
assertEqual
(
len
(
messages
),
0
)
@ddt.ddt
@attr
(
shard
=
1
)
class
TestScheduleOverrides
(
SharedModuleStoreTestCase
):
...
...
@@ -599,15 +605,28 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
@override_waffle_flag
(
CREATE_SCHEDULE_WAFFLE_FLAG
,
True
)
def
test_date_with_self_paced_with_enrollment_after_course_start
(
self
):
""" Enrolling after a course begins should result in the upgrade deadline being set relative to the
enrollment date. """
enrollment date.
Additionally, OrgDynamicUpgradeDeadlineConfiguration should override the number of days until the deadline,
and CourseDynamicUpgradeDeadlineConfiguration should override the org-level override.
"""
global_config
=
DynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
True
)
course
=
create_self_paced_course_run
(
days_till_start
=-
1
)
course
=
create_self_paced_course_run
(
days_till_start
=-
1
,
org_id
=
'TestOrg'
)
enrollment
=
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
mode
=
CourseMode
.
AUDIT
)
block
=
VerifiedUpgradeDeadlineDate
(
course
,
enrollment
.
user
)
expected
=
enrollment
.
created
+
timedelta
(
days
=
global_config
.
deadline_days
)
self
.
assertEqual
(
block
.
date
,
expected
)
# Courses should be able to override the deadline
# Orgs should be able to override the deadline
org_config
=
OrgDynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
True
,
org_id
=
course
.
org
,
deadline_days
=
4
)
enrollment
=
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
mode
=
CourseMode
.
AUDIT
)
block
=
VerifiedUpgradeDeadlineDate
(
course
,
enrollment
.
user
)
expected
=
enrollment
.
created
+
timedelta
(
days
=
org_config
.
deadline_days
)
self
.
assertEqual
(
block
.
date
,
expected
)
# Courses should be able to override the deadline (and the org-level override)
course_config
=
CourseDynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
True
,
course_id
=
course
.
id
,
deadline_days
=
3
)
...
...
@@ -650,6 +669,68 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
block
=
VerifiedUpgradeDeadlineDate
(
course
,
enrollment
.
user
)
self
.
assertEqual
(
block
.
date
,
expected
)
@ddt.data
(
# (enroll before configs, org enabled, org opt-out, course enabled, course opt-out, expected dynamic deadline)
(
False
,
False
,
False
,
False
,
False
,
True
),
(
False
,
False
,
False
,
False
,
True
,
True
),
(
False
,
False
,
False
,
True
,
False
,
True
),
(
False
,
False
,
False
,
True
,
True
,
False
),
(
False
,
False
,
True
,
False
,
False
,
True
),
(
False
,
False
,
True
,
False
,
True
,
True
),
(
False
,
False
,
True
,
True
,
False
,
True
),
(
False
,
False
,
True
,
True
,
True
,
False
),
(
False
,
True
,
False
,
False
,
False
,
True
),
(
False
,
True
,
False
,
False
,
True
,
True
),
(
False
,
True
,
False
,
True
,
False
,
True
),
(
False
,
True
,
False
,
True
,
True
,
False
),
# course-level overrides org-level
(
False
,
True
,
True
,
False
,
False
,
False
),
(
False
,
True
,
True
,
False
,
True
,
False
),
(
False
,
True
,
True
,
True
,
False
,
True
),
# course-level overrides org-level
(
False
,
True
,
True
,
True
,
True
,
False
),
(
True
,
False
,
False
,
False
,
False
,
True
),
(
True
,
False
,
False
,
False
,
True
,
True
),
(
True
,
False
,
False
,
True
,
False
,
True
),
(
True
,
False
,
False
,
True
,
True
,
False
),
(
True
,
False
,
True
,
False
,
False
,
True
),
(
True
,
False
,
True
,
False
,
True
,
True
),
(
True
,
False
,
True
,
True
,
False
,
True
),
(
True
,
False
,
True
,
True
,
True
,
False
),
(
True
,
True
,
False
,
False
,
False
,
True
),
(
True
,
True
,
False
,
False
,
True
,
True
),
(
True
,
True
,
False
,
True
,
False
,
True
),
(
True
,
True
,
False
,
True
,
True
,
False
),
# course-level overrides org-level
(
True
,
True
,
True
,
False
,
False
,
False
),
(
True
,
True
,
True
,
False
,
True
,
False
),
(
True
,
True
,
True
,
True
,
False
,
True
),
# course-level overrides org-level
(
True
,
True
,
True
,
True
,
True
,
False
),
)
@ddt.unpack
@override_waffle_flag
(
CREATE_SCHEDULE_WAFFLE_FLAG
,
True
)
def
test_date_with_org_and_course_config_overrides
(
self
,
enroll_first
,
org_config_enabled
,
org_config_opt_out
,
course_config_enabled
,
course_config_opt_out
,
expected_dynamic_deadline
):
""" Runs through every combination of org-level plus course-level DynamicUpgradeDeadlineConfiguration enabled
and opt-out states to verify that course-level overrides the org-level config. """
course
=
create_self_paced_course_run
(
days_till_start
=-
1
,
org_id
=
'TestOrg'
)
DynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
True
)
if
enroll_first
:
enrollment
=
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
mode
=
CourseMode
.
AUDIT
,
course__self_paced
=
True
)
OrgDynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
org_config_enabled
,
opt_out
=
org_config_opt_out
,
org_id
=
course
.
id
.
org
)
CourseDynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
course_config_enabled
,
opt_out
=
course_config_opt_out
,
course_id
=
course
.
id
)
if
not
enroll_first
:
enrollment
=
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
mode
=
CourseMode
.
AUDIT
,
course__self_paced
=
True
)
# The enrollment has a schedule, and the upgrade_deadline is set when expected_dynamic_deadline is True
if
not
enroll_first
:
self
.
assertEqual
(
enrollment
.
schedule
.
upgrade_deadline
is
not
None
,
expected_dynamic_deadline
)
# The CourseEnrollment.upgrade_deadline property method is checking the configs
self
.
assertEqual
(
enrollment
.
dynamic_upgrade_deadline
is
not
None
,
expected_dynamic_deadline
)
def
create_user
(
verification_status
=
None
):
""" Create a new User instance.
...
...
@@ -705,7 +786,7 @@ def create_course_run(
return
course
def
create_self_paced_course_run
(
days_till_start
=
1
):
def
create_self_paced_course_run
(
days_till_start
=
1
,
org_id
=
None
):
""" Create a new course run and course modes.
All date-related arguments are relative to the current date-time (now) unless otherwise specified.
...
...
@@ -714,9 +795,11 @@ def create_self_paced_course_run(days_till_start=1):
Arguments:
days_till_start (int): Number of days until the course starts.
org_id (string): String org id to assign the course to (default: None; use CourseFactory default)
"""
now
=
datetime
.
now
(
utc
)
course
=
CourseFactory
.
create
(
start
=
now
+
timedelta
(
days
=
days_till_start
),
self_paced
=
True
)
course
=
CourseFactory
.
create
(
start
=
now
+
timedelta
(
days
=
days_till_start
),
self_paced
=
True
,
org
=
org_id
if
org_id
else
'TestedX'
)
CourseModeFactory
(
course_id
=
course
.
id
,
...
...
openedx/core/djangoapps/schedules/management/commands/tests/test_send_upgrade_reminder.py
View file @
52897d58
...
...
@@ -139,6 +139,7 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
bins_in_use
=
frozenset
((
self
.
_calculate_bin_for_user
(
s
.
enrollment
.
user
))
for
s
in
schedules
)
is_first_match
=
True
course_switch_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
for
s
in
schedules
))
org_switch_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
.
org
for
s
in
schedules
))
test_datetime
=
upgrade_deadline
test_datetime_str
=
serialize
(
test_datetime
)
...
...
@@ -151,7 +152,7 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
# Since this is the first match, we need to cache all of the config models, so we run a query
# for each of those...
NUM_QUERIES_FIRST_MATCH
+
course_switch_queries
+
course_switch_queries
+
org_switch_queries
)
is_first_match
=
False
else
:
...
...
@@ -257,7 +258,8 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
test_datetime_str
=
serialize
(
test_datetime
)
course_switch_queries
=
1
expected_queries
=
NUM_QUERIES_FIRST_MATCH
+
course_switch_queries
org_switch_queries
=
1
expected_queries
=
NUM_QUERIES_FIRST_MATCH
+
course_switch_queries
+
org_switch_queries
if
not
this_org_list
:
expected_queries
+=
NUM_QUERIES_NO_ORG_LIST
...
...
@@ -283,11 +285,14 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
for
course_num
in
(
1
,
2
,
3
)
]
num_courses
=
len
(
set
(
s
.
enrollment
.
course
.
id
for
s
in
schedules
))
course_switch_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
for
s
in
schedules
))
org_switch_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
.
org
for
s
in
schedules
))
test_datetime
=
datetime
.
datetime
(
2017
,
8
,
3
,
19
,
44
,
30
,
tzinfo
=
pytz
.
UTC
)
test_datetime_str
=
serialize
(
test_datetime
)
expected_query_count
=
NUM_QUERIES_FIRST_MATCH
+
num_courses
+
NUM_QUERIES_NO_ORG_LIST
expected_query_count
=
(
NUM_QUERIES_FIRST_MATCH
+
course_switch_queries
+
org_switch_queries
+
NUM_QUERIES_NO_ORG_LIST
)
with
self
.
assertNumQueries
(
expected_query_count
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
ScheduleUpgradeReminder
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=
2
,
...
...
@@ -320,7 +325,8 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
expiration_datetime
=
future_datetime
)
num_courses
=
len
(
set
(
s
.
enrollment
.
course
.
id
for
s
in
schedules
))
course_switch_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
for
s
in
schedules
))
org_switch_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
.
org
for
s
in
schedules
))
test_datetime
=
future_datetime
test_datetime_str
=
serialize
(
test_datetime
)
...
...
@@ -339,7 +345,9 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
mock_schedule_send
.
apply_async
=
lambda
args
,
*
_a
,
**
_kw
:
sent_messages
.
append
(
args
)
# we execute one query per course to see if it's opted out of dynamic upgrade deadlines
num_expected_queries
=
NUM_QUERIES_FIRST_MATCH
+
NUM_QUERIES_NO_ORG_LIST
+
num_courses
num_expected_queries
=
(
NUM_QUERIES_FIRST_MATCH
+
NUM_QUERIES_NO_ORG_LIST
+
course_switch_queries
+
org_switch_queries
)
with
self
.
assertNumQueries
(
num_expected_queries
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
ScheduleUpgradeReminder
.
apply
(
kwargs
=
dict
(
...
...
openedx/core/djangoapps/schedules/signals.py
View file @
52897d58
...
...
@@ -6,7 +6,11 @@ from django.dispatch import receiver
from
django.utils
import
timezone
from
course_modes.models
import
CourseMode
from
courseware.models
import
DynamicUpgradeDeadlineConfiguration
,
CourseDynamicUpgradeDeadlineConfiguration
from
courseware.models
import
(
CourseDynamicUpgradeDeadlineConfiguration
,
DynamicUpgradeDeadlineConfiguration
,
OrgDynamicUpgradeDeadlineConfiguration
)
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
...
...
@@ -110,9 +114,18 @@ def _get_upgrade_deadline_delta_setting(course_id):
# Use the default from this model whether or not the feature is enabled
delta
=
global_config
.
deadline_days
# Check if the org has a deadline
org_config
=
OrgDynamicUpgradeDeadlineConfiguration
.
current
(
course_id
.
org
)
if
org_config
.
opted_in
():
delta
=
org_config
.
deadline_days
elif
org_config
.
opted_out
():
delta
=
None
# Check if the course has a deadline
course_config
=
CourseDynamicUpgradeDeadlineConfiguration
.
current
(
course_id
)
if
course_config
.
enabled
and
not
course_config
.
opt_out
:
if
course_config
.
opted_in
()
:
delta
=
course_config
.
deadline_days
elif
course_config
.
opted_out
():
delta
=
None
return
delta
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