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:
@@ -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}">
|
||||
|
||||
@@ -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">')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user