Merge pull request #23283 from edx/ndalfonso/AA-12-reset-deadlines
AA-12 reset self paced due dates
This commit is contained in:
@@ -110,7 +110,8 @@ from openedx.features.course_duration_limits.access import generate_course_expir
|
||||
from openedx.features.course_experience import (
|
||||
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG,
|
||||
UNIFIED_COURSE_TAB_FLAG,
|
||||
course_home_url_name
|
||||
course_home_url_name,
|
||||
RELATIVE_DATES_FLAG,
|
||||
)
|
||||
from openedx.features.course_experience.course_tools import CourseToolsPluginManager
|
||||
from openedx.features.course_experience.views.course_dates import CourseDatesFragmentView
|
||||
@@ -728,6 +729,10 @@ class CourseTabView(EdxFragmentView):
|
||||
else:
|
||||
masquerade = None
|
||||
|
||||
reset_deadlines_url = reverse(
|
||||
'openedx.course_experience.reset_course_deadlines', kwargs={'course_id': text_type(course.id)}
|
||||
)
|
||||
|
||||
context = {
|
||||
'course': course,
|
||||
'tab': tab,
|
||||
@@ -738,6 +743,8 @@ class CourseTabView(EdxFragmentView):
|
||||
'uses_bootstrap': uses_bootstrap,
|
||||
'uses_pattern_library': not uses_bootstrap,
|
||||
'disable_courseware_js': True,
|
||||
'relative_dates_is_enabled': RELATIVE_DATES_FLAG.is_enabled(course.id),
|
||||
'reset_deadlines_url': reset_deadlines_url,
|
||||
}
|
||||
# Avoid Multiple Mathjax loading on the 'user_profile'
|
||||
if 'profile_page_context' in kwargs:
|
||||
|
||||
@@ -15,6 +15,7 @@ $static-path: '../..';
|
||||
@import 'layouts';
|
||||
@import 'components';
|
||||
@import 'course/layout/courseware_preview';
|
||||
@import 'course/layout/reset_deadlines';
|
||||
@import 'shared/modal';
|
||||
@import 'shared/help-tab';
|
||||
|
||||
|
||||
23
lms/static/sass/course/layout/_reset_deadlines.scss
Normal file
23
lms/static/sass/course/layout/_reset_deadlines.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
div.reset-deadlines-banner {
|
||||
background-color: theme-color("primary");
|
||||
display: none;
|
||||
flex-wrap: wrap;
|
||||
padding: 15px 20px;
|
||||
margin-top: 5px;
|
||||
|
||||
div,
|
||||
button {
|
||||
flex: 0 0 auto;
|
||||
|
||||
&.reset-deadlines-text {
|
||||
color: theme-color("inverse");
|
||||
padding-top: 2px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&.reset-deadlines-button {
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,7 @@
|
||||
@include box-sizing(border-box);
|
||||
|
||||
margin: 0 auto;
|
||||
padding: ($baseline*0.75) ($baseline*2);
|
||||
padding: ($baseline*0.75) 20px;
|
||||
background-color: theme-color("primary");
|
||||
|
||||
@media print {
|
||||
|
||||
@@ -184,6 +184,10 @@ from pipeline_mako import render_require_js_path_overrides
|
||||
<%include file="/preview_menu.html" />
|
||||
% endif
|
||||
|
||||
% if course and course.self_paced and tab and relative_dates_is_enabled:
|
||||
<%include file="/reset_deadlines_banner.html" />
|
||||
% endif
|
||||
|
||||
<%include file="/page_banner.html" />
|
||||
|
||||
<div class="marketing-hero"><%block name="marketing_hero"></%block></div>
|
||||
|
||||
13
lms/templates/reset_deadlines_banner.html
Normal file
13
lms/templates/reset_deadlines_banner.html
Normal file
@@ -0,0 +1,13 @@
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
<div class="reset-deadlines-banner">
|
||||
<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}">
|
||||
<button class="reset-deadlines-button">${_("Reset my deadlines")}</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.28 on 2020-03-03 18:36
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
import simple_history.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('schedules', '0012_auto_20200302_1914'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='HistoricalSchedule',
|
||||
fields=[
|
||||
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||
('active', models.BooleanField(default=True, help_text='Indicates if this schedule is actively used')),
|
||||
('start', models.DateTimeField(db_index=True, help_text='Date this schedule went into effect')),
|
||||
('start_date', models.DateTimeField(db_index=True, help_text='Date this schedule went into effect')),
|
||||
('upgrade_deadline', models.DateTimeField(blank=True, db_index=True, help_text='Deadline by which the learner must upgrade to a verified seat', null=True)),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField()),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('enrollment', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='student.CourseEnrollment')),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'get_latest_by': 'history_date',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'verbose_name': 'historical Schedule',
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
]
|
||||
@@ -6,6 +6,7 @@ from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from model_utils import Choices
|
||||
from model_utils.models import TimeStampedModel
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
|
||||
class Schedule(TimeStampedModel):
|
||||
@@ -33,6 +34,7 @@ class Schedule(TimeStampedModel):
|
||||
null=True,
|
||||
help_text=_('Deadline by which the learner must upgrade to a verified seat')
|
||||
)
|
||||
history = HistoricalRecords()
|
||||
|
||||
def get_experience_type(self):
|
||||
try:
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
|
||||
<%!
|
||||
import json
|
||||
from datetime import date
|
||||
import pytz
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
@@ -16,6 +18,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<%
|
||||
course_sections = blocks.get('children')
|
||||
self_paced = context.get('self_paced', False)
|
||||
reset_deadlines_banner_displayed = False
|
||||
%>
|
||||
<main role="main" class="course-outline" id="main" tabindex="-1">
|
||||
% if course_sections is not None:
|
||||
@@ -56,6 +59,12 @@ self_paced = context.get('self_paced', False)
|
||||
scored = 'scored' if subsection.get('scored', False) else ''
|
||||
graded = 'graded' if subsection.get('graded') else ''
|
||||
%>
|
||||
% if not subsection.get('complete', True) and subsection.get('due', timezone.now() + timedelta(1)) < timezone.now() and not reset_deadlines_banner_displayed:
|
||||
<% reset_deadlines_banner_displayed = True %>
|
||||
<script type="text/javascript">
|
||||
$('.reset-deadlines-banner').css('display', 'flex');
|
||||
</script>
|
||||
% endif
|
||||
<li class="subsection accordion ${ 'current' if subsection.get('resume_block') else '' } ${graded} ${scored}">
|
||||
<a
|
||||
% if enable_links:
|
||||
|
||||
@@ -51,6 +51,7 @@ from openedx.core.djangolib.markup import HTML
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
from openedx.features.course_experience import (
|
||||
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG,
|
||||
RELATIVE_DATES_FLAG,
|
||||
SHOW_REVIEWS_TOOL_FLAG,
|
||||
SHOW_UPGRADE_MSG_ON_COURSE_HOME,
|
||||
UNIFIED_COURSE_TAB_FLAG
|
||||
@@ -948,6 +949,7 @@ class CourseHomeFragmentViewTests(ModuleStoreTestCase):
|
||||
self.course = CourseFactory(
|
||||
start=now() - timedelta(days=30),
|
||||
end=end,
|
||||
self_paced=True,
|
||||
)
|
||||
self.url = course_home_url(self.course)
|
||||
|
||||
@@ -1020,3 +1022,8 @@ class CourseHomeFragmentViewTests(ModuleStoreTestCase):
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertContains(response, "<span>DISCOUNT_PRICE</span>")
|
||||
|
||||
@override_waffle_flag(RELATIVE_DATES_FLAG, active=True)
|
||||
def test_reset_deadline_banner_is_present_on_course_tab(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertContains(response, '<div class="reset-deadlines-banner">')
|
||||
|
||||
@@ -14,6 +14,7 @@ from completion.test_utils import CompletionWaffleTestMixin
|
||||
from django.contrib.sites.models import Site
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from mock import Mock, patch
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
@@ -25,6 +26,8 @@ from waffle.testutils import override_switch
|
||||
from lms.djangoapps.courseware.tests.factories import StaffFactory
|
||||
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.lib.gating import api as gating_api
|
||||
from openedx.features.course_experience.views.course_outline import (
|
||||
DEFAULT_COMPLETION_TRACKING_START,
|
||||
@@ -55,7 +58,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
|
||||
# pylint: disable=super-method-not-called
|
||||
with super(TestCourseOutlinePage, cls).setUpClassAndTestData():
|
||||
cls.courses = []
|
||||
course = CourseFactory.create()
|
||||
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)
|
||||
@@ -132,6 +135,18 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
|
||||
self.assertContains(response, sequential.format)
|
||||
self.assertTrue(sequential.children)
|
||||
|
||||
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
|
||||
)
|
||||
url = '{}{}'.format(course_home_url(course), 'reset_deadlines')
|
||||
self.client.post(url)
|
||||
updated_schedule = Schedule.objects.get(enrollment=enrollment)
|
||||
self.assertEqual(updated_schedule.start_date.day, datetime.datetime.today().day)
|
||||
|
||||
|
||||
class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
|
||||
"""
|
||||
|
||||
@@ -7,7 +7,7 @@ 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
|
||||
from .views.course_outline import CourseOutlineFragmentView, reset_course_deadlines
|
||||
from .views.course_reviews import CourseReviewsView
|
||||
from .views.course_sock import CourseSockFragmentView
|
||||
from .views.course_updates import CourseUpdatesFragmentView, CourseUpdatesView
|
||||
@@ -70,4 +70,9 @@ urlpatterns = [
|
||||
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',
|
||||
),
|
||||
]
|
||||
|
||||
@@ -5,11 +5,16 @@ Views to show a course outline.
|
||||
|
||||
import datetime
|
||||
import re
|
||||
import pytz
|
||||
import six
|
||||
|
||||
from completion import waffle as completion_waffle
|
||||
from django.contrib.auth.models import User
|
||||
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.views.decorators.csrf import ensure_csrf_cookie
|
||||
import edx_when.api as edx_when_api
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from pytz import UTC
|
||||
@@ -17,6 +22,7 @@ from waffle.models import Switch
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from lms.djangoapps.courseware.courses import get_course_overview_with_access
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from student.models import CourseEnrollment
|
||||
from util.milestones_helpers import get_course_content_milestones
|
||||
@@ -154,3 +160,19 @@ 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 = CourseOverview.objects.get(id=course_id)
|
||||
if course.self_paced:
|
||||
enrollment = CourseEnrollment.objects.get(user=request.user, course=course_id)
|
||||
schedule = enrollment.schedule
|
||||
if schedule:
|
||||
schedule.start_date = datetime.datetime.now(pytz.utc)
|
||||
schedule.save()
|
||||
return redirect(reverse('openedx.course_experience.course_home', args=[six.text_type(course_id)]))
|
||||
|
||||
Reference in New Issue
Block a user