Merge pull request #23283 from edx/ndalfonso/AA-12-reset-deadlines

AA-12 reset self paced due dates
This commit is contained in:
Nick
2020-03-04 15:42:46 -05:00
committed by GitHub
13 changed files with 158 additions and 5 deletions

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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',
),
]

View File

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