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
This commit is contained in:
Nicholas D'Alfonso
2020-05-14 19:04:35 -04:00
parent 1e103b2fdd
commit b454f9be1d
20 changed files with 321 additions and 254 deletions

View File

@@ -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)

View File

@@ -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):
"""

View File

@@ -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(

View File

@@ -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)

View File

@@ -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;
.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;
div.reset-deadlines-text {
color: theme-color("inverse");
margin: 10px 10px 10px 0;
flex: 0 1 auto;
.dates-banner-text {
font-size: 16px;
line-height: 24px;
color: #414141;
a {
color: #fff;
text-decoration: underline;
a.mobile-dates-link {
color: #0075b4;
}
}
}
form {
button {
color: #0075b4;
background-color: theme-color("inverse");
cursor: pointer;
&.has-button {
.dates-banner-text {
flex: 1 1 20em;
max-width: 70%;
}
}
&.on-mobile {
margin-left: 20px;
margin-right: 20px;
}
}
}

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -319,6 +319,10 @@
// Course outline for visual progress waffle switch
.course-outline {
.dates-banner-wrapper {
display: none;
}
.block-tree {
margin: 0;
padding: 0;

View File

@@ -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:

View File

@@ -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. Dont worry—you wont lose any of the progress youve 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) %>

View File

@@ -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>.
${_(' Dont worry—you wont lose any of the progress youve 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. Dont worry—you wont lose any of the progress youve 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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}">

View File

@@ -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">')

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)