diff --git a/lms/djangoapps/course_home_api/dates/v1/views.py b/lms/djangoapps/course_home_api/dates/v1/views.py index 7a55fdd923fc8a6866085d9bff9d5ed3c6c4c1c7..8653ec535dbc66235493c2b7babdd4568fafb03c 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 7c6209497745c6e0d5300ff22cead578706e60b8..83674c543e5c1fc1868a9d480fc57450d93d6dd5 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 d055e7c8ab7dd18b727ed9a729fb702135187144..d13fdd3c9dc8b5b46afae4dc80dc5644c65635cf 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 e0fdd3be6c5ae9a20068f7f62b825db43160b1d5..31c14a67cf475cc545dfca1b079ada6459b72556 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 5f3fb4ca2c9e3fdbdc0ad82d72d114e498884436..e572f7c5f17620a272047e8011b2e65e7b39904e 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 b25e635ab8f4f13f906afd5a7533ed19b63e6d39..a1a11a1496e5746f5bc06bb9f09b83abafee0dc6 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 4207a44e9f85c19b821c63995fd4c6bfb0c1a2aa..0a9de0ab274613506aeb42da549e84bf0caedc8c 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 0000000000000000000000000000000000000000..813d87fb02e1494495afef8977d353ad597209f0 --- /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 34b7b66978ff11f36014408be5767b270c136f3a..0000000000000000000000000000000000000000 --- 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 61a6903cad842a9d9c291e06b7eb1e03e2f4d84d..5205cdc282ac6204db9bc724726e042650ab982f 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 05f8ce798326cf8dfee18a2f3a9a6fae1adaeed2..99825e950a0a12ead9f7a187f5f8a5f1ce9df600 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 c4a4bdab83a92d011d185f977190727eff6e63ae..14df637b262065b4d72774cf1cdc62303f4e7164 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 0000000000000000000000000000000000000000..f26e7c2451b1ec6aca24d9d243c5473ab200d598 --- /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 efeeca17182f571065e5ec9328a02c27387c4a31..48edc767a132895becd6eaeacf25ca5e9ec1108e 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 a4de68053e0a1b9004b50ad5891538eaaf7ade76..0000000000000000000000000000000000000000 --- 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 53169150c3a6860225e89d93afa6523105d855b8..5435b2370c57ac8cda957481cc8c7ecef40c19e3 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 70b256f03b628cd8665b2bf11d29c07866aa4b6d..ec9e85c7af68aa0584f52506ff048f7dd91f61ea 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 81fe5701518e4bfa9d354fd39fe6a6abd611327f..709bd8ef56d10be6ec76452d90217b231ae8df49 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 a7ab4f386e870a93fdad097bc27a989d63653b10..f355811aaca3d55b46e6ab0119379681dec094aa 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 61abbc926b6dd3100bfa7ddcf69322a0104ee409..8005a74f2540a7534cfc8aec9fff11fa80442c2b 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)