diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 1e61ac540ecd81ddef0c503cb2f7aaa7e3616d15..898c91aac9a5163af1e2fd8f1170a38ac2b40b0f 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -273,8 +273,8 @@ class IndexQueryTestCase(ModuleStoreTestCase): NUM_PROBLEMS = 20 @ddt.data( - (ModuleStoreEnum.Type.mongo, 10, 172), - (ModuleStoreEnum.Type.split, 4, 170), + (ModuleStoreEnum.Type.mongo, 10, 174), + (ModuleStoreEnum.Type.split, 4, 172), ) @ddt.unpack def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): @@ -2600,6 +2600,33 @@ class TestIndexView(ModuleStoreTestCase): expected_should_show_enroll_button ) + def test_reset_deadlines_banner_is_present_when_viewing_courseware(self): + user = UserFactory() + course = CourseFactory.create(self_paced=True) + with self.store.bulk_operations(course.id): + chapter = ItemFactory.create(parent=course, category='chapter') + section = ItemFactory.create( + parent=chapter, category='sequential', + display_name="Sequence", + due=datetime.today() - timedelta(1), + ) + + CourseOverview.load_from_module_store(course.id) + CourseEnrollmentFactory(user=user, course_id=course.id, mode=CourseMode.VERIFIED) + self.client.login(username=user.username, password='test') + response = self.client.get( + reverse( + 'courseware_section', + kwargs={ + 'course_id': six.text_type(course.id), + 'chapter': chapter.url_name, + 'section': section.url_name, + } + ) + '?activate_block_id=test_block_id' + ) + + self.assertContains(response, '<div class="reset-deadlines-banner">') + @ddt.ddt class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin): diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index e1f26bfd9c1759728151323c64d82ad2da427a8a..a0b83ac1c4f41f50e6e9ea97ae92c131ba226518 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -7,6 +7,7 @@ View for Courseware Index import logging +from datetime import timedelta import six import six.moves.urllib as urllib # pylint: disable=import-error import six.moves.urllib.error # pylint: disable=import-error @@ -18,6 +19,7 @@ from django.contrib.auth.views import redirect_to_login from django.http import Http404 from django.template.context_processors import csrf from django.urls import reverse +from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.functional import cached_property from django.utils.translation import ugettext as _ @@ -29,6 +31,7 @@ from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey from web_fragments.fragment import Fragment +from course_modes.models import CourseMode from edxmako.shortcuts import render_to_response, render_to_string from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context @@ -44,11 +47,14 @@ from openedx.core.djangolib.markup import HTML, Text from openedx.features.course_experience import ( COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, COURSE_OUTLINE_PAGE_FLAG, - default_course_url_name + default_course_url_name, + RELATIVE_DATES_FLAG, ) +from openedx.features.course_experience.utils import get_course_outline_block_tree from openedx.features.course_experience.views.course_sock import CourseSockFragmentView from openedx.features.enterprise_support.api import data_sharing_consent_required from shoppingcart.models import CourseRegistrationCode +from student.models import CourseEnrollment from student.views import is_course_blocked from util.views import ensure_valid_course_key from xmodule.course_module import COURSE_VISIBILITY_PUBLIC @@ -446,6 +452,32 @@ class CoursewareIndex(View): ) staff_access = self.is_staff + reset_deadlines_url = reverse( + 'openedx.course_experience.reset_course_deadlines', kwargs={'course_id': six.text_type(self.course.id)} + ) + + allow_anonymous = allow_public_access(self.course, [COURSE_VISIBILITY_PUBLIC]) + display_reset_dates_banner = False + if not allow_anonymous: # pylint: disable=too-many-nested-blocks + course_overview = CourseOverview.objects.get(id=str(self.course_key)) + end_date = getattr(course_overview, 'end_date') + if not end_date or timezone.now() < end_date: + if (CourseEnrollment.objects.filter( + course=course_overview, user=request.user, mode=CourseMode.VERIFIED + ).exists()): + course_block_tree = get_course_outline_block_tree( + request, str(self.course_key), request.user + ) + course_sections = course_block_tree.get('children') + for section in course_sections: + if display_reset_dates_banner: + break + for subsection in section.get('children', []): + if (not subsection.get('complete', True) + and subsection.get('due', timezone.now() + timedelta(1)) < timezone.now()): + display_reset_dates_banner = True + break + courseware_context = { 'csrf': csrf(self.request)['csrf_token'], 'course': self.course, @@ -467,6 +499,9 @@ class CoursewareIndex(View): 'sequence_title': None, 'disable_accordion': COURSE_OUTLINE_PAGE_FLAG.is_enabled(self.course.id), 'show_search': show_search, + 'relative_dates_is_enabled': RELATIVE_DATES_FLAG.is_enabled(self.course.id), + 'reset_deadlines_url': reset_deadlines_url, + 'display_reset_dates_banner': display_reset_dates_banner, } courseware_context.update( get_experiment_user_metadata_context( diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index c190efeff42356dd25a6d874ece6d2c25148abb6..9a7d67de64835986c4778b28e9709eb6f197bcc0 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -24,6 +24,7 @@ from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpRespo from django.shortcuts import redirect from django.template.context_processors import csrf from django.urls import reverse +from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.http import urlquote_plus from django.utils.text import slugify @@ -734,6 +735,15 @@ class CourseTabView(EdxFragmentView): 'openedx.course_experience.reset_course_deadlines', kwargs={'course_id': text_type(course.id)} ) + display_reset_dates_banner = False + if RELATIVE_DATES_FLAG.is_enabled(course.id): + course_overview = CourseOverview.get_from_id(course.id) + end_date = getattr(course_overview, 'end_date', None) + if (not end_date or timezone.now() < end_date and CourseEnrollment.objects.filter( + course=course_overview, user=request.user, mode=CourseMode.VERIFIED + ).exists()): + display_reset_dates_banner = True + context = { 'course': course, 'tab': tab, @@ -744,8 +754,8 @@ class CourseTabView(EdxFragmentView): 'uses_bootstrap': uses_bootstrap, 'uses_pattern_library': not uses_bootstrap, 'disable_courseware_js': True, - 'relative_dates_is_enabled': RELATIVE_DATES_FLAG.is_enabled(course.id), 'reset_deadlines_url': reset_deadlines_url, + 'display_reset_dates_banner': display_reset_dates_banner, } # Avoid Multiple Mathjax loading on the 'user_profile' if 'profile_page_context' in kwargs: diff --git a/lms/static/sass/base/_base.scss b/lms/static/sass/base/_base.scss index ede0085a9e5874faba877c78d8add312cb5b656f..61aaf3103b860842cc75240c39d2eafded338527 100644 --- a/lms/static/sass/base/_base.scss +++ b/lms/static/sass/base/_base.scss @@ -304,3 +304,26 @@ mark { } } } + +div.reset-deadlines-banner { + background-color: theme-color("primary"); + display: none; + flex-wrap: wrap; + padding: 15px 20px; + margin-top: 5px; + + div.reset-deadlines-text { + color: theme-color("inverse"); + padding-top: 10px; + margin-right: 10px; + flex: 0 0 auto; + } + + form { + button { + color: #0075b4; + background-color: theme-color("inverse"); + cursor: pointer; + } + } +} diff --git a/lms/static/sass/course/layout/_reset_deadlines.scss b/lms/static/sass/course/layout/_reset_deadlines.scss index 24c915e4ac9073ea575fdc209bc855bfc506c2e3..b931bad5ee0a1387999256ca7198050588cd244c 100644 --- a/lms/static/sass/course/layout/_reset_deadlines.scss +++ b/lms/static/sass/course/layout/_reset_deadlines.scss @@ -11,12 +11,13 @@ div.reset-deadlines-banner { &.reset-deadlines-text { color: theme-color("inverse"); - padding-top: 2px; + padding-top: 10px; margin-right: 10px; } &.reset-deadlines-button { - border-radius: 5px; + color: #0075b4; + background-color: theme-color("inverse"); cursor: pointer; } } diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 6dba5cd21ce9f7328cc70c21f382bbc6b92bc734..4167364ef41164f4831bd3834c8c637d143849cd 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -22,6 +22,12 @@ from openedx.features.course_experience import course_home_page_title, COURSE_OU (course.enable_proctored_exams or course.enable_timed_exams) ) %> + +% if display_reset_dates_banner: + <script type="text/javascript"> + $('.reset-deadlines-banner').css('display', 'flex'); + </script> +% endif <%def name="course_name()"> <% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %> </%def> diff --git a/lms/templates/main.html b/lms/templates/main.html index 83d4195bc20c73eeb9afa66d7edfbb9d8e662b0f..efeeca17182f571065e5ec9328a02c27387c4a31 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -186,7 +186,7 @@ from pipeline_mako import render_require_js_path_overrides % endif <% is_course_staff = bool(user and course and has_access(user, 'staff', course, course.id)) %> - % if course and course.self_paced and tab and not is_course_staff and relative_dates_is_enabled: + % if course and course.self_paced and display_reset_dates_banner and not is_course_staff: <%include file="/reset_deadlines_banner.html" /> % endif diff --git a/lms/templates/reset_deadlines_banner.html b/lms/templates/reset_deadlines_banner.html index 39cc944037eb837aa5e22c95382d747b0f9b5735..868bcf869d7246a452d35dc4e031c89b6275ac79 100644 --- a/lms/templates/reset_deadlines_banner.html +++ b/lms/templates/reset_deadlines_banner.html @@ -8,6 +8,6 @@ from django.utils.translation import ugettext as _ <div class="reset-deadlines-text">${_("It looks like you've missed some important deadlines. Reset your deadlines and get started today.")}</div> <form method="post" action="${reset_deadlines_url}"> <input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}"> - <button class="reset-deadlines-button">${_("Reset my deadlines")}</button> + <button class="btn reset-deadlines-button">${_("Reset my deadlines")}</button> </form> </div> diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html index a0f0bdaf196a06b61c7c2c138d1aa5f29f232036..75c6f871ae189a290e58d295c52772da7caa69f9 100644 --- a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html @@ -58,8 +58,10 @@ reset_deadlines_banner_displayed = False needs_prereqs = not gated_content[subsection['id']]['completed_prereqs'] if gated_subsection else False scored = 'scored' if subsection.get('scored', False) else '' graded = 'graded' if subsection.get('graded') else '' + due_date = subsection.get('due') + overdue = due_date is not None and due_date < timezone.now() and not subsection.get('complete', True) %> - % if not subsection.get('complete', True) and subsection.get('due', timezone.now() + timedelta(1)) < timezone.now() and not reset_deadlines_banner_displayed: + % if overdue and not reset_deadlines_banner_displayed: <% reset_deadlines_banner_displayed = True %> <script type="text/javascript"> $('.reset-deadlines-banner').css('display', 'flex'); diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py index 798f348d13db0565b01e056667e6c8dbac6f4fcf..6e56952bd154b7435b2760a6755022baba630595 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -1025,5 +1025,7 @@ class CourseHomeFragmentViewTests(ModuleStoreTestCase): @RELATIVE_DATES_FLAG.override(active=True) def test_reset_deadline_banner_is_present_on_course_tab(self): + CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) # pylint: disable=no-member response = self.client.get(self.url) + self.assertContains(response, '<div class="reset-deadlines-banner">')