Skip to content
Snippets Groups Projects
Unverified Commit 26f0176d authored by Nick's avatar Nick Committed by GitHub
Browse files

Merge pull request #23550 from edx/ndalfonso/AA-85-reset-dates-mobile

AA-85 mobile reset dates
parents 0a39e6db ac9d2bfd
No related branches found
No related tags found
No related merge requests found
Showing
with 245 additions and 69 deletions
import ast
import json
import logging
import sys
......@@ -10,10 +11,15 @@ import crum
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseServerError
from django.views.decorators.csrf import requires_csrf_token
from django.views.decorators.csrf import ensure_csrf_cookie, requires_csrf_token
from django.views.defaults import server_error
from django.shortcuts import redirect
from django.urls import reverse
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.masquerade import setup_masquerade
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule
from six.moves import map
import track.views
......@@ -187,3 +193,34 @@ def add_p3p_header(view_func):
response['P3P'] = settings.P3P_HEADER
return response
return inner
@ensure_csrf_cookie
def reset_course_deadlines(request):
"""
Set the start_date of a schedule to today, which in turn will adjust due dates for
sequentials belonging to a self paced course
"""
from lms.urls import RENDER_XBLOCK_NAME
from openedx.features.course_experience.urls import COURSE_HOME_VIEW_NAME
detail_id_dict = ast.literal_eval(request.POST.get('reset_deadlines_redirect_url_id_dict'))
print('***************', detail_id_dict)
redirect_url = request.POST.get('reset_deadlines_redirect_url_base', COURSE_HOME_VIEW_NAME)
course_key = CourseKey.from_string(detail_id_dict['course_id'])
masquerade_details, masquerade_user = setup_masquerade(
request,
course_key,
has_access(request.user, 'staff', course_key)
)
if masquerade_details and masquerade_details.role == 'student' and masquerade_details.user_name and (
redirect_url == COURSE_HOME_VIEW_NAME
):
# Masquerading as a specific student, so reset that student's schedule
user = masquerade_user
else:
user = request.user
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))
......@@ -51,7 +51,9 @@ from openedx.features.course_experience import (
default_course_url_name,
RELATIVE_DATES_FLAG,
)
from openedx.features.course_experience.urls import COURSE_HOME_VIEW_NAME
from openedx.features.course_experience.utils import get_course_outline_block_tree
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 shoppingcart.models import CourseRegistrationCode
......@@ -450,6 +452,8 @@ 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)})
show_search = (
......@@ -458,31 +462,14 @@ class CoursewareIndex(View):
)
staff_access = self.is_staff
reset_deadlines_url = reverse(
'openedx.course_experience.reset_course_deadlines', kwargs={'course_id': six.text_type(self.course.id)}
)
allow_anonymous = allow_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): # pylint: disable=too-many-nested-blocks
course_overview = CourseOverview.objects.get(id=str(self.course_key))
end_date = getattr(course_overview, 'end_date')
if course_overview.self_paced and (not end_date or timezone.now() < end_date):
if (CourseEnrollment.objects.filter(
course=course_overview, user=request.user, mode=CourseMode.VERIFIED
).exists()):
course_block_tree = get_course_outline_block_tree(
request, str(self.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('due', timezone.now() + timedelta(1)) < timezone.now()):
display_reset_dates_banner = True
break
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'],
......@@ -506,8 +493,10 @@ class CoursewareIndex(View):
'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),
'reset_deadlines_url': reset_deadlines_url,
'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(
......
......@@ -115,6 +115,7 @@ from openedx.features.course_experience import (
RELATIVE_DATES_FLAG,
)
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.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
......@@ -717,6 +718,9 @@ class CourseTabView(EdxFragmentView):
"""
Creates the context for the fragment's template.
"""
from lms.urls import RESET_COURSE_DEADLINES_NAME
from openedx.features.course_experience.urls import COURSE_HOME_VIEW_NAME
can_masquerade = request.user.has_perm(MASQUERADE_AS_STUDENT, course)
supports_preview_menu = tab.get('supports_preview_menu', False)
uses_bootstrap = self.uses_bootstrap(request, course, tab=tab)
......@@ -731,10 +735,6 @@ class CourseTabView(EdxFragmentView):
else:
masquerade = None
reset_deadlines_url = reverse(
'openedx.course_experience.reset_course_deadlines', kwargs={'course_id': text_type(course.id)}
)
display_reset_dates_banner = False
if RELATIVE_DATES_FLAG.is_enabled(course.id):
course_overview = CourseOverview.get_from_id(course.id)
......@@ -744,6 +744,10 @@ class CourseTabView(EdxFragmentView):
).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,
......@@ -754,8 +758,10 @@ class CourseTabView(EdxFragmentView):
'uses_bootstrap': uses_bootstrap,
'uses_pattern_library': not uses_bootstrap,
'disable_courseware_js': True,
'reset_deadlines_url': reset_deadlines_url,
'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:
......@@ -1614,11 +1620,14 @@ def _track_successful_certificate_generation(user_id, course_id):
@ensure_valid_usage_key
@xframe_options_exempt
@transaction.non_atomic_requests
@ensure_csrf_cookie
def render_xblock(request, usage_key_string, check_if_enrolled=True):
"""
Returns an HttpResponse with HTML content for the xBlock with the given usage_key.
The returned HTML is a chromeless rendering of the xBlock (excluding content of the containing courseware).
"""
from lms.urls import RENDER_XBLOCK_NAME, RESET_COURSE_DEADLINES_NAME
usage_key = UsageKey.from_string(usage_key_string)
usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
......@@ -1655,6 +1664,14 @@ 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)
reset_deadlines_url = reverse(RESET_COURSE_DEADLINES_NAME) if display_reset_dates_banner else None
reset_deadlines_redirect_url_base = RENDER_XBLOCK_NAME if reset_deadlines_url else None
context = {
'fragment': block.render('student_view', context=student_view_context),
'course': course,
......@@ -1667,6 +1684,10 @@ 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,
'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), 'usage_key_string': usage_key_string}
}
return render_to_response('courseware/courseware-chromeless.html', context)
......
......@@ -316,7 +316,7 @@ div.reset-deadlines-banner {
color: theme-color("inverse");
padding-top: 10px;
margin-right: 10px;
flex: 0 0 auto;
flex: 0 1 auto;
}
form {
......
......@@ -7,7 +7,7 @@ div.reset-deadlines-banner {
div,
button {
flex: 0 0 auto;
flex: 0 1 auto;
&.reset-deadlines-text {
color: theme-color("inverse");
......
......@@ -3,3 +3,28 @@
@import 'bourbon/bourbon';
@import 'vendor/bi-app/bi-app-rtl'; // set the layout for right to left languages
@import 'build-mobile';
.reset-deadlines-banner {
background-color: theme-color("primary");
display: flex;
flex-wrap: wrap;
padding: 15px 20px;
margin-top: 5px;
div,
button {
flex: 0 1 auto;
&.reset-deadlines-text {
color: theme-color("inverse");
padding-top: 10px;
margin-right: 10px;
}
&.reset-deadlines-button {
color: #0075b4;
background-color: theme-color("inverse");
cursor: pointer;
}
}
}
......@@ -3,3 +3,28 @@
@import 'bourbon/bourbon';
@import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages
@import 'build-mobile';
.reset-deadlines-banner {
background-color: theme-color("primary");
display: flex;
flex-wrap: wrap;
padding: 15px 20px;
margin-top: 5px;
div,
button {
flex: 0 1 auto;
&.reset-deadlines-text {
color: theme-color("inverse");
padding-top: 10px;
margin-right: 10px;
}
&.reset-deadlines-button {
color: #0075b4;
background-color: theme-color("inverse");
cursor: pointer;
}
}
}
......@@ -12,6 +12,13 @@ from openedx.core.djangolib.js_utils import js_escaped_string
<% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %>
</%def>
% if display_reset_dates_banner:
<%include file="/reset_deadlines_banner.html" />
<script type="text/javascript">
$('.reset-deadlines-banner').css('display', 'flex');
</script>
% endif
<%block name="bodyclass">view-in-course view-courseware courseware ${course.css_class or ''}</%block>
<%block name="title"><title>
% if section_title:
......
......@@ -8,6 +8,8 @@ from django.utils.translation import ugettext as _
<div class="reset-deadlines-text">${_("It looks like you've missed some important deadlines. Reset your deadlines and get started today.")}</div>
<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>
</div>
......@@ -55,6 +55,9 @@ from staticbook import views as staticbook_views
from student import views as student_views
from util import views as util_views
RESET_COURSE_DEADLINES_NAME = 'reset_course_deadlines'
RENDER_XBLOCK_NAME = 'render_xblock'
if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
django_autodiscover()
admin.site.site_header = _('LMS Administration')
......@@ -237,6 +240,7 @@ COURSE_URLS = [
name='registration_code_details',
),
]
urlpatterns += [
# jump_to URLs for direct access to a location in the course
url(
......@@ -291,7 +295,7 @@ urlpatterns += [
url(
r'^xblock/{usage_key_string}$'.format(usage_key_string=settings.USAGE_KEY_PATTERN),
courseware_views.render_xblock,
name='render_xblock',
name=RENDER_XBLOCK_NAME,
),
# xblock Resource URL
......@@ -316,6 +320,12 @@ urlpatterns += [
# TODO: These views need to be updated before they work
url(r'^calculate$', util_views.calculate),
url(
r'^reset_deadlines$',
util_views.reset_course_deadlines,
name=RESET_COURSE_DEADLINES_NAME,
),
url(r'^courses/?$', branding_views.courses, name='courses'),
# About the course
......
......@@ -4,6 +4,9 @@
<%namespace name="static" file="../../static_content.html"/>
% if display_reset_dates_banner:
<%include file="/reset_deadlines_banner.html" />
% endif
% for course_date in course_date_blocks:
<%include file="../dates-summary.html" args="course_date=course_date"/>
% endfor
......
......@@ -24,6 +24,7 @@ from waffle.models import Switch
from waffle.testutils import override_switch
from lms.djangoapps.courseware.tests.factories import StaffFactory
from lms.urls import RESET_COURSE_DEADLINES_NAME
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
......@@ -170,8 +171,8 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
start_date=timezone.now() - datetime.timedelta(1),
enrollment=enrollment
)
url = '{}{}'.format(course_home_url(course), 'reset_deadlines')
self.client.post(url)
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())
......@@ -204,8 +205,8 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
assert response.status_code == 200
url = '{}{}'.format(course_home_url(course), 'reset_deadlines')
self.client.post(url)
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(id=student_schedule.id)
self.assertEqual(updated_schedule.start_date.date(), datetime.datetime.today().date())
updated_staff_schedule = Schedule.objects.get(id=staff_schedule.id)
......@@ -240,8 +241,8 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
assert response.status_code == 200
url = '{}{}'.format(course_home_url(course), 'reset_deadlines')
self.client.post(url)
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_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)
......
......@@ -7,18 +7,21 @@ from django.conf.urls import url
from .views.course_dates import CourseDatesFragmentMobileView
from .views.course_home import CourseHomeFragmentView, CourseHomeView
from .views.course_outline import CourseOutlineFragmentView, reset_course_deadlines
from .views.course_outline import CourseOutlineFragmentView
from .views.course_reviews import CourseReviewsView
from .views.course_sock import CourseSockFragmentView
from .views.course_updates import CourseUpdatesFragmentView, CourseUpdatesView
from .views.latest_update import LatestUpdateFragmentView
from .views.welcome_message import WelcomeMessageFragmentView, dismiss_welcome_message
COURSE_HOME_VIEW_NAME = 'openedx.course_experience.course_home'
COURSE_DATES_FRAGMENT_VIEW_NAME = 'openedx.course_experience.mobile_dates_fragment_view'
urlpatterns = [
url(
r'^$',
CourseHomeView.as_view(),
name='openedx.course_experience.course_home',
name=COURSE_HOME_VIEW_NAME,
),
url(
r'^updates$',
......@@ -68,11 +71,6 @@ urlpatterns = [
url(
r'^mobile_dates_fragment',
CourseDatesFragmentMobileView.as_view(),
name='openedx.course_experience.mobile_dates_fragment_view',
),
url(
r'^reset_deadlines$',
reset_course_deadlines,
name='openedx.course_experience.reset_course_deadlines',
name=COURSE_DATES_FRAGMENT_VIEW_NAME,
),
]
......@@ -3,13 +3,20 @@ Common utilities for the course experience, including course outline.
"""
from datetime import timedelta
from completion.models import BlockCompletion
from django.utils import timezone
from opaque_keys.edx.keys import CourseKey
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.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 student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore
......@@ -232,3 +239,36 @@ def get_resume_block(block):
if resume_block:
return resume_block
return block
def reset_deadlines_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
"""
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, 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('due', timezone.now() + timedelta(1)) < timezone.now()):
display_reset_dates_banner = True
break
return display_reset_dates_banner
......@@ -5,9 +5,14 @@ Fragment for rendering the course dates sidebar.
from django.http import Http404
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
from django.utils.translation import get_language_bidi
from opaque_keys.edx.keys import CourseKey
from web_fragments.fragment import Fragment
from openedx.features.course_experience import RELATIVE_DATES_FLAG
from openedx.features.course_experience.utils import reset_deadlines_banner_should_display
from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
......@@ -49,10 +54,10 @@ class CourseDatesFragmentMobileView(CourseDatesFragmentView):
"""
template_name = 'course_experience/mobile/course-dates-fragment.html'
@method_decorator(ensure_csrf_cookie)
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated:
raise Http404
print('****************', CourseDatesFragmentMobileView.__dict__)
return super(CourseDatesFragmentMobileView, self).get(request, *args, **kwargs)
def css_dependencies(self):
......@@ -67,3 +72,37 @@ class CourseDatesFragmentMobileView(CourseDatesFragmentView):
return self.get_css_dependencies('style-mobile-rtl')
else:
return self.get_css_dependencies('style-mobile')
def render_to_fragment(self, request, course_id=None, **kwargs):
"""
Render the course dates fragment.
"""
from lms.urls import RESET_COURSE_DEADLINES_NAME
from openedx.features.course_experience.urls import COURSE_DATES_FRAGMENT_VIEW_NAME
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False)
course_date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=2)
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)
reset_deadlines_url = reverse(RESET_COURSE_DEADLINES_NAME) if display_reset_dates_banner else None
reset_deadlines_redirect_url_base = COURSE_DATES_FRAGMENT_VIEW_NAME if (
reset_deadlines_url) else None
context = {
'course_date_blocks': [block for block in course_date_blocks if block.title != 'current_datetime'],
'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': course_id}
}
html = render_to_string(self.template_name, context)
dates_fragment = Fragment(html)
self.add_fragment_resource_urls(dates_fragment)
return dates_fragment
......@@ -160,24 +160,3 @@ class CourseOutlineFragmentView(EdxFragmentView):
if children:
children[0]['resume_block'] = True
self.mark_first_unit_to_resume(children[0])
@ensure_csrf_cookie
def reset_course_deadlines(request, course_id):
"""
Set the start_date of a schedule to today, which in turn will adjust due dates for
sequentials belonging to a self paced course
"""
course_key = CourseKey.from_string(course_id)
masquerade_details, masquerade_user = setup_masquerade(
request,
course_key,
has_access(request.user, 'staff', course_key)
)
if masquerade_details and masquerade_details.role == 'student' and masquerade_details.user_name:
# Masquerading as a specific student, so reset that student's schedule
user = masquerade_user
else:
user = request.user
reset_self_paced_schedule(user, course_key)
return redirect(reverse('openedx.course_experience.course_home', args=[six.text_type(course_key)]))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment