% if course_sections is not None: @@ -56,6 +59,12 @@ self_paced = context.get('self_paced', False) scored = 'scored' if subsection.get('scored', False) else '' graded = 'graded' if subsection.get('graded') else '' %> + % if not subsection.get('complete', True) and subsection.get('due', timezone.now() + timedelta(1)) < timezone.now() and not reset_deadlines_banner_displayed: + <% reset_deadlines_banner_displayed = True %> + + % endif
  • DISCOUNT_PRICE") + + @override_waffle_flag(RELATIVE_DATES_FLAG, active=True) + def test_reset_deadline_banner_is_present_on_course_tab(self): + response = self.client.get(self.url) + self.assertContains(response, '
    ') diff --git a/openedx/features/course_experience/tests/views/test_course_outline.py b/openedx/features/course_experience/tests/views/test_course_outline.py index 88d413a250..07353f8634 100644 --- a/openedx/features/course_experience/tests/views/test_course_outline.py +++ b/openedx/features/course_experience/tests/views/test_course_outline.py @@ -14,6 +14,7 @@ from completion.test_utils import CompletionWaffleTestMixin from django.contrib.sites.models import Site from django.test import override_settings from django.urls import reverse +from django.utils import timezone from milestones.tests.utils import MilestonesTestCaseMixin from mock import Mock, patch from opaque_keys.edx.keys import CourseKey, UsageKey @@ -25,6 +26,8 @@ from waffle.testutils import override_switch from lms.djangoapps.courseware.tests.factories import StaffFactory from gating import api as lms_gating_api from lms.djangoapps.course_api.blocks.transformers.milestones import MilestonesAndSpecialExamsTransformer +from openedx.core.djangoapps.schedules.models import Schedule +from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory from openedx.core.lib.gating import api as gating_api from openedx.features.course_experience.views.course_outline import ( DEFAULT_COMPLETION_TRACKING_START, @@ -55,7 +58,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase): # pylint: disable=super-method-not-called with super(TestCourseOutlinePage, cls).setUpClassAndTestData(): cls.courses = [] - course = CourseFactory.create() + course = CourseFactory.create(self_paced=True) with cls.store.bulk_operations(course.id): chapter = ItemFactory.create(category='chapter', parent_location=course.location) sequential = ItemFactory.create(category='sequential', parent_location=chapter.location) @@ -132,6 +135,18 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase): self.assertContains(response, sequential.format) self.assertTrue(sequential.children) + def test_reset_course_deadlines(self): + course = self.courses[0] + enrollment = CourseEnrollment.objects.get(course_id=course.id) + ScheduleFactory( + start_date=timezone.now() - datetime.timedelta(1), + enrollment=enrollment + ) + url = '{}{}'.format(course_home_url(course), 'reset_deadlines') + self.client.post(url) + updated_schedule = Schedule.objects.get(enrollment=enrollment) + self.assertEqual(updated_schedule.start_date.day, datetime.datetime.today().day) + class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, MilestonesTestCaseMixin): """ diff --git a/openedx/features/course_experience/urls.py b/openedx/features/course_experience/urls.py index 14a96b7ac5..1a444ab99a 100644 --- a/openedx/features/course_experience/urls.py +++ b/openedx/features/course_experience/urls.py @@ -7,7 +7,7 @@ from django.conf.urls import url from .views.course_dates import CourseDatesFragmentMobileView from .views.course_home import CourseHomeFragmentView, CourseHomeView -from .views.course_outline import CourseOutlineFragmentView +from .views.course_outline import CourseOutlineFragmentView, reset_course_deadlines from .views.course_reviews import CourseReviewsView from .views.course_sock import CourseSockFragmentView from .views.course_updates import CourseUpdatesFragmentView, CourseUpdatesView @@ -70,4 +70,9 @@ urlpatterns = [ CourseDatesFragmentMobileView.as_view(), name='openedx.course_experience.mobile_dates_fragment_view', ), + url( + r'^reset_deadlines$', + reset_course_deadlines, + name='openedx.course_experience.reset_course_deadlines', + ), ] diff --git a/openedx/features/course_experience/views/course_outline.py b/openedx/features/course_experience/views/course_outline.py index bab2cdc485..228aa2c480 100644 --- a/openedx/features/course_experience/views/course_outline.py +++ b/openedx/features/course_experience/views/course_outline.py @@ -5,11 +5,16 @@ Views to show a course outline. import datetime import re +import pytz +import six from completion import waffle as completion_waffle from django.contrib.auth.models import User +from django.shortcuts import redirect from django.template.context_processors import csrf from django.template.loader import render_to_string +from django.urls import reverse +from django.views.decorators.csrf import ensure_csrf_cookie import edx_when.api as edx_when_api from opaque_keys.edx.keys import CourseKey from pytz import UTC @@ -17,6 +22,7 @@ from waffle.models import Switch from web_fragments.fragment import Fragment from lms.djangoapps.courseware.courses import get_course_overview_with_access +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from student.models import CourseEnrollment from util.milestones_helpers import get_course_content_milestones @@ -154,3 +160,19 @@ class CourseOutlineFragmentView(EdxFragmentView): if children: children[0]['resume_block'] = True self.mark_first_unit_to_resume(children[0]) + + +@ensure_csrf_cookie +def reset_course_deadlines(request, course_id): + """ + Set the start_date of a schedule to today, which in turn will adjust due dates for + sequentials belonging to a self paced course + """ + course = CourseOverview.objects.get(id=course_id) + if course.self_paced: + enrollment = CourseEnrollment.objects.get(user=request.user, course=course_id) + schedule = enrollment.schedule + if schedule: + schedule.start_date = datetime.datetime.now(pytz.utc) + schedule.save() + return redirect(reverse('openedx.course_experience.course_home', args=[six.text_type(course_id)]))