Skip to content
Snippets Groups Projects
Unverified Commit fe2ca00c authored by Michael Terry's avatar Michael Terry Committed by GitHub
Browse files

Merge pull request #23978 from edx/mikix/highlights-dont-assume-due-dates

Don't assume due dates on sections
parents 4bdb8c56 3f2b2da2
No related branches found
Tags release-2021-03-25-11.22
No related merge requests found
......@@ -9,7 +9,7 @@ from xblock.fields import Scope
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import SignalHandler, modulestore
from .models import SelfPacedRelativeDatesConfig
from .utils import get_expected_duration
from .utils import spaced_out_sections
from edx_when.api import FIELDS_TO_EXTRACT, set_dates_for_course
log = logging.getLogger(__name__)
......@@ -49,19 +49,16 @@ def extract_dates_from_course(course):
date_items = [(course.location, metadata)]
if SelfPacedRelativeDatesConfig.current(course_key=course.id).enabled:
duration = get_expected_duration(course)
sections = course.get_children()
time_per_week = duration / len(sections)
# Apply the same relative due date to all content inside a section,
# unless that item already has a relative date set
for idx, section in enumerate(sections):
for _, section, weeks_to_complete in spaced_out_sections(course):
items = [section]
while items:
next_item = items.pop()
if next_item.graded:
# TODO: Once studio can manually set relative dates,
# we would need to manually check for them here
date_items.append((next_item.location, {'due': time_per_week * (idx + 1)}))
date_items.append((next_item.location, {'due': weeks_to_complete}))
items.extend(next_item.get_children())
else:
date_items = []
......
......@@ -6,7 +6,6 @@ get_expected_duration: return the expected duration of a course (absent any user
from datetime import timedelta
from course_modes.models import CourseMode
from openedx.core.djangoapps.catalog.utils import get_course_run_details
......@@ -21,11 +20,6 @@ def get_expected_duration(course):
access_duration = MIN_DURATION
verified_mode = CourseMode.verified_mode_for_course(course=course, include_expired=True)
if not verified_mode:
return None
# The user course expiration date is the content availability date
# plus the weeks_to_complete field from course-discovery.
discovery_course_details = get_course_run_details(course.id, ['weeks_to_complete'])
......@@ -37,3 +31,19 @@ def get_expected_duration(course):
access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration))
return access_duration
def spaced_out_sections(course):
"""
Generator that returns sections of the course module with a suggested time to complete for each
Returns:
index (int): index of section
section (block): a section block of the course
relative time (timedelta): the amount of weeks to complete the section, since start of course
"""
duration = get_expected_duration(course)
sections = course.get_children()
weeks_per_section = duration / len(sections)
for idx, section in enumerate(sections):
yield idx, section, weeks_per_section * (idx + 1)
......@@ -6,8 +6,7 @@ schedule experience built on the Schedules app.
import logging
from datetime import datetime, timedelta
from openedx.core.djangoapps.course_date_signals.utils import spaced_out_sections
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
from openedx.core.djangoapps.schedules.exceptions import CourseUpdateDoesNotExist
from openedx.core.lib.request_utils import get_request_or_stub
......@@ -64,7 +63,7 @@ def get_week_highlights(user, course_key, week_num):
return highlights
def get_next_section_highlights(user, course_key, target_date):
def get_next_section_highlights(user, course_key, start_date, target_date):
"""
Get highlights (list of unicode strings) for a week, based upon the current date.
......@@ -75,8 +74,9 @@ def get_next_section_highlights(user, course_key, target_date):
course_module = _get_course_module(course_descriptor, user)
sections_with_highlights = _get_sections_with_highlights(course_module)
highlights = _get_highlights_for_next_section(
course_module,
sections_with_highlights,
course_key,
start_date,
target_date
)
return highlights
......@@ -129,11 +129,12 @@ def _get_course_module(course_descriptor, user):
)
def _section_has_highlights(section):
return section.highlights and not section.hide_from_toc
def _get_sections_with_highlights(course_module):
return [
section for section in course_module.get_children()
if section.highlights and not section.hide_from_toc
]
return list(filter(_section_has_highlights, course_module.get_children()))
def _get_highlights_for_week(sections, week_num, course_key):
......@@ -150,15 +151,23 @@ def _get_highlights_for_week(sections, week_num, course_key):
return section.highlights
def _get_highlights_for_next_section(sections, course_key, target_date):
sorted_sections = sorted(sections, key=lambda section: section.due)
for index, sorted_section in enumerate(sorted_sections):
if sorted_section.due.date() == target_date and index + 1 < len(sorted_sections):
def _get_highlights_for_next_section(course_module, sections, start_date, target_date):
for index, section, weeks_to_complete in spaced_out_sections(course_module):
if not _section_has_highlights(section):
continue
# We calculate section due date ourselves (rather than grabbing the due attribute),
# since not every section has a real due date (i.e. not all are graded), but we still
# want to know when this section should have been completed by the learner.
section_due_date = start_date + weeks_to_complete
if section_due_date.date() == target_date and index + 1 < len(sections):
# Return index + 2 for "week_num", since weeks start at 1 as opposed to indexes,
# and we want the next week, so +1 for index and +1 for next
return sections[index + 1].highlights, index + 2
raise CourseUpdateDoesNotExist(
u"No section found ending on {} for {}".format(
target_date, course_key
target_date, course_module.id
)
)
......@@ -469,6 +469,7 @@ class CourseNextSectionUpdate(PrefixedDebugLoggerMixin, RecipientResolver):
enrollment = schedule.enrollment
course = schedule.enrollment.course
user = enrollment.user
start_date = schedule.start_date
LOG.info(u'Received a schedule for user {} in course {} for date {}'.format(
user.username,
self.course_id,
......@@ -476,7 +477,7 @@ class CourseNextSectionUpdate(PrefixedDebugLoggerMixin, RecipientResolver):
))
try:
week_highlights, week_num = get_next_section_highlights(user, course.id, target_date)
week_highlights, week_num = get_next_section_highlights(user, course.id, start_date, target_date)
except CourseUpdateDoesNotExist:
LOG.warning(
u'Weekly highlights for user {} of course {} does not exist or is disabled'.format(
......
......@@ -2,6 +2,8 @@
import datetime
import mock
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
from openedx.core.djangoapps.schedules.content_highlights import (
course_has_highlights, get_week_highlights, get_next_section_highlights,
......@@ -135,23 +137,23 @@ class TestContentHighlights(ModuleStoreTestCase):
get_week_highlights(self.user, self.course_key, week_num=1)
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
def test_get_next_section_highlights(self):
@mock.patch('openedx.core.djangoapps.course_date_signals.utils.get_expected_duration')
def test_get_next_section_highlights(self, mock_duration):
mock_duration.return_value = datetime.timedelta(days=2)
yesterday = datetime.datetime.utcnow() - datetime.timedelta(days=1)
today = datetime.datetime.utcnow()
tomorrow = datetime.datetime.utcnow() + datetime.timedelta(days=1)
with self.store.bulk_operations(self.course_key):
self._create_chapter( # Week 1
highlights=[u'a', u'b', u'á'],
due=yesterday,
)
self._create_chapter( # Week 2
highlights=[u'skipped a week'],
due=today,
)
self.assertEqual(
get_next_section_highlights(self.user, self.course_key, yesterday.date()),
get_next_section_highlights(self.user, self.course_key, yesterday, today.date()),
([u'skipped a week'], 2),
)
with self.assertRaises(CourseUpdateDoesNotExist):
get_next_section_highlights(self.user, self.course_key, tomorrow.date())
get_next_section_highlights(self.user, self.course_key, yesterday, tomorrow.date())
......@@ -67,6 +67,10 @@ def get_user_course_duration(user, course):
if enrollment is None or enrollment.mode != CourseMode.AUDIT:
return None
verified_mode = CourseMode.verified_mode_for_course(course=course, include_expired=True)
if not verified_mode:
return None
return get_expected_duration(course)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment