From 3cee26feb4d883388e57a5d9148e6adf3b0b8dd5 Mon Sep 17 00:00:00 2001 From: Nicholas D'Alfonso Date: Tue, 10 Mar 2020 15:24:17 -0400 Subject: [PATCH] AA-59 show reset dates banner on courseware page - Also, only show banner if the course end_date has not already pass AND if the user is verified within the course. --- lms/djangoapps/courseware/tests/test_views.py | 31 +++++++++++++++- lms/djangoapps/courseware/views/index.py | 37 ++++++++++++++++++- lms/djangoapps/courseware/views/views.py | 12 +++++- lms/static/sass/base/_base.scss | 23 ++++++++++++ .../sass/course/layout/_reset_deadlines.scss | 5 ++- lms/templates/courseware/courseware.html | 6 +++ lms/templates/main.html | 2 +- lms/templates/reset_deadlines_banner.html | 2 +- .../course-outline-fragment.html | 4 +- .../tests/views/test_course_home.py | 2 + 10 files changed, 115 insertions(+), 9 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 1e61ac540e..898c91aac9 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, '
') + @ddt.ddt class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin): diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index e1f26bfd9c..a0b83ac1c4 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 c190efeff4..9a7d67de64 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 ede0085a9e..61aaf3103b 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 24c915e4ac..b931bad5ee 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 6dba5cd21c..4167364ef4 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: + +% endif <%def name="course_name()"> <% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %> diff --git a/lms/templates/main.html b/lms/templates/main.html index 83d4195bc2..efeeca1718 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 39cc944037..868bcf869d 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 _
${_("It looks like you've missed some important deadlines. Reset your deadlines and get started today.")}
- +
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 a0f0bdaf19..75c6f871ae 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 %>