Skip to content

Commit

Permalink
feat: allow disabling spaced out sections in self paced courses
Browse files Browse the repository at this point in the history
In self paced courses, if relative due dates are enabled via
SelfPacedRelativeDatesConfig, all graded content would be assigned
relative due dates which are evenly spaced out over an estimated
duration of a course (aka. Personal Learner Schedule or PLS). If
CUSTOM_RELATIVE_DATES are enabled, custom set relative due dates would
(sometimes) override the "spaced out" ones.

However, there are some usecases, when custom relative due dates are
desired, without the PLS. For this usecase we are adding a
DISABLE_SPACED_OUT_SECTIONS CourseWaffleFlag. None of the existing
behaviour is changed unwillingly. When the flag is enabled, the relative
due dates will only be applied to the subsections that have custom
relative due dates set, or when a similar setting is set in Advanced
Settings of a course.
  • Loading branch information
Cup0fCoffee committed Sep 2, 2024
1 parent 9ade4ea commit 3c5b534
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 22 deletions.
91 changes: 69 additions & 22 deletions openedx/core/djangoapps/course_date_signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from xmodule.modulestore.django import SignalHandler, modulestore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.util.misc import is_xblock_an_assignment # lint-amnesty, pylint: disable=wrong-import-order

from openedx.core.djangoapps.course_date_signals.waffle import DISABLE_SPACED_OUT_SECTIONS

from .models import SelfPacedRelativeDatesConfig
from .utils import spaced_out_sections

Expand Down Expand Up @@ -118,6 +120,63 @@ def _get_custom_pacing_children(subsection, num_weeks):
return section_date_items


def extract_dates_from_course_spaced_out_sections(course):
"""
Extract all dates from the supplied course. Apply PLS to subsections that
don't have custom relative_weeks_due set, by spacing them out evenly based
on the estimated course duration.
"""
date_items = []
# Apply the same relative due date to all content inside a section,
# unless that item already has a relative date set
for _, section, weeks_to_complete in spaced_out_sections(course):
section_date_items = []
# section_due_date will end up being the max of all due dates of its subsections
section_due_date = timedelta(weeks=1)
for subsection in section.get_children():
# If custom pacing is set on a subsection, apply the set relative
# date to all the content inside the subsection. Otherwise
# apply the default Personalized Learner Schedules (PLS)
# logic for self paced courses.
relative_weeks_due = subsection.fields['relative_weeks_due'].read_from(subsection)
if (CUSTOM_RELATIVE_DATES.is_enabled(course.id) and relative_weeks_due):
section_due_date = max(section_due_date, timedelta(weeks=relative_weeks_due))
section_date_items.extend(_get_custom_pacing_children(subsection, relative_weeks_due))
else:
section_due_date = max(section_due_date, weeks_to_complete)
section_date_items.extend(_gather_graded_items(subsection, weeks_to_complete))
if section_date_items and (section.graded or CUSTOM_RELATIVE_DATES.is_enabled(course.id)):
date_items.append((section.location, {'due': section_due_date}))
date_items.extend(section_date_items)
return date_items


def extract_dates_from_course_custom_dates_only(course):
"""
Extract all dates from the supplied course. Only considers subsections that
have relative_weeks_due set, either custom or through Advanced Settings.
"""
date_items = []
# Apply relative due date only to content inside a section,
# that already has a relative date set. Also inherits relative
# due date set in the advanced settings.
for section in course.get_children():
if section.visible_to_staff_only:
continue
section_date_items = []
for subsection in section.get_children():
# If custom pacing is set on a subsection, apply the set relative
# date to all the content inside the subsection.
relative_weeks_due = subsection.fields['relative_weeks_due'].read_from(subsection)
if relative_weeks_due:
section_due_date = timedelta(weeks=relative_weeks_due)
section_date_items.extend(_get_custom_pacing_children(subsection, relative_weeks_due))
if section_date_items:
date_items.append((section.location, {'due': section_due_date}))
date_items.extend(section_date_items)
return date_items


def extract_dates_from_course(course):
"""
Extract all dates from the supplied course.
Expand All @@ -129,28 +188,16 @@ def extract_dates_from_course(course):
metadata.pop('due', None)
date_items = [(course.location, metadata)]

if SelfPacedRelativeDatesConfig.current(course_key=course.id).enabled:
# Apply the same relative due date to all content inside a section,
# unless that item already has a relative date set
for _, section, weeks_to_complete in spaced_out_sections(course):
section_date_items = []
# section_due_date will end up being the max of all due dates of its subsections
section_due_date = timedelta(weeks=1)
for subsection in section.get_children():
# If custom pacing is set on a subsection, apply the set relative
# date to all the content inside the subsection. Otherwise
# apply the default Personalized Learner Schedules (PLS)
# logic for self paced courses.
relative_weeks_due = subsection.fields['relative_weeks_due'].read_from(subsection)
if (CUSTOM_RELATIVE_DATES.is_enabled(course.id) and relative_weeks_due):
section_due_date = max(section_due_date, timedelta(weeks=relative_weeks_due))
section_date_items.extend(_get_custom_pacing_children(subsection, relative_weeks_due))
else:
section_due_date = max(section_due_date, weeks_to_complete)
section_date_items.extend(_gather_graded_items(subsection, weeks_to_complete))
if section_date_items and (section.graded or CUSTOM_RELATIVE_DATES.is_enabled(course.id)):
date_items.append((section.location, {'due': section_due_date}))
date_items.extend(section_date_items)
self_paced_relative_dates_config = SelfPacedRelativeDatesConfig.current(course_key=course.id)
if self_paced_relative_dates_config.enabled:
if not DISABLE_SPACED_OUT_SECTIONS.is_enabled(course.id):
date_items.extend(
extract_dates_from_course_spaced_out_sections(course)
)
elif CUSTOM_RELATIVE_DATES.is_enabled(course.id):
date_items.extend(
extract_dates_from_course_custom_dates_only(course)
)
else:
date_items = []
store = modulestore()
Expand Down
57 changes: 57 additions & 0 deletions openedx/core/djangoapps/course_date_signals/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
extract_dates_from_course
)
from openedx.core.djangoapps.course_date_signals.models import SelfPacedRelativeDatesConfig
from openedx.core.djangoapps.course_date_signals.waffle import DISABLE_SPACED_OUT_SECTIONS

from . import utils

Expand Down Expand Up @@ -370,3 +371,59 @@ def test_extract_dates_from_course_no_subsections(self):
expected_dates = [(self.course.location, {})]
course = self.store.get_item(self.course.location)
self.assertCountEqual(extract_dates_from_course(course), expected_dates)

@override_waffle_flag(CUSTOM_RELATIVE_DATES, active=True)
@override_waffle_flag(DISABLE_SPACED_OUT_SECTIONS, active=True)
def test_extract_dates_from_course_spaced_out_sections_disabled(self):
"""
A section with a subsection that has relative_weeks_due and
a subsection without relative_weeks_due that has graded content.
With DISABLE_SPACED_OUT_SECTIONS active, PLS should not apply for the
subsections without relative_weeks_due, even if it's graded. In other
words, when DISABLE_SPACED_OUT_SECTIONS is active, only custom set
relative_weeks_due are applied.
"""
with self.store.bulk_operations(self.course.id):
sequential1 = BlockFactory.create(category='sequential', parent=self.chapter, relative_weeks_due=2)
vertical1 = BlockFactory.create(category='vertical', parent=sequential1)
problem1 = BlockFactory.create(category='problem', parent=vertical1)

chapter2 = BlockFactory.create(category='chapter', parent=self.course)
sequential2 = BlockFactory.create(category='sequential', parent=chapter2, graded=True)
vertical2 = BlockFactory.create(category='vertical', parent=sequential2)
problem2 = BlockFactory.create(category='problem', parent=vertical2)

expected_dates = [
(self.course.location, {}),
(self.chapter.location, {'due': timedelta(days=14)}),
(sequential1.location, {'due': timedelta(days=14)}),
(vertical1.location, {'due': timedelta(days=14)}),
(problem1.location, {'due': timedelta(days=14)}),
]
course = self.store.get_item(self.course.location)
self.assertCountEqual(extract_dates_from_course(course), expected_dates)

@override_waffle_flag(CUSTOM_RELATIVE_DATES, active=False)
@override_waffle_flag(DISABLE_SPACED_OUT_SECTIONS, active=True)
def test_extract_dates_from_course_spaced_out_sections_and_custom_dates_disabled(self):
"""
A section with a subsection that has relative_weeks_due and
a subsection without relative_weeks_due that has graded content.
With DISABLE_SPACED_OUT_SECTIONS active and CUSTOM_RELATIVE_DATES
disabled, PLS should not apply for the subsections with relative_weeks_due.
"""
with self.store.bulk_operations(self.course.id):
sequential1 = BlockFactory.create(category='sequential', parent=self.chapter, relative_weeks_due=2)
vertical1 = BlockFactory.create(category='vertical', parent=sequential1)
problem1 = BlockFactory.create(category='problem', parent=vertical1)

chapter2 = BlockFactory.create(category='chapter', parent=self.course)
sequential2 = BlockFactory.create(category='sequential', parent=chapter2, graded=True)
vertical2 = BlockFactory.create(category='vertical', parent=sequential2)
problem2 = BlockFactory.create(category='problem', parent=vertical2)

expected_dates = [
(self.course.location, {}),
]
course = self.store.get_item(self.course.location)
self.assertCountEqual(extract_dates_from_course(course), expected_dates)
19 changes: 19 additions & 0 deletions openedx/core/djangoapps/course_date_signals/waffle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
This module contains various configuration settings via waffle switches for
course date signals.
"""

from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag

WAFFLE_FLAG_NAMESPACE = "course_date_signals"

# .. toggle_name: course_date_signals.disable_spaced_out_sections
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Waffle flag to disable spaced out sections for self paced courses.
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2024-09-02
# .. toggle_target_removal_date: None
DISABLE_SPACED_OUT_SECTIONS = CourseWaffleFlag(
f"{WAFFLE_FLAG_NAMESPACE}.disable_spaced_out_sections", __name__
)

0 comments on commit 3c5b534

Please sign in to comment.