Merge pull request #24142 from cpennington/allow-reset-for-visible-missed-dates

Allow reset for visible missed dates
This commit is contained in:
Calen Pennington
2020-06-10 15:48:57 -04:00
committed by GitHub
12 changed files with 98 additions and 86 deletions

View File

@@ -21,6 +21,7 @@ from six.moves import map
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.masquerade import setup_masquerade
from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule
from openedx.features.course_experience.utils import dates_banner_should_display
import track.views
from edxmako.shortcuts import render_to_response
@@ -218,7 +219,10 @@ def reset_course_deadlines(request):
user = masquerade_user
else:
user = request.user
reset_self_paced_schedule(user, course_key)
missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, user)
if missed_deadlines and not missed_gated_content:
reset_self_paced_schedule(user, course_key)
if redirect_url == RENDER_XBLOCK_NAME:
detail_id_dict.pop('course_id')
return redirect(reverse(redirect_url, kwargs=detail_id_dict))

View File

@@ -38,7 +38,8 @@ class DatesTabSerializer(serializers.Serializer):
Serializer for the Dates Tab
"""
course_date_blocks = DateSummarySerializer(many=True)
display_reset_dates_text = serializers.BooleanField()
missed_deadlines = serializers.BooleanField()
missed_gated_content = serializers.BooleanField()
learner_is_full_access = serializers.BooleanField()
user_timezone = serializers.CharField()
verified_upgrade_link = serializers.URLField()

View File

@@ -43,8 +43,8 @@ class DatesTabView(RetrieveAPIView):
link: (str) An absolute link to content related to the date event
(ex. verified link or link to assignment)
title: (str) The title of the date event
display_reset_dates_text: (bool) Indicates whether the reset dates banner should be shown
for the given user
missed_deadlines: (bool) Indicates whether the user missed any graded content deadlines
missed_gated_content: (bool) Indicates whether the user missed gated content
learner_is_full_access: (bool) Indicates if the user is verified in the course
user_timezone: (str) The user's preferred timezone
verified_upgrade_link: (str) The link for upgrading to the Verified track in a course
@@ -73,7 +73,7 @@ class DatesTabView(RetrieveAPIView):
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 = dates_banner_should_display(course_key, request)
missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user)
learner_is_full_access = not ContentTypeGatingConfig.enabled_for_enrollment(
user=request.user,
@@ -86,7 +86,8 @@ class DatesTabView(RetrieveAPIView):
data = {
'course_date_blocks': [block for block in blocks if not isinstance(block, TodaysDate)],
'display_reset_dates_text': display_reset_dates_text,
'missed_deadlines': missed_deadlines,
'missed_gated_content': missed_gated_content,
'learner_is_full_access': learner_is_full_access,
'user_timezone': user_timezone,
'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course),

View File

@@ -1060,7 +1060,7 @@ def dates(request, course_id):
user_timezone = user_timezone_locale['user_timezone']
user_language = user_timezone_locale['user_language']
missed_deadlines = dates_banner_should_display(course_key, request)
missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user)
context = {
'course': course,
@@ -1078,6 +1078,7 @@ def dates(request, course_id):
course_key=course_key,
),
'missed_deadlines': missed_deadlines,
'missed_gated_content': missed_gated_content,
'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)}
@@ -1663,7 +1664,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()
}
missed_deadlines = dates_banner_should_display(course_key, request)
missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user)
context = {
'fragment': block.render('student_view', context=student_view_context),
@@ -1678,12 +1679,9 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
'staff_access': bool(request.user.has_perm(VIEW_XQA_INTERFACE, course)),
'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'),
'missed_deadlines': missed_deadlines,
'missed_gated_content': missed_gated_content,
'web_app_course_url': reverse(COURSE_HOME_VIEW_NAME, args=[course.id]),
'on_courseware_page': True,
'content_type_gating_enabled': ContentTypeGatingConfig.enabled_for_enrollment(
user=request.user,
course_key=course_key,
),
'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course),
'is_learning_mfe': request.META.get('HTTP_REFERER', '').startswith(settings.LEARNING_MICROFRONTEND_URL),
'is_mobile_app': is_request_from_mobile_app(request),

View File

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

View File

@@ -96,7 +96,7 @@ additional_styling_class = 'on-mobile' if is_mobile_app else 'has-button'
% if on_dates_tab and content_type_gating_enabled and not missed_deadlines:
${upgrade_to_complete_graded_banner()}
% elif missed_deadlines:
% if content_type_gating_enabled:
% if missed_gated_content:
${upgrade_to_reset_banner()}
% else:
${reset_dates_banner()}

View File

@@ -482,7 +482,7 @@ class BlockStructureBlockData(BlockStructure):
override_data (object) - The data you want to set
"""
block_data = self._block_data_map.get(usage_key)
block_data = self._get_or_create_block(usage_key)
setattr(block_data, field_name, override_data)
def get_transformer_data(self, transformer, key, default=None):

View File

@@ -26,11 +26,7 @@ is_course_staff = bool(user and course and has_access(user, 'staff', course, cou
dates_banner_displayed = False
%>
<main role="main" class="course-outline" id="main" tabindex="-1">
<div class="dates-banner-wrapper">
% if relative_dates_flag_is_enabled and self_paced and not is_course_staff:
<%include file="/dates_banner.html" />
% endif
</div>
<%include file="/dates_banner.html" />
% if course_sections is not None:
<button class="btn btn-primary"
id="expand-collapse-outline-all-button"
@@ -69,15 +65,7 @@ dates_banner_displayed = False
scored = 'scored' if subsection.get('scored', False) else ''
graded = 'graded' if subsection.get('graded') else ''
num_graded_problems = subsection.get('num_graded_problems', 0)
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 dates_banner_displayed:
<% dates_banner_displayed = True %>
<script type="text/javascript">
$('.dates-banner-wrapper').css('display', 'block');
</script>
% endif
<li class="subsection accordion ${ 'current' if subsection.get('resume_block') else '' } ${graded} ${scored}">
<a
% if enable_links:

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(74, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with check_mongo_calls(4):
url = course_home_url(self.course)
self.client.get(url)

View File

@@ -13,7 +13,7 @@ from completion import waffle
from completion.models import BlockCompletion
from completion.test_utils import CompletionWaffleTestMixin
from django.contrib.sites.models import Site
from django.test import override_settings
from django.test import override_settings, RequestFactory
from django.urls import reverse
from django.utils import timezone
from milestones.tests.utils import MilestonesTestCaseMixin
@@ -32,6 +32,7 @@ from gating import api as lms_gating_api
from lms.djangoapps.course_api.blocks.transformers.milestones import MilestonesAndSpecialExamsTransformer
from openedx.core.djangoapps.schedules.models import Schedule
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
from openedx.core.djangoapps.course_date_signals.models import SelfPacedRelativeDatesConfig
from openedx.core.lib.gating import api as gating_api
from openedx.features.course_experience import RELATIVE_DATES_FLAG
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
@@ -45,6 +46,8 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from ...utils import get_course_outline_block_tree
from .test_course_home import course_home_url
TEST_PASSWORD = 'test'
@@ -56,11 +59,16 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
"""
Test the course outline view.
"""
ENABLED_SIGNALS = ['course_published']
@classmethod
def setUpClass(cls):
"""
Set up an array of various courses to be tested.
"""
SelfPacedRelativeDatesConfig.objects.create(enabled=True)
# setUpClassAndTestData() already calls setUpClass on SharedModuleStoreTestCase
# pylint: disable=super-method-not-called
with super(TestCourseOutlinePage, cls).setUpClassAndTestData():
@@ -68,11 +76,13 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
course = CourseFactory.create(self_paced=True)
with cls.store.bulk_operations(course.id):
chapter = ItemFactory.create(category='chapter', parent_location=course.location)
sequential = ItemFactory.create(category='sequential', parent_location=chapter.location)
sequential = ItemFactory.create(category='sequential', parent_location=chapter.location, graded=True, format="Homework")
vertical = ItemFactory.create(category='vertical', parent_location=sequential.location)
problem = ItemFactory.create(category='problem', parent_location=vertical.location)
course.children = [chapter]
chapter.children = [sequential]
sequential.children = [vertical]
vertical.children = [problem]
cls.courses.append(course)
course = CourseFactory.create()
@@ -117,7 +127,11 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
"""Set up and enroll our fake user in the course."""
cls.user = UserFactory(password=TEST_PASSWORD)
for course in cls.courses:
CourseEnrollment.enroll(cls.user, course.id)
enrollment = CourseEnrollment.enroll(cls.user, course.id)
ScheduleFactory.create(
start_date=timezone.now() - datetime.timedelta(days=1),
enrollment=enrollment
)
def setUp(self):
"""
@@ -126,21 +140,32 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
super(TestCourseOutlinePage, self).setUp()
self.client.login(username=self.user.username, password=TEST_PASSWORD)
@RELATIVE_DATES_FLAG.override(active=True)
def test_outline_details(self):
for course in self.courses:
url = course_home_url(course)
request_factory = RequestFactory()
request = request_factory.get(url)
request.user = self.user
course_block_tree = get_course_outline_block_tree(
request, str(course.id), self.user
)
response = self.client.get(url)
self.assertTrue(course.children)
for chapter in course.children:
self.assertContains(response, chapter.display_name)
self.assertTrue(chapter.children)
for sequential in chapter.children:
self.assertContains(response, sequential.display_name)
if sequential.graded:
self.assertContains(response, sequential.due.strftime(u'%Y-%m-%d %H:%M:%S'))
self.assertContains(response, sequential.format)
self.assertTrue(sequential.children)
for chapter in course_block_tree['children']:
self.assertContains(response, chapter['display_name'])
self.assertTrue(chapter['children'])
for sequential in chapter['children']:
self.assertContains(response, sequential['display_name'])
if sequential['graded']:
print(sequential)
self.assertContains(response, sequential['due'].strftime(u'%Y-%m-%d %H:%M:%S'))
self.assertContains(response, sequential['format'])
self.assertTrue(sequential['children'])
def test_num_graded_problems(self):
course = CourseFactory.create()
@@ -188,7 +213,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
):
ContentTypeGatingConfig.objects.create(
enabled=True,
enabled_as_of=datetime.datetime(2018, 1, 1),
enabled_as_of=datetime.datetime(2017, 1, 1),
)
course = self.courses[0]
for mode in course_modes:
@@ -197,6 +222,8 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
enrollment = CourseEnrollment.objects.get(course_id=course.id, user=self.user)
enrollment.mode = enrollment_mode
enrollment.save()
enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=30)
enrollment.schedule.save()
self.user.is_staff = is_course_staff
self.user.save()
@@ -208,28 +235,21 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
else:
self.assertNotContains(response, '<div class="dates-banner-text"')
@RELATIVE_DATES_FLAG.override(active=True)
def test_reset_course_deadlines(self):
course = self.courses[0]
enrollment = CourseEnrollment.objects.get(course_id=course.id)
ScheduleFactory(
start_date=timezone.now() - datetime.timedelta(1),
enrollment=enrollment
)
enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=30)
enrollment.schedule.save()
post_dict = {'reset_deadlines_redirect_url_id_dict': json.dumps({'course_id': str(course.id)})}
self.client.post(reverse(RESET_COURSE_DEADLINES_NAME), post_dict)
updated_schedule = Schedule.objects.get(enrollment=enrollment)
self.assertEqual(updated_schedule.start_date.date(), datetime.datetime.today().date())
def test_reset_course_deadlines_masquerade_specific_student(self):
course = self.courses[0]
student_schedule = ScheduleFactory(
start_date=timezone.now() - datetime.timedelta(1),
enrollment=CourseEnrollment.objects.get(course_id=course.id, user=self.user),
)
student_schedule = CourseEnrollment.objects.get(course_id=course.id, user=self.user).schedule
student_schedule.start_date = timezone.now() - datetime.timedelta(days=30)
student_schedule.save()
staff = StaffFactory(course_key=course.id)
staff_schedule = ScheduleFactory(
start_date=timezone.now() - datetime.timedelta(1),
start_date=timezone.now() - datetime.timedelta(days=30),
enrollment__course__id=course.id,
enrollment__user=staff,
)
@@ -256,16 +276,17 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
updated_staff_schedule = Schedule.objects.get(id=staff_schedule.id)
self.assertEqual(updated_staff_schedule.start_date, staff_schedule.start_date)
@RELATIVE_DATES_FLAG.override(active=True)
def test_reset_course_deadlines_masquerade_generic_student(self):
course = self.courses[0]
student_schedule = ScheduleFactory(
start_date=timezone.now() - datetime.timedelta(1),
enrollment=CourseEnrollment.objects.get(course_id=course.id, user=self.user),
)
student_schedule = CourseEnrollment.objects.get(course_id=course.id, user=self.user).schedule
student_schedule.start_date = timezone.now() - datetime.timedelta(days=30)
student_schedule.save()
staff = StaffFactory(course_key=course.id)
staff_schedule = ScheduleFactory(
start_date=timezone.now() - datetime.timedelta(1),
start_date=timezone.now() - datetime.timedelta(days=30),
enrollment__course__id=course.id,
enrollment__user=staff,
)
@@ -290,7 +311,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
updated_student_schedule = Schedule.objects.get(id=student_schedule.id)
self.assertEqual(updated_student_schedule.start_date, student_schedule.start_date)
updated_staff_schedule = Schedule.objects.get(id=staff_schedule.id)
self.assertEqual(updated_staff_schedule.start_date.date(), datetime.datetime.today().date())
self.assertEqual(updated_staff_schedule.start_date.date(), datetime.date.today())
class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, MilestonesTestCaseMixin):

View File

@@ -202,7 +202,7 @@ def get_course_outline_block_tree(request, course_id, user=None, allow_start_dat
all_blocks = get_blocks(
request,
course_usage_key,
user=request.user,
user=user,
nav_depth=3,
requested_fields=[
'children',
@@ -232,7 +232,7 @@ def get_course_outline_block_tree(request, course_id, user=None, allow_start_dat
set_last_accessed_default(course_outline_root_block)
mark_blocks_completed(
block=course_outline_root_block,
user=request.user,
user=user,
course_key=course_key
)
return course_outline_root_block
@@ -255,15 +255,20 @@ def get_resume_block(block):
return block
def dates_banner_should_display(course_key, request):
def dates_banner_should_display(course_key, user):
"""
Return whether or not the reset banner should display,
determined by whether or not a course has any past-due,
incomplete sequentials and which enrollment mode is being
dealt with for the current user and course.
Returns:
(missed_deadlines, missed_gated_content):
missed_deadlines is True if the user has missed any graded content deadlines
missed_gated_content is True if the first content that the user missed was gated content
"""
if not RELATIVE_DATES_FLAG.is_enabled(str(course_key)):
return False
if not RELATIVE_DATES_FLAG.is_enabled(course_key):
return False, False
course_overview = CourseOverview.objects.get(id=str(course_key))
course_end_date = getattr(course_overview, 'end_date', None)
@@ -271,26 +276,26 @@ def dates_banner_should_display(course_key, request):
# Only display the banner for self-paced courses
if not is_self_paced:
return False
return False, False
# Only display the banner for enrolled users
if not CourseEnrollment.is_enrolled(request.user, course_key):
return False
if not CourseEnrollment.is_enrolled(user, course_key):
return False, False
# Don't display the banner for course staff
is_course_staff = bool(
request.user and course_overview and has_access(request.user, 'staff', course_overview, course_overview.id)
user and course_overview and has_access(user, 'staff', course_overview, course_overview.id)
)
if is_course_staff:
return False
return False, False
# Don't display the banner if the course has ended
if course_end_date and course_end_date < timezone.now():
return False
return False, False
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)
block_data = get_course_blocks(user, course_usage_key, include_completion=True)
for section_key in block_data.get_children(course_usage_key):
for subsection_key in block_data.get_children(section_key):
subsection_due_date = block_data.get_xblock_field(subsection_key, 'due', None)
@@ -301,7 +306,7 @@ def dates_banner_should_display(course_key, request):
):
# Display the banner if the due date for an incomplete graded subsection
# has passed
return True
return True, block_data.get_xblock_field(subsection_key, 'contains_gated_content', False)
# Don't display the banner if there were no missed deadlines
return False
return False, False

View File

@@ -31,6 +31,7 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
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 openedx.features.course_experience.utils import dates_banner_should_display
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from student.models import CourseEnrollment
from util.milestones_helpers import get_course_content_milestones
@@ -83,6 +84,8 @@ class CourseOutlineFragmentView(EdxFragmentView):
xblock_display_names = self.create_xblock_id_and_name_dict(course_block_tree)
gated_content = self.get_content_milestones(request, course_key)
missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user)
context['gated_content'] = gated_content
context['xblock_display_names'] = xblock_display_names
@@ -102,13 +105,8 @@ class CourseOutlineFragmentView(EdxFragmentView):
context['reset_deadlines_redirect_url_id_dict'] = {'course_id': str(course.id)}
context['verified_upgrade_link'] = verified_upgrade_deadline_link(request.user, course=course)
context['on_course_outline_page'] = True
context['content_type_gating_enabled'] = ContentTypeGatingConfig.enabled_for_enrollment(
user=request.user,
course_key=course_key
)
# We use javascript to check whether to actually display this banner, so we let the banner assume
# that deadlines have been missed.
context['missed_deadlines'] = True
context['missed_deadlines'] = missed_deadlines
context['missed_gated_content'] = missed_gated_content
html = render_to_string('course_experience/course-outline-fragment.html', context)
return Fragment(html)