From b454f9be1d2b10156b8ea392d1eea97819b55858 Mon Sep 17 00:00:00 2001 From: Nicholas D'Alfonso <ndalfonso@edx.org> Date: Thu, 14 May 2020 19:04:35 -0400 Subject: [PATCH] AA-151 dates banner on course outline and refactor - use new dates banner template on course outline page - remove old banner from main.html - let dates tab use new dates banner template - remove dates banner completely from the courseware problem view on the web app - use new banner on the courseware problem view on the mobile app - update banner util to use get_course_blocks --- .../course_home_api/dates/v1/views.py | 4 +- lms/djangoapps/courseware/tests/test_views.py | 53 ++++------- lms/djangoapps/courseware/views/index.py | 16 ---- lms/djangoapps/courseware/views/views.py | 36 ++------ lms/static/sass/base/_base.scss | 50 +++++----- lms/static/sass/bootstrap/lms-main.scss | 2 +- lms/static/sass/course/_dates.scss | 2 +- .../sass/course/layout/_dates_banner.scss | 53 +++++++++++ .../sass/course/layout/_reset_deadlines.scss | 23 ----- .../sass/features/_course-experience.scss | 4 + .../courseware/courseware-chromeless.html | 8 +- lms/templates/courseware/dates.html | 52 +---------- lms/templates/dates_banner.html | 92 +++++++++++++++++++ lms/templates/main.html | 5 - lms/templates/reset_deadlines_banner.html | 25 ----- .../course-outline-fragment.html | 17 +++- .../tests/views/test_course_home.py | 9 +- .../tests/views/test_course_outline.py | 33 +++++++ openedx/features/course_experience/utils.py | 67 ++++++++------ .../course_experience/views/course_outline.py | 26 ++++++ 20 files changed, 322 insertions(+), 255 deletions(-) create mode 100644 lms/static/sass/course/layout/_dates_banner.scss delete mode 100644 lms/static/sass/course/layout/_reset_deadlines.scss create mode 100644 lms/templates/dates_banner.html delete mode 100644 lms/templates/reset_deadlines_banner.html diff --git a/lms/djangoapps/course_home_api/dates/v1/views.py b/lms/djangoapps/course_home_api/dates/v1/views.py index 7a55fdd923f..8653ec535db 100644 --- a/lms/djangoapps/course_home_api/dates/v1/views.py +++ b/lms/djangoapps/course_home_api/dates/v1/views.py @@ -15,7 +15,7 @@ from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course from lms.djangoapps.courseware.date_summary import TodaysDate, verified_upgrade_deadline_link from lms.djangoapps.course_home_api.dates.v1.serializers import DatesTabSerializer from openedx.core.djangoapps.enrollments.api import get_enrollment -from openedx.features.course_experience.utils import reset_deadlines_banner_should_display +from openedx.features.course_experience.utils import dates_banner_should_display class DatesTabView(RetrieveAPIView): @@ -65,7 +65,7 @@ class DatesTabView(RetrieveAPIView): course_key = CourseKey.from_string(course_key_string) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) blocks = get_course_date_blocks(course, request.user, request, include_access=True, include_past_dates=True) - display_reset_dates_text = reset_deadlines_banner_should_display(course_key, request) + display_reset_dates_text, _ = dates_banner_should_display(course_key, request) learner_is_verified = False enrollment = get_enrollment(request.user.username, course_key_string) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 7c620949774..83674c543e5 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -2531,40 +2531,6 @@ class TestIndexView(ModuleStoreTestCase): expected_should_show_enroll_button ) - @RELATIVE_DATES_FLAG.override(active=True) - @ddt.data(True, False) - def test_reset_deadlines_banner_is_present_when_viewing_courseware(self, graded_section): - 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), - graded=graded_section, - ) - - 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' - ) - - banner = '<div class="reset-deadlines-banner">' - if graded_section: - self.assertContains(response, banner) - else: - self.assertNotContains(response, banner) - @ddt.ddt class TestIndexViewCompleteOnView(ModuleStoreTestCase, CompletionWaffleTestMixin): @@ -3114,7 +3080,7 @@ class DatesTabTestCase(ModuleStoreTestCase): super(DatesTabTestCase, self).setUp() now = datetime.now(utc) - self.course = CourseFactory.create(start=now + timedelta(days=-1)) + self.course = CourseFactory.create(start=now + timedelta(days=-1), self_paced=True) self.course.end = now + timedelta(days=3) CourseModeFactory(course_id=self.course.id, mode_slug=CourseMode.AUDIT) @@ -3189,6 +3155,23 @@ class DatesTabTestCase(ModuleStoreTestCase): # Should have verified pills for audit enrollments self.assertContains(response, '<div class="pill verified">') + @RELATIVE_DATES_FLAG.override(active=True) + def test_reset_deadlines_banner_displays(self): + CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode=CourseMode.VERIFIED) + now = datetime.now(utc) + with self.store.bulk_operations(self.course.id): + section = ItemFactory.create(category='chapter', parent_location=self.course.location) + ItemFactory.create( + category='sequential', + display_name='Released', + parent_location=section.location, + start=now - timedelta(days=1), + due=now - timedelta(days=1), # Setting this to tomorrow so it'll show the 'Due Next' pill + graded=True, + ) + response = self._get_response(self.course) + self.assertContains(response, 'div class="dates-banner-text"') + class TestShowCoursewareMFE(TestCase): """ diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index d055e7c8ab7..d13fdd3c9dc 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -45,7 +45,6 @@ from openedx.features.course_experience import ( RELATIVE_DATES_FLAG, ) from openedx.features.course_experience.urls import COURSE_HOME_VIEW_NAME -from openedx.features.course_experience.utils import reset_deadlines_banner_should_display from openedx.features.course_experience.views.course_sock import CourseSockFragmentView from openedx.features.enterprise_support.api import data_sharing_consent_required from student.models import CourseEnrollment @@ -424,7 +423,6 @@ class CoursewareIndex(View): Returns and creates the rendering context for the courseware. Also returns the table of contents for the courseware. """ - from lms.urls import RESET_COURSE_DEADLINES_NAME course_url_name = default_course_url_name(self.course.id) course_url = reverse(course_url_name, kwargs={'course_id': six.text_type(self.course.id)}) @@ -434,15 +432,6 @@ class CoursewareIndex(View): ) staff_access = self.is_staff - allow_anonymous = check_public_access(self.course, [COURSE_VISIBILITY_PUBLIC]) - display_reset_dates_banner = False - if not allow_anonymous and RELATIVE_DATES_FLAG.is_enabled(self.course.id): - display_reset_dates_banner = reset_deadlines_banner_should_display(self.course_key, request) - - reset_deadlines_url = reverse(RESET_COURSE_DEADLINES_NAME) if display_reset_dates_banner else None - - reset_deadlines_redirect_url_base = COURSE_HOME_VIEW_NAME if reset_deadlines_url else None - courseware_context = { 'csrf': csrf(self.request)['csrf_token'], 'course': self.course, @@ -464,11 +453,6 @@ 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), - 'display_reset_dates_banner': display_reset_dates_banner, - 'reset_deadlines_url': reset_deadlines_url, - 'reset_deadlines_redirect_url_base': reset_deadlines_redirect_url_base, - 'reset_deadlines_redirect_url_id_dict': {'course_id': str(self.course.id)}, } 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 e0fdd3be6c5..31c14a67cf4 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -115,7 +115,7 @@ from openedx.features.course_experience import ( course_home_url_name ) from openedx.features.course_experience.course_tools import CourseToolsPluginManager -from openedx.features.course_experience.utils import reset_deadlines_banner_should_display +from openedx.features.course_experience.utils import dates_banner_should_display from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML from openedx.features.course_experience.waffle import waffle as course_experience_waffle @@ -740,19 +740,6 @@ class CourseTabView(EdxFragmentView): else: masquerade = None - 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 - - reset_deadlines_url = reverse(RESET_COURSE_DEADLINES_NAME) if display_reset_dates_banner else None - - reset_deadlines_redirect_url_base = COURSE_HOME_VIEW_NAME if reset_deadlines_url else None - context = { 'course': course, 'tab': tab, @@ -763,10 +750,6 @@ class CourseTabView(EdxFragmentView): 'uses_bootstrap': uses_bootstrap, 'uses_pattern_library': not uses_bootstrap, 'disable_courseware_js': True, - 'display_reset_dates_banner': display_reset_dates_banner, - 'reset_deadlines_url': reset_deadlines_url, - 'reset_deadlines_redirect_url_base': reset_deadlines_redirect_url_base, - 'reset_deadlines_redirect_url_id_dict': {'course_id': str(course.id)} } # Avoid Multiple Mathjax loading on the 'user_profile' if 'profile_page_context' in kwargs: @@ -1078,9 +1061,7 @@ def dates(request, course_id): user_timezone = user_timezone_locale['user_timezone'] user_language = user_timezone_locale['user_language'] - display_reset_dates_text = False - if RELATIVE_DATES_FLAG.is_enabled(course.id): - display_reset_dates_text = reset_deadlines_banner_should_display(course_key, request) + missed_deadlines, enrollment_mode = dates_banner_should_display(course_key, request) context = { 'course': course, @@ -1092,7 +1073,9 @@ def dates(request, course_id): 'supports_preview_menu': True, 'can_masquerade': can_masquerade, 'masquerade': masquerade, - 'display_reset_dates_text': display_reset_dates_text, + 'on_dates_tab': True, + 'missed_deadlines': missed_deadlines, + 'enrollment_mode': enrollment_mode, 'reset_deadlines_url': reverse(RESET_COURSE_DEADLINES_NAME), 'reset_deadlines_redirect_url_base': COURSE_DATES_NAME, 'reset_deadlines_redirect_url_id_dict': {'course_id': str(course.id)} @@ -1678,9 +1661,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): 'mark-completed-on-view-after-delay': completion_service.get_complete_on_view_delay_ms() } - display_reset_dates_banner = False - if RELATIVE_DATES_FLAG.is_enabled(course.id): - display_reset_dates_banner = reset_deadlines_banner_should_display(course_key, request) + missed_deadlines, enrollment_mode = dates_banner_should_display(course_key, request) context = { 'fragment': block.render('student_view', context=student_view_context), @@ -1694,8 +1675,11 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): 'edx_notes_enabled': is_feature_enabled(course, request.user), 'staff_access': bool(request.user.has_perm(VIEW_XQA_INTERFACE, course)), 'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'), - 'display_reset_dates_banner': display_reset_dates_banner, + 'missed_deadlines': missed_deadlines, + 'enrollment_mode': enrollment_mode, 'web_app_course_url': reverse(COURSE_HOME_VIEW_NAME, args=[course.id]), + 'on_courseware_page': True, + 'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course), 'is_learning_mfe': request.META.get('HTTP_REFERER', '').startswith(settings.LEARNING_MICROFRONTEND_URL), } return render_to_response('courseware/courseware-chromeless.html', context) diff --git a/lms/static/sass/base/_base.scss b/lms/static/sass/base/_base.scss index 5f3fb4ca2c9..e572f7c5f17 100644 --- a/lms/static/sass/base/_base.scss +++ b/lms/static/sass/base/_base.scss @@ -306,29 +306,37 @@ 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"); - margin: 10px 10px 10px 0; - flex: 0 1 auto; +.dates-banner { + border-radius: 4px; + border: solid 1px #9cd2e6; + background-color: #eff8fa; + margin-top: 20px; + margin-bottom: 20px; + padding: 24px; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + max-width: $text-width-readability-max; + + .dates-banner-text { + font-size: 16px; + line-height: 24px; + color: #414141; + + a.mobile-dates-link { + color: #0075b4; + } + } - a { - color: #fff; - text-decoration: underline; + &.has-button { + .dates-banner-text { + flex: 1 1 20em; + max-width: 70%; + } } - } - form { - button { - color: #0075b4; - background-color: theme-color("inverse"); - cursor: pointer; + &.on-mobile { + margin-left: 20px; + margin-right: 20px; } - } } diff --git a/lms/static/sass/bootstrap/lms-main.scss b/lms/static/sass/bootstrap/lms-main.scss index b25e635ab8f..a1a11a1496e 100644 --- a/lms/static/sass/bootstrap/lms-main.scss +++ b/lms/static/sass/bootstrap/lms-main.scss @@ -15,7 +15,7 @@ $static-path: '../..'; @import 'layouts'; @import 'components'; @import 'course/layout/courseware_preview'; -@import 'course/layout/reset_deadlines'; +@import 'course/layout/dates_banner'; @import 'shared/modal'; @import 'shared/help-tab'; @import './elements/banners'; diff --git a/lms/static/sass/course/_dates.scss b/lms/static/sass/course/_dates.scss index 4207a44e9f8..0a9de0ab274 100644 --- a/lms/static/sass/course/_dates.scss +++ b/lms/static/sass/course/_dates.scss @@ -14,7 +14,7 @@ border: solid 1px #9cd2e6; background-color: #eff8fa; margin-top: 20px; - margin-bottom: 40px; + margin-bottom: 20px; padding: 24px; display: flex; flex-wrap: wrap; diff --git a/lms/static/sass/course/layout/_dates_banner.scss b/lms/static/sass/course/layout/_dates_banner.scss new file mode 100644 index 00000000000..813d87fb02e --- /dev/null +++ b/lms/static/sass/course/layout/_dates_banner.scss @@ -0,0 +1,53 @@ +.dates-banner { + border-radius: 4px; + border: solid 1px #9cd2e6; + background-color: #eff8fa; + margin-top: 20px; + margin-bottom: 20px; + padding: 24px; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + max-width: $text-width-readability-max; + + .dates-banner-text { + font-size: 16px; + line-height: 24px; + color: #414141; + } + + &.has-button { + .dates-banner-text { + flex: 1 1 20em; + max-width: 70%; + } + } + + .upgrade-button { + align-self: start; + flex: none; + + a { + text-decoration: none; + } + + button { + display: block; + border-radius: 2px; + border: solid 1px #0175b4; + background: white; + color: #2d323e; + font-size: 14px; + font-weight: bold; + line-height: 24px; + padding: 7px 18px; + + &:hover, + &:focus, + &:active { + cursor: pointer; + box-shadow: 0 2px 1px $shadow; + } + } + } +} diff --git a/lms/static/sass/course/layout/_reset_deadlines.scss b/lms/static/sass/course/layout/_reset_deadlines.scss deleted file mode 100644 index 34b7b66978f..00000000000 --- a/lms/static/sass/course/layout/_reset_deadlines.scss +++ /dev/null @@ -1,23 +0,0 @@ -div.reset-deadlines-banner { - background-color: theme-color("primary"); - display: none; - flex-wrap: wrap; - padding: 15px 20px; - margin-top: 5px; - - div, - button { - flex: 0 1 auto; - - &.reset-deadlines-text { - color: theme-color("inverse"); - margin: 10px 10px 10px 0; - } - - &.reset-deadlines-button { - color: #0075b4; - background-color: theme-color("inverse"); - cursor: pointer; - } - } -} diff --git a/lms/static/sass/features/_course-experience.scss b/lms/static/sass/features/_course-experience.scss index 61a6903cad8..5205cdc282a 100644 --- a/lms/static/sass/features/_course-experience.scss +++ b/lms/static/sass/features/_course-experience.scss @@ -319,6 +319,10 @@ // Course outline for visual progress waffle switch .course-outline { + .dates-banner-wrapper { + display: none; + } + .block-tree { margin: 0; padding: 0; diff --git a/lms/templates/courseware/courseware-chromeless.html b/lms/templates/courseware/courseware-chromeless.html index 05f8ce79832..99825e950a0 100644 --- a/lms/templates/courseware/courseware-chromeless.html +++ b/lms/templates/courseware/courseware-chromeless.html @@ -11,13 +11,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string <%def name="course_name()"> <% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %> </%def> - -% if display_reset_dates_banner: - <script type="text/javascript"> - $('.reset-deadlines-banner').css('display', 'flex'); - </script> -% endif - +<%include file="/dates_banner.html" /> <%block name="bodyclass">view-in-course view-courseware courseware ${course.css_class or ''}</%block> <%block name="title"><title> % if section_title: diff --git a/lms/templates/courseware/dates.html b/lms/templates/courseware/dates.html index c4a4bdab83a..14df637b262 100644 --- a/lms/templates/courseware/dates.html +++ b/lms/templates/courseware/dates.html @@ -26,57 +26,7 @@ from openedx.core.djangolib.markup import HTML, Text <h2 class="hd hd-2 date-title"> ${_("Important Dates")} </h2> - % if not display_reset_dates_text: - <div class="dates-banner"> - <div class="dates-banner-text"> - <strong>${_("We've built a suggested schedule to help you stay on track.")}</strong> - ${_("But don't worry—it's flexible so you can learn at your own pace.")} - ${_("If you happen to fall behind on our suggested dates, you'll be able to adjust them to keep yourself on track.")} - </div> - </div> - % endif - <% has_locked_assignments = any(hasattr(block, 'contains_gated_content') and block.contains_gated_content for block in course_date_blocks) %> - % if has_locked_assignments and verified_upgrade_link: - <div class="dates-banner"> - <div class="dates-banner-text banner-has-button"> - <strong>${_('You are auditing this course,')}</strong> - ${_(' which means that you are unable to participate in graded assignments.')} - % if display_reset_dates_text: - ${_(' It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today.')} - % else: - ${_(' To complete graded assignments as part of this course, you can upgrade today.')} - % endif - </div> - <div class="upgrade-button"> - <a href="${verified_upgrade_link}"> - <button type="button"> - % if display_reset_dates_text: - ${_('Upgrade to shift due dates')} - % else: - ${_('Upgrade now')} - % endif - </button> - </a> - </div> - </div> - % endif - % if display_reset_dates_text and learner_is_verified: - <div class="dates-banner"> - <div class="dates-banner-text banner-has-button"> - <strong>${_('It looks like you missed some important deadlines based on our suggested schedule.')}</strong> - ${_('To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates. ')} - </div> - <div class="upgrade-button"> - <form method="post" action="${reset_deadlines_url}"> - <input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}"> - <input type="hidden" name="reset_deadlines_redirect_url_base" value="${reset_deadlines_redirect_url_base}"> - <input type="hidden" name="reset_deadlines_redirect_url_id_dict" value="${reset_deadlines_redirect_url_id_dict}"> - <button class="upgrade-button">${_("Reset my deadlines")}</button> - </form> - </div> - </div> - % endif - + <%include file="/dates_banner.html" /> <% due_next_set = False %> % for block in course_date_blocks: <% block_is_verified = (hasattr(block, 'contains_gated_content') and block.contains_gated_content) or isinstance(block, VerificationDeadlineDate) %> diff --git a/lms/templates/dates_banner.html b/lms/templates/dates_banner.html new file mode 100644 index 00000000000..f26e7c2451b --- /dev/null +++ b/lms/templates/dates_banner.html @@ -0,0 +1,92 @@ +## mako + +<%page expression_filter="h"/> +<%! +from django.utils.translation import ugettext as _ + +from lms.djangoapps.courseware.date_summary import CourseAssignmentDate +from course_modes.models import CourseMode +%> + +% if on_dates_tab and not missed_deadlines and getattr(course, 'self_paced', False): + <div class="dates-banner"> + <div class="dates-banner-text"> + <strong>${_("We've built a suggested schedule to help you stay on track.")}</strong> + ${_("But don't worry—it's flexible so you can learn at your own pace. If you happen to fall behind on our suggested dates, you'll be able to adjust them to keep yourself on track.")} + </div> + </div> +% endif +<% +has_locked_assignments = any(hasattr(block, 'contains_gated_content') and block.contains_gated_content for block in course_date_blocks if isinstance(block, CourseAssignmentDate)) if (course_date_blocks and on_dates_tab) else False +on_dates_tab_as_audit = on_dates_tab and enrollment_mode == CourseMode.AUDIT +on_dates_tab_as_verified = on_dates_tab and enrollment_mode == CourseMode.VERIFIED +on_course_outline_page_as_audit = on_course_outline_page and enrollment_mode == CourseMode.AUDIT +on_course_outline_page_as_verified = on_course_outline_page and enrollment_mode == CourseMode.VERIFIED +on_courseware_page_as_audit = on_courseware_page and enrollment_mode == CourseMode.AUDIT +on_courseware_page_as_verified = on_courseware_page and enrollment_mode == CourseMode.VERIFIED +additional_styling_class = 'on-mobile' if web_app_course_url else 'has-button' +%> +% if (missed_deadlines and (on_dates_tab_as_verified or on_courseware_page_as_audit or on_courseware_page_as_verified)) or (on_dates_tab_as_audit and has_locked_assignments) or on_course_outline_page_as_audit or on_course_outline_page_as_verified: + <div class="dates-banner ${additional_styling_class}"> + <div class="dates-banner-text"> + % if web_app_course_url: + % if enrollment_mode == CourseMode.VERIFIED: + ${_('It looks like you missed some important deadlines based on our suggested schedule. ')} + ${_('To keep yourself on track, you can update this schedule and shift the past due assignments into the future by visiting ')} + <a class="mobile-dates-link" href="${web_app_course_url}">edx.org</a>. + ${_(' Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.')} + % else: + <strong>${_('You are auditing this course,')}</strong> + ${_(' which means that you are unable to participate in graded assignments.')} + ${_(' It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today by visiting ')} + <a class="mobile-dates-link" href="${verified_upgrade_link}">edx.org</a>. + % endif + % else: + % if on_course_outline_page_as_verified or (missed_deadlines and (on_dates_tab_as_verified or on_courseware_page_as_verified)): + <strong>${_('It looks like you missed some important deadlines based on our suggested schedule.')}</strong> + ${_('To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.')} + % endif + % if (on_dates_tab_as_audit and has_locked_assignments) or on_course_outline_page_as_audit or (on_courseware_page_as_audit and missed_deadlines): + <strong>${_('You are auditing this course,')}</strong> + ${_(' which means that you are unable to participate in graded assignments.')} + % if on_dates_tab: + % if missed_deadlines: + ${_(' It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today.')} + % else: + ${_(' To complete graded assignments as part of this course, you can upgrade today.')} + % endif + % else: + ${_(' It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today.')} + % endif + % endif + % endif + </div> + % if not web_app_course_url: + <div class="upgrade-button"> + % if on_course_outline_page_as_verified or (missed_deadlines and (on_dates_tab_as_verified or on_courseware_page_as_verified)): + <form method="post" action="${reset_deadlines_url}"> + <input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}"> + <input type="hidden" name="reset_deadlines_redirect_url_base" value="${reset_deadlines_redirect_url_base}"> + <input type="hidden" name="reset_deadlines_redirect_url_id_dict" value="${reset_deadlines_redirect_url_id_dict}"> + <button class="btn reset-deadlines-button">${_("Reset my deadlines")}</button> + </form> + % endif: + % if (on_dates_tab_as_audit and has_locked_assignments) or on_course_outline_page_as_audit or (on_courseware_page_as_audit and missed_deadlines): + <a href="${verified_upgrade_link}"> + <button type="button"> + % if on_dates_tab: + % if missed_deadlines: + ${_('Upgrade to shift due dates')} + % else: + ${_('Upgrade now')} + % endif + % else: + ${_('Upgrade to shift due dates')} + % endif + </button> + </a> + % endif + </div> + % endif + </div> +% endif diff --git a/lms/templates/main.html b/lms/templates/main.html index efeeca17182..48edc767a13 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -185,11 +185,6 @@ from pipeline_mako import render_require_js_path_overrides <%include file="/preview_menu.html" /> % endif - <% is_course_staff = bool(user and course and has_access(user, 'staff', course, course.id)) %> - % if course and course.self_paced and display_reset_dates_banner and not is_course_staff: - <%include file="/reset_deadlines_banner.html" /> - % endif - <%include file="/page_banner.html" /> <div class="marketing-hero"><%block name="marketing_hero"></%block></div> diff --git a/lms/templates/reset_deadlines_banner.html b/lms/templates/reset_deadlines_banner.html deleted file mode 100644 index a4de68053e0..00000000000 --- a/lms/templates/reset_deadlines_banner.html +++ /dev/null @@ -1,25 +0,0 @@ -## mako - -<%page expression_filter="h"/> -<%! -from django.utils.translation import ugettext as _ -%> -<div class="reset-deadlines-banner"> - <div class="reset-deadlines-text"> - % if web_app_course_url: - ${_("It looks like you've missed some important deadlines. Visit ")} - <a href="${web_app_course_url}">${_("edx.org")}</a> - ${_(" to reset your deadlines and get started today.")} - % else: - ${_("It looks like you've missed some important deadlines. Reset your deadlines and get started today.")} - % endif - </div> - % if not web_app_course_url: - <form method="post" action="${reset_deadlines_url}"> - <input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}"> - <input type="hidden" name="reset_deadlines_redirect_url_base" value="${reset_deadlines_redirect_url_base}"> - <input type="hidden" name="reset_deadlines_redirect_url_id_dict" value="${reset_deadlines_redirect_url_id_dict}"> - <button class="btn reset-deadlines-button">${_("Reset my deadlines")}</button> - </form> - % endif -</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 53169150c3a..5435b2370c5 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 @@ -13,15 +13,24 @@ from django.utils import timezone from django.utils.translation import gettext as _ from django.utils.translation import ngettext +from lms.djangoapps.courseware.access import has_access from openedx.core.djangolib.markup import HTML, Text +from openedx.features.course_experience import RELATIVE_DATES_FLAG %> <% course_sections = blocks.get('children') self_paced = context.get('self_paced', False) -reset_deadlines_banner_displayed = False +relative_dates_flag_is_enabled = RELATIVE_DATES_FLAG.is_enabled(str(course_key)) +is_course_staff = bool(user and course and has_access(user, 'staff', course, course.id)) +dates_banner_displayed = False %> <main role="main" class="course-outline" id="main" tabindex="-1"> + <div class="dates-banner-wrapper"> + % if enrollment_mode and relative_dates_flag_is_enabled and self_paced and not is_course_staff: + <%include file="/dates_banner.html" /> + % endif + </div> % if course_sections is not None: <button class="btn btn-primary" id="expand-collapse-outline-all-button" @@ -63,10 +72,10 @@ reset_deadlines_banner_displayed = False due_date = subsection.get('due') overdue = due_date is not None and due_date < timezone.now() and not subsection.get('complete', True) %> - % if graded and overdue and not reset_deadlines_banner_displayed: - <% reset_deadlines_banner_displayed = True %> + % if graded and overdue and not dates_banner_displayed: + <% dates_banner_displayed = True %> <script type="text/javascript"> - $('.reset-deadlines-banner').css('display', 'flex'); + $('.dates-banner-wrapper').css('display', 'block'); </script> % endif <li class="subsection accordion ${ 'current' if subsection.get('resume_block') else '' } ${graded} ${scored}"> 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 70b256f03b6..ec9e85c7af6 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -219,7 +219,7 @@ class TestCourseHomePage(CourseHomePageTestCase): # Fetch the view and verify the query counts # TODO: decrease query count as part of REVO-28 - with self.assertNumQueries(75, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): + with self.assertNumQueries(77, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): with check_mongo_calls(4): url = course_home_url(self.course) self.client.get(url) @@ -1022,10 +1022,3 @@ class CourseHomeFragmentViewTests(ModuleStoreTestCase): response = self.client.get(self.url) self.assertContains(response, "<span>DISCOUNT_PRICE</span>") - - @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">') 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 81fe5701518..709bd8ef56d 100644 --- a/openedx/features/course_experience/tests/views/test_course_outline.py +++ b/openedx/features/course_experience/tests/views/test_course_outline.py @@ -7,6 +7,7 @@ import datetime import json import re +import ddt import six from completion import waffle from completion.models import BlockCompletion @@ -23,6 +24,7 @@ from six import text_type from waffle.models import Switch from waffle.testutils import override_switch +from course_modes.models import CourseMode from lms.djangoapps.courseware.tests.factories import StaffFactory from lms.urls import RESET_COURSE_DEADLINES_NAME from gating import api as lms_gating_api @@ -30,6 +32,7 @@ from lms.djangoapps.course_api.blocks.transformers.milestones import MilestonesA 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 import RELATIVE_DATES_FLAG from openedx.features.course_experience.views.course_outline import ( DEFAULT_COMPLETION_TRACKING_START, CourseOutlineFragmentView @@ -46,6 +49,7 @@ TEST_PASSWORD = 'test' GATING_NAMESPACE_QUALIFIER = '.gating' +@ddt.ddt class TestCourseOutlinePage(SharedModuleStoreTestCase): """ Test the course outline view. @@ -164,6 +168,35 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase): self.assertRegex(content, sequential2.display_name + r'\s*\(1 Question\)\s*</h4>') self.assertRegex(content, sequential3.display_name + r'\s*\(2 Questions\)\s*</h4>') + @RELATIVE_DATES_FLAG.override(active=True) + @ddt.data( + (CourseMode.AUDIT, False, True), + (CourseMode.VERIFIED, False, True), + (CourseMode.MASTERS, False, False), + (CourseMode.VERIFIED, True, False), + ) + @ddt.unpack + def test_reset_course_deadlines_banner_shows_for_self_paced_course( + self, + enrollment_mode, + is_course_staff, + should_display + ): + course = self.courses[0] + enrollment = CourseEnrollment.objects.get(course_id=course.id) + enrollment.mode = enrollment_mode + enrollment.save() + self.user.is_staff = is_course_staff + self.user.save() + + url = course_home_url(course) + response = self.client.get(url) + + if should_display: + self.assertContains(response, '<div class="dates-banner-text"') + else: + self.assertNotContains(response, '<div class="dates-banner-text"') + def test_reset_course_deadlines(self): course = self.courses[0] enrollment = CourseEnrollment.objects.get(course_id=course.id) diff --git a/openedx/features/course_experience/utils.py b/openedx/features/course_experience/utils.py index a7ab4f386e8..f355811aaca 100644 --- a/openedx/features/course_experience/utils.py +++ b/openedx/features/course_experience/utils.py @@ -13,10 +13,12 @@ from six.moves import range from course_modes.models import CourseMode from lms.djangoapps.course_api.blocks.api import get_blocks +from lms.djangoapps.course_blocks.api import get_course_blocks from lms.djangoapps.course_blocks.utils import get_student_module_as_dict from lms.djangoapps.courseware.access import has_access from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.lib.cache_utils import request_cached +from openedx.features.course_experience import RELATIVE_DATES_FLAG from student.models import CourseEnrollment from xmodule.modulestore.django import modulestore @@ -253,38 +255,43 @@ def get_resume_block(block): return block -def reset_deadlines_banner_should_display(course_key, request): +def dates_banner_should_display(course_key, request): """ Return whether or not the reset banner should display, determined by whether or not a course has any past-due, - incomplete sequentials + incomplete sequentials and which enrollment mode is being + dealt with for the current user and course. """ - display_reset_dates_banner = False - course_overview = CourseOverview.objects.get(id=str(course_key)) - course_end_date = getattr(course_overview, 'end_date', None) - is_self_paced = getattr(course_overview, 'self_paced', False) - is_course_staff = bool( - request.user and course_overview and has_access(request.user, 'staff', course_overview, course_overview.id) - ) - if is_self_paced and (not is_course_staff) and (not course_end_date or timezone.now() < course_end_date): - if CourseEnrollment.objects.filter( - course=course_overview, user=request.user, - ).filter( - Q(mode=CourseMode.AUDIT) | Q(mode=CourseMode.VERIFIED) - ).exists(): - course_block_tree = get_course_outline_block_tree( - request, str(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('graded', False) - and subsection.get('due', timezone.now() + timedelta(1)) < timezone.now() - ): - display_reset_dates_banner = True + missed_deadlines = False + course_enrollment = None + if RELATIVE_DATES_FLAG.is_enabled(str(course_key)): + course_overview = CourseOverview.objects.get(id=str(course_key)) + course_end_date = getattr(course_overview, 'end_date', None) + is_self_paced = getattr(course_overview, 'self_paced', False) + is_course_staff = bool( + request.user and course_overview and has_access(request.user, 'staff', course_overview, course_overview.id) + ) + if is_self_paced and (not is_course_staff) and (not course_end_date or timezone.now() < course_end_date): + course_enrollment = CourseEnrollment.objects.filter( + course=course_overview, user=request.user, + ).filter( + Q(mode=CourseMode.AUDIT) | Q(mode=CourseMode.VERIFIED) + ).first() + if course_enrollment: + store = modulestore() + course_usage_key = store.make_course_usage_key(course_key) + block_data = get_course_blocks(request.user, course_usage_key, include_completion=True) + for section_key in block_data.get_children(course_usage_key): + if missed_deadlines: break - return display_reset_dates_banner + for subsection_key in block_data.get_children(section_key): + if ( + not block_data.get_xblock_field(subsection_key, 'complete', False) + and block_data.get_xblock_field(subsection_key, 'graded', False) + and block_data.get_xblock_field( + subsection_key, 'due', timezone.now() + timedelta(1)) < timezone.now() + ): + missed_deadlines = True + break + + return missed_deadlines, getattr(course_enrollment, 'mode', None) diff --git a/openedx/features/course_experience/views/course_outline.py b/openedx/features/course_experience/views/course_outline.py index 61abbc926b6..8005a74f254 100644 --- a/openedx/features/course_experience/views/course_outline.py +++ b/openedx/features/course_experience/views/course_outline.py @@ -9,10 +9,12 @@ import six from completion import waffle as completion_waffle from django.contrib.auth.models import User +from django.db.models import Q 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.utils import timezone from django.views.decorators.csrf import ensure_csrf_cookie import edx_when.api as edx_when_api from opaque_keys.edx.keys import CourseKey @@ -20,11 +22,16 @@ from pytz import UTC from waffle.models import Switch from web_fragments.fragment import Fragment +from course_modes.models import CourseMode from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.courses import get_course_overview_with_access +from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link from lms.djangoapps.courseware.masquerade import setup_masquerade +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule +from openedx.features.course_experience import RELATIVE_DATES_FLAG +from student.models import CourseEnrollment from util.milestones_helpers import get_course_content_milestones from xmodule.course_module import COURSE_VISIBILITY_PUBLIC from xmodule.modulestore.django import modulestore @@ -43,6 +50,9 @@ class CourseOutlineFragmentView(EdxFragmentView): """ Renders the course outline as a fragment. """ + from lms.urls import RESET_COURSE_DEADLINES_NAME + from openedx.features.course_experience.urls import COURSE_HOME_VIEW_NAME + course_key = CourseKey.from_string(course_id) course_overview = get_course_overview_with_access( request.user, 'load', course_key, check_if_enrolled=user_is_enrolled @@ -61,6 +71,7 @@ class CourseOutlineFragmentView(EdxFragmentView): 'due_date_display_format': course.due_date_display_format, 'blocks': course_block_tree, 'enable_links': user_is_enrolled or course.course_visibility == COURSE_VISIBILITY_PUBLIC, + 'course_key': course_key, } resume_block = get_resume_block(course_block_tree) if user_is_enrolled else None @@ -82,6 +93,21 @@ class CourseOutlineFragmentView(EdxFragmentView): # managed by edx-when. context['in_edx_when'] = edx_when_api.is_enabled_for_course(course_key) + reset_deadlines_url = reverse(RESET_COURSE_DEADLINES_NAME) + reset_deadlines_redirect_url_base = COURSE_HOME_VIEW_NAME + + course_enrollment = None + if not request.user.is_anonymous: + course_enrollment = CourseEnrollment.objects.filter(course=course_overview, user=request.user).filter( + Q(mode=CourseMode.AUDIT) | Q(mode=CourseMode.VERIFIED)).first() + + context['reset_deadlines_url'] = reset_deadlines_url + context['reset_deadlines_redirect_url_base'] = reset_deadlines_redirect_url_base + context['reset_deadlines_redirect_url_id_dict'] = {'course_id': str(course.id)} + context['enrollment_mode'] = getattr(course_enrollment, 'mode', None) + context['verified_upgrade_link'] = verified_upgrade_deadline_link(request.user, course=course), + context['on_course_outline_page'] = True, + html = render_to_string('course_experience/course-outline-fragment.html', context) return Fragment(html) -- GitLab