Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow disabling spaced out sections in self paced courses #678

Open
wants to merge 1 commit into
base: opencraft-release/palm.1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
32 changes: 32 additions & 0 deletions openedx/core/djangoapps/course_date_signals/waffle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
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.relative_dates_disable_suggested_schedule
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Waffle flag to disable suggested schedule for self paced courses.
# When suggested schedule is enabled, graded content in self paced courses
# will be assigned a suggested relative due date. Suggested relative due dates
# are calculated by getting an average time needed per section, by getting an
# estimated duration of a course and dividing it by a number of sections,
# and then multiplying it by an index of a section that is currently being
# assigned a due date. E.g. if a course is estimated to be 4 weeks, has 4
# sections, and each one is marked as graded, the first section's relative due
# date is going to be one week from the date of the enrollment, the second -
# two weeks, etc.
# The estimated course duration is fetched from the Course Discovery service,
# and is clamped between 4 and 18 weeks. If Course Discovery is not available
# or value is not set for a course that is being requested, the estimated time
# would be set to 4 weeks.
# .. 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}.relative_dates_disable_suggested_schedule", __name__
)
Loading