Merge pull request #26663 from edx/mikix/enable-schedules
feat: turn on schedule creation by default
This commit is contained in:
@@ -45,7 +45,7 @@ class CourseQualityViewTest(BaseCourseViewTest):
|
||||
'number_with_highlights': 0,
|
||||
'total_visible': 1,
|
||||
'total_number': 1,
|
||||
'highlights_enabled': False,
|
||||
'highlights_enabled': True,
|
||||
'highlights_active_for_course': False,
|
||||
},
|
||||
'subsections': {
|
||||
|
||||
@@ -9,7 +9,6 @@ from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
from scipy import stats
|
||||
|
||||
from cms.djangoapps.contentstore.views.item import highlights_setting
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
from openedx.core.lib.graph_traversals import traverse_pre_order
|
||||
@@ -149,7 +148,7 @@ class CourseQualityView(DeveloperErrorViewMixin, GenericAPIView):
|
||||
total_visible=len(visible_sections),
|
||||
number_with_highlights=len(sections_with_highlights),
|
||||
highlights_active_for_course=course.highlights_enabled_for_messaging,
|
||||
highlights_enabled=highlights_setting.is_enabled(),
|
||||
highlights_enabled=True, # used to be controlled by a waffle switch, now just always enabled
|
||||
)
|
||||
|
||||
def _subsections_quality(self, course, request): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Views for items (modules)."""
|
||||
|
||||
|
||||
import hashlib # lint-amnesty, pylint: disable=unused-import
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
@@ -21,7 +20,6 @@ from edx_proctoring.api import (
|
||||
get_exam_configuration_dashboard_url
|
||||
)
|
||||
from edx_proctoring.exceptions import ProctoredExamNotFoundException
|
||||
from edx_toggles.toggles import LegacyWaffleSwitch
|
||||
from help_tokens.core import HelpUrlExpert
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import LibraryUsageLocator
|
||||
@@ -36,7 +34,6 @@ from cms.djangoapps.models.settings.course_grading import CourseGradingModel
|
||||
from cms.djangoapps.xblock_config.models import CourseEditLTIFieldsEnabledFlag
|
||||
from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW
|
||||
from common.djangoapps.edxmako.shortcuts import render_to_string
|
||||
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
|
||||
from openedx.core.lib.gating import api as gating_api
|
||||
from openedx.core.lib.xblock_utils import hash_resource, request_token, wrap_xblock, wrap_xblock_aside
|
||||
from openedx.core.djangoapps.bookmarks import api as bookmarks_api
|
||||
@@ -92,9 +89,6 @@ NEVER = lambda x: False
|
||||
ALWAYS = lambda x: True
|
||||
|
||||
|
||||
highlights_setting = LegacyWaffleSwitch('dynamic_pacing', 'studio_course_update', __name__)
|
||||
|
||||
|
||||
def _filter_entrance_exam_grader(graders):
|
||||
"""
|
||||
If the entrance exams feature is enabled we need to hide away the grader from
|
||||
@@ -1262,8 +1256,8 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
'highlights_enabled_for_messaging': course.highlights_enabled_for_messaging,
|
||||
})
|
||||
xblock_info.update({
|
||||
'highlights_enabled': highlights_setting.is_enabled(),
|
||||
'highlights_preview_only': not COURSE_UPDATE_WAFFLE_FLAG.is_enabled(course.id),
|
||||
'highlights_enabled': True, # used to be controlled by a waffle switch, now just always enabled
|
||||
'highlights_preview_only': False, # used to be controlled by a waffle flag, now just always disabled
|
||||
'highlights_doc_url': HelpUrlExpert.the_one().url_for_token('content_highlights'),
|
||||
})
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse
|
||||
from edx_proctoring.exceptions import ProctoredExamNotFoundException
|
||||
from edx_toggles.toggles.testutils import override_waffle_switch
|
||||
from mock import Mock, PropertyMock, patch
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.asides import AsideUsageKeyV2
|
||||
@@ -66,7 +65,6 @@ from ..item import (
|
||||
_xblock_type_and_display_name,
|
||||
add_container_page_publishing_info,
|
||||
create_xblock_info,
|
||||
highlights_setting
|
||||
)
|
||||
|
||||
|
||||
@@ -2696,12 +2694,8 @@ class TestXBlockInfo(ItemTest):
|
||||
def test_highlights_enabled(self):
|
||||
self.course.highlights_enabled_for_messaging = True
|
||||
self.store.update_item(self.course, None)
|
||||
chapter = self.store.get_item(self.chapter.location)
|
||||
with override_waffle_switch(highlights_setting, active=True):
|
||||
chapter_xblock_info = create_xblock_info(chapter)
|
||||
course_xblock_info = create_xblock_info(self.course)
|
||||
self.assertTrue(chapter_xblock_info['highlights_enabled'])
|
||||
self.assertTrue(course_xblock_info['highlights_enabled_for_messaging'])
|
||||
course_xblock_info = create_xblock_info(self.course)
|
||||
self.assertTrue(course_xblock_info['highlights_enabled_for_messaging'])
|
||||
|
||||
def validate_course_xblock_info(self, xblock_info, has_child_info=True, course_outline=False):
|
||||
"""
|
||||
@@ -2731,7 +2725,7 @@ class TestXBlockInfo(ItemTest):
|
||||
self.assertEqual(xblock_info['due'], None)
|
||||
self.assertEqual(xblock_info['format'], None)
|
||||
self.assertEqual(xblock_info['highlights'], self.chapter.highlights)
|
||||
self.assertFalse(xblock_info['highlights_enabled'])
|
||||
self.assertTrue(xblock_info['highlights_enabled'])
|
||||
|
||||
# Finally, validate the entire response for consistency
|
||||
self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info)
|
||||
|
||||
@@ -573,8 +573,7 @@ describe('CourseOutlinePage', function() {
|
||||
});
|
||||
|
||||
describe('Content Highlights', function() {
|
||||
var createCourse, createCourseWithHighlights, createCourseWithHighlightsDisabled,
|
||||
clickSaveOnModal, clickCancelOnModal;
|
||||
let createCourse, createCourseWithHighlights, clickSaveOnModal, clickCancelOnModal;
|
||||
|
||||
beforeEach(function() {
|
||||
setSelfPaced();
|
||||
@@ -592,11 +591,6 @@ describe('CourseOutlinePage', function() {
|
||||
createCourse({highlights: highlights});
|
||||
};
|
||||
|
||||
createCourseWithHighlightsDisabled = function() {
|
||||
var highlightsDisabled = {highlights_enabled: false};
|
||||
createCourse(highlightsDisabled, highlightsDisabled);
|
||||
};
|
||||
|
||||
clickSaveOnModal = function() {
|
||||
$('.wrapper-modal-window .action-save').click();
|
||||
};
|
||||
@@ -643,11 +637,6 @@ describe('CourseOutlinePage', function() {
|
||||
$('button.status-highlights-enabled-value').click();
|
||||
};
|
||||
|
||||
it('does not display settings when disabled', function() {
|
||||
createCourseWithHighlightsDisabled();
|
||||
expect(highlightsSetting()).not.toExist();
|
||||
});
|
||||
|
||||
it('displays settings when enabled', function() {
|
||||
createCourseWithHighlights([]);
|
||||
expect(highlightsSetting()).toExist();
|
||||
@@ -778,11 +767,6 @@ describe('CourseOutlinePage', function() {
|
||||
expectHighlightsToBe(updatedHighlights);
|
||||
};
|
||||
|
||||
it('does not display link when disabled', function() {
|
||||
createCourseWithHighlightsDisabled();
|
||||
expect(highlightsLink()).not.toExist();
|
||||
});
|
||||
|
||||
it('displays link when no highlights exist', function() {
|
||||
createCourseWithHighlights([]);
|
||||
expectHighlightLinkNumberToBe(0);
|
||||
|
||||
@@ -212,7 +212,7 @@ if (is_proctored_exam) {
|
||||
</p>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (xblockInfo.get('highlights_enabled') && xblockInfo.isChapter()) { %>
|
||||
<% if (xblockInfo.isChapter()) { %>
|
||||
<div class="block-highlights">
|
||||
<% var number_of_highlights = (xblockInfo.get('highlights') || []).length; %>
|
||||
<button class="block-highlights-value highlights-button action-button">
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
<h3 class="modal-section-title" id="highlights_label"><%- gettext('Section Highlights') %></h3>
|
||||
<div class="highlights-info">
|
||||
|
||||
<% if (highlights_preview_only) { %>
|
||||
<p><b>
|
||||
<%- gettext('This feature is currently in testing. Course teams can enter highlights, but learners will not receive email messages.') %>
|
||||
</b></p>
|
||||
<% } %>
|
||||
|
||||
<%- gettext('Enter 3-5 highlights to include in the email message that learners receive for this section (250 character limit).') %>
|
||||
|
||||
<p>
|
||||
|
||||
@@ -20,7 +20,6 @@ from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from lms.djangoapps.courseware.models import DynamicUpgradeDeadlineConfiguration
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
|
||||
from openedx.core.djangoapps.user_api.models import UserOrgTag
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
@@ -485,7 +484,7 @@ class EntitlementViewSetTest(ModuleStoreTestCase):
|
||||
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
|
||||
course = CourseFactory.create(self_paced=True)
|
||||
course_uuid = uuid.uuid4()
|
||||
course_mode = CourseModeFactory(
|
||||
CourseModeFactory(
|
||||
course_id=course.id,
|
||||
mode_slug=CourseMode.VERIFIED,
|
||||
# This must be in the future to ensure it is returned by downstream code.
|
||||
@@ -499,11 +498,9 @@ class EntitlementViewSetTest(ModuleStoreTestCase):
|
||||
# Add an audit course enrollment for user.
|
||||
enrollment = CourseEnrollment.enroll(self.user, course.id, mode=CourseMode.AUDIT)
|
||||
|
||||
# Set an upgrade schedule so that dynamic upgrade deadlines are used
|
||||
ScheduleFactory.create(
|
||||
enrollment=enrollment,
|
||||
upgrade_deadline=course_mode.expiration_datetime + timedelta(days=-3)
|
||||
)
|
||||
# Set an expired dynamic upgrade deadline
|
||||
enrollment.schedule.upgrade_deadline = now() + timedelta(days=-2)
|
||||
enrollment.schedule.save()
|
||||
|
||||
# The upgrade should complete and ignore the deadline
|
||||
response = self.client.post(
|
||||
|
||||
@@ -27,7 +27,6 @@ from lms.djangoapps.courseware.toggles import (
|
||||
)
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.schedules.models import Schedule
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from common.djangoapps.student.models import (
|
||||
@@ -139,8 +138,6 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase): # lint-amnesty, pylint:
|
||||
self.assertListEqual([self.user, self.user_2], all_enrolled_users)
|
||||
|
||||
@skip_unless_lms
|
||||
# NOTE: We mute the post_save signal to prevent Schedules from being created for new enrollments
|
||||
@factory.django.mute_signals(signals.post_save)
|
||||
def test_upgrade_deadline(self):
|
||||
""" The property should use either the CourseMode or related Schedule to determine the deadline. """
|
||||
course = CourseFactory(self_paced=True)
|
||||
@@ -151,12 +148,10 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase): # lint-amnesty, pylint:
|
||||
expiration_datetime=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=1)
|
||||
)
|
||||
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
|
||||
assert Schedule.objects.all().count() == 0
|
||||
Schedule.objects.all().delete()
|
||||
assert enrollment.upgrade_deadline == course_mode.expiration_datetime
|
||||
|
||||
@skip_unless_lms
|
||||
# NOTE: We mute the post_save signal to prevent Schedules from being created for new enrollments
|
||||
@factory.django.mute_signals(signals.post_save)
|
||||
def test_upgrade_deadline_with_schedule(self):
|
||||
""" The property should use either the CourseMode or related Schedule to determine the deadline. """
|
||||
course = CourseFactory(self_paced=True)
|
||||
@@ -167,16 +162,17 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase): # lint-amnesty, pylint:
|
||||
expiration_datetime=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30),
|
||||
)
|
||||
course_overview = CourseOverview.load_from_module_store(course.id)
|
||||
enrollment = CourseEnrollmentFactory(
|
||||
CourseEnrollmentFactory(
|
||||
course_id=course.id,
|
||||
mode=CourseMode.AUDIT,
|
||||
course=course_overview,
|
||||
)
|
||||
Schedule.objects.update(upgrade_deadline=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=5))
|
||||
enrollment = CourseEnrollment.objects.first()
|
||||
|
||||
# The schedule's upgrade deadline should be used if a schedule exists
|
||||
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
|
||||
schedule = ScheduleFactory(enrollment=enrollment)
|
||||
assert enrollment.upgrade_deadline == schedule.upgrade_deadline
|
||||
assert enrollment.upgrade_deadline == enrollment.schedule.upgrade_deadline
|
||||
|
||||
@skip_unless_lms
|
||||
@ddt.data(*(set(CourseMode.ALL_MODES) - set(CourseMode.AUDIT_MODES)))
|
||||
@@ -197,7 +193,6 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase): # lint-amnesty, pylint:
|
||||
)
|
||||
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
|
||||
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
|
||||
ScheduleFactory(enrollment=enrollment)
|
||||
assert enrollment.schedule is not None
|
||||
assert enrollment.upgrade_deadline == course_upgrade_deadline
|
||||
|
||||
@@ -215,13 +210,10 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase): # lint-amnesty, pylint:
|
||||
)
|
||||
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
|
||||
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
|
||||
ScheduleFactory(enrollment=enrollment)
|
||||
assert enrollment.schedule is not None
|
||||
assert enrollment.upgrade_deadline is None
|
||||
|
||||
@skip_unless_lms
|
||||
# NOTE: We mute the post_save signal to prevent Schedules from being created for new enrollments
|
||||
@factory.django.mute_signals(signals.post_save)
|
||||
def test_enrollments_not_deleted(self):
|
||||
""" Recreating a CourseOverview with an outdated version should not delete the associated enrollment. """
|
||||
course = CourseFactory(self_paced=True)
|
||||
|
||||
@@ -17,7 +17,6 @@ from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from mock import patch
|
||||
from opaque_keys import InvalidKeyError
|
||||
@@ -31,8 +30,6 @@ from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFact
|
||||
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration_context
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
from openedx.features.course_experience.tests.views.helpers import add_course_mode
|
||||
@@ -755,7 +752,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
assert resume_button_html in dashboard_html
|
||||
assert view_button_html not in dashboard_html
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_content_gating_course_card_changes(self):
|
||||
"""
|
||||
When a course is expired, the links on the course card should be removed.
|
||||
@@ -775,9 +771,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
|
||||
enrollment.created = self.THREE_YEARS_AGO + timedelta(days=1)
|
||||
enrollment.save()
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
schedule = ScheduleFactory(enrollment=enrollment)
|
||||
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
dashboard_html = self._remove_whitespace_from_response(response)
|
||||
access_expired_substring = 'Accessexpired'
|
||||
|
||||
@@ -418,7 +418,7 @@ class DashboardTest(ModuleStoreTestCase, TestVerificationBase):
|
||||
|
||||
Note to future developers:
|
||||
If you break this test so that the "check_mongo_calls(0)" fails,
|
||||
please do NOT change it to "check_mongo_calls(n>1)". Instead, change
|
||||
please do NOT change it to "check_mongo_calls(n>=1)". Instead, change
|
||||
your code to not load courses from the module store. This may
|
||||
involve adding fields to CourseOverview so that loading a full
|
||||
CourseDescriptor isn't necessary.
|
||||
|
||||
@@ -12,12 +12,9 @@ from mock import patch
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from lms.djangoapps.courseware.course_tools import FinancialAssistanceTool, VerifiedUpgradeTool
|
||||
from lms.djangoapps.courseware.models import DynamicUpgradeDeadlineConfiguration
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.schedules.config import CREATE_SCHEDULE_WAFFLE_FLAG
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -39,7 +36,6 @@ class VerifiedUpgradeToolTest(SharedModuleStoreTestCase): # lint-amnesty, pylin
|
||||
)
|
||||
cls.course_overview = CourseOverview.get_from_id(cls.course.id)
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
def setUp(self):
|
||||
super(VerifiedUpgradeToolTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
|
||||
@@ -49,11 +45,6 @@ class VerifiedUpgradeToolTest(SharedModuleStoreTestCase): # lint-amnesty, pylin
|
||||
expiration_datetime=self.now + datetime.timedelta(days=30),
|
||||
)
|
||||
|
||||
patcher = patch('openedx.core.djangoapps.schedules.signals.get_current_site')
|
||||
mock_get_current_site = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
mock_get_current_site.return_value = SiteFactory.create()
|
||||
|
||||
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
|
||||
|
||||
self.request = RequestFactory().request()
|
||||
@@ -122,7 +113,6 @@ class FinancialAssistanceToolTest(SharedModuleStoreTestCase):
|
||||
)
|
||||
cls.course_overview = CourseOverview.get_from_id(cls.course.id)
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
def setUp(self):
|
||||
super(FinancialAssistanceToolTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ from lms.djangoapps.verify_student.models import VerificationDeadline
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.schedules.signals import CREATE_SCHEDULE_WAFFLE_FLAG
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
|
||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
||||
@@ -884,16 +883,6 @@ class TestDateAlerts(SharedModuleStoreTestCase):
|
||||
class TestScheduleOverrides(SharedModuleStoreTestCase):
|
||||
""" Tests for Schedule Overrides """
|
||||
|
||||
def setUp(self):
|
||||
super(TestScheduleOverrides, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
|
||||
patcher = patch('openedx.core.djangoapps.schedules.signals.get_current_site')
|
||||
mock_get_current_site = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
mock_get_current_site.return_value = SiteFactory.create()
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
def test_date_with_self_paced_with_enrollment_before_course_start(self):
|
||||
""" Enrolling before a course begins should result in the upgrade deadline being set relative to the
|
||||
course start date. """
|
||||
@@ -914,7 +903,6 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
|
||||
" certificate."
|
||||
assert upgrade_date_summary.relative_datestring == u'by {date}'
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
def test_date_with_self_paced_with_enrollment_after_course_start(self):
|
||||
""" Enrolling after a course begins should result in the upgrade deadline being set relative to the
|
||||
enrollment date.
|
||||
@@ -947,7 +935,6 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
|
||||
expected = enrollment.created + timedelta(days=course_config.deadline_days)
|
||||
assert block.date == expected
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
def test_date_with_self_paced_without_dynamic_upgrade_deadline(self):
|
||||
""" Disabling the dynamic upgrade deadline functionality should result in the verified mode's
|
||||
expiration date being returned. """
|
||||
@@ -958,7 +945,6 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
|
||||
block = VerifiedUpgradeDeadlineDate(course, enrollment.user)
|
||||
assert block.date == expected
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
def test_date_with_existing_schedule(self):
|
||||
""" If a schedule is created while deadlines are disabled, they shouldn't magically appear once the feature is
|
||||
turned on. """
|
||||
@@ -1018,7 +1004,6 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
|
||||
(True, True, True, True, True, False),
|
||||
)
|
||||
@ddt.unpack
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
def test_date_with_org_and_course_config_overrides(self, enroll_first, org_config_enabled, org_config_opt_out,
|
||||
course_config_enabled, course_config_opt_out,
|
||||
expected_dynamic_deadline):
|
||||
|
||||
@@ -1408,7 +1408,7 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
self.assertContains(resp, u"Download Your Certificate")
|
||||
|
||||
@ddt.data(
|
||||
(True, 55),
|
||||
(True, 54),
|
||||
(False, 54),
|
||||
)
|
||||
@ddt.unpack
|
||||
|
||||
@@ -22,8 +22,6 @@ from django.http import HttpRequest, HttpResponse
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse as django_reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from edx_toggles.toggles.testutils import \
|
||||
override_waffle_flag # lint-amnesty, pylint: disable=unused-import, wrong-import-order
|
||||
from edx_when.api import get_dates_for_course, get_overrides_for_user, set_date_for_block
|
||||
from freezegun import freeze_time
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
@@ -48,28 +46,23 @@ from common.djangoapps.student.models import (
|
||||
get_retired_email_by_email,
|
||||
get_retired_username_by_username
|
||||
)
|
||||
from common.djangoapps.student.roles import ( # lint-amnesty, pylint: disable=unused-import
|
||||
from common.djangoapps.student.roles import (
|
||||
CourseBetaTesterRole,
|
||||
CourseDataResearcherRole,
|
||||
CourseFinanceAdminRole,
|
||||
CourseInstructorRole,
|
||||
CourseSalesAdminRole
|
||||
)
|
||||
from common.djangoapps.student.tests.factories import ( # lint-amnesty, pylint: disable=unused-import
|
||||
AdminFactory,
|
||||
UserFactory
|
||||
)
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from lms.djangoapps.bulk_email.models import BulkEmailFlag, CourseEmail, CourseEmailTemplate
|
||||
from lms.djangoapps.certificates.api import generate_user_certificates
|
||||
from lms.djangoapps.certificates.models import CertificateStatuses
|
||||
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
|
||||
from lms.djangoapps.courseware.models import StudentModule
|
||||
from lms.djangoapps.courseware.tests.factories import ( # lint-amnesty, pylint: disable=unused-import
|
||||
from lms.djangoapps.courseware.tests.factories import (
|
||||
BetaTesterFactory,
|
||||
GlobalStaffFactory,
|
||||
InstructorFactory,
|
||||
StaffFactory,
|
||||
UserProfileFactory
|
||||
)
|
||||
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag
|
||||
@@ -89,7 +82,6 @@ from openedx.core.djangoapps.course_date_signals.handlers import extract_dates
|
||||
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted
|
||||
from openedx.core.djangoapps.django_comment_common.models import FORUM_ROLE_COMMUNITY_TA
|
||||
from openedx.core.djangoapps.django_comment_common.utils import seed_permissions_roles
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
|
||||
from openedx.core.lib.teams_config import TeamsConfig
|
||||
@@ -3905,8 +3897,8 @@ class TestDueDateExtensions(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
|
||||
self.user1 = user1
|
||||
self.user2 = user2
|
||||
ScheduleFactory.create(enrollment__user=self.user1, enrollment__course_id=self.course.id)
|
||||
ScheduleFactory.create(enrollment__user=self.user2, enrollment__course_id=self.course.id)
|
||||
CourseEnrollmentFactory.create(user=self.user1, course_id=self.course.id)
|
||||
CourseEnrollmentFactory.create(user=self.user2, course_id=self.course.id)
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
extract_dates(None, self.course.id)
|
||||
@@ -4076,8 +4068,8 @@ class TestDueDateExtensionsDeletedDate(ModuleStoreTestCase, LoginEnrollmentTestC
|
||||
|
||||
self.user1 = user1
|
||||
self.user2 = user2
|
||||
ScheduleFactory.create(enrollment__user=self.user1, enrollment__course_id=self.course.id)
|
||||
ScheduleFactory.create(enrollment__user=self.user2, enrollment__course_id=self.course.id)
|
||||
CourseEnrollmentFactory.create(user=self.user1, course_id=self.course.id)
|
||||
CourseEnrollmentFactory.create(user=self.user2, course_id=self.course.id)
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
extract_dates(None, self.course.id)
|
||||
|
||||
@@ -17,9 +17,8 @@ from edx_when.field_data import DateLookupFieldData
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from pytz import UTC
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from openedx.core.djangoapps.course_date_signals import handlers
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from xmodule.fields import Date
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
@@ -219,7 +218,7 @@ class TestSetDueDateExtension(ModuleStoreTestCase):
|
||||
self.week3 = week3
|
||||
self.user = user
|
||||
|
||||
ScheduleFactory.create(enrollment__user=self.user, enrollment__course_id=self.course.id)
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
|
||||
|
||||
inject_field_data((course, week1, week2, week3, homework, assignment), course, user)
|
||||
|
||||
@@ -307,8 +306,8 @@ class TestDataDumps(ModuleStoreTestCase):
|
||||
self.week2 = week2
|
||||
self.user1 = user1
|
||||
self.user2 = user2
|
||||
ScheduleFactory.create(enrollment__user=self.user1, enrollment__course_id=self.course.id)
|
||||
ScheduleFactory.create(enrollment__user=self.user2, enrollment__course_id=self.course.id)
|
||||
CourseEnrollmentFactory.create(user=self.user1, course_id=self.course.id)
|
||||
CourseEnrollmentFactory.create(user=self.user2, course_id=self.course.id)
|
||||
handlers.extract_dates(None, course.id)
|
||||
|
||||
def test_dump_module_extensions(self):
|
||||
|
||||
@@ -34,7 +34,6 @@ from lms.djangoapps.mobile_api.testutils import (
|
||||
MobileCourseAccessTestMixin
|
||||
)
|
||||
from lms.djangoapps.mobile_api.utils import API_V1, API_V05
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.lib.courses import course_image_url
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
from openedx.features.course_experience.tests.views.helpers import add_course_mode
|
||||
@@ -279,10 +278,6 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
|
||||
)
|
||||
enrollment.created = self.THREE_YEARS_AGO + datetime.timedelta(days=1)
|
||||
enrollment.save()
|
||||
|
||||
ScheduleFactory(
|
||||
enrollment=enrollment
|
||||
)
|
||||
else:
|
||||
course = CourseFactory.create(start=self.LAST_WEEK, mobile_available=True)
|
||||
self.enroll(course.id)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.19 on 2021-02-23 14:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('course_overviews', '0023_courseoverview_banner_image_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='courseoverview',
|
||||
name='has_highlights',
|
||||
field=models.NullBooleanField(default=None),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalcourseoverview',
|
||||
name='has_highlights',
|
||||
field=models.NullBooleanField(default=None),
|
||||
),
|
||||
]
|
||||
@@ -12,7 +12,9 @@ from config_models.models import ConfigurationModel
|
||||
from django.conf import settings
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.db.models.fields import BooleanField, DateTimeField, DecimalField, FloatField, IntegerField, TextField
|
||||
from django.db.models.fields import (
|
||||
BooleanField, DateTimeField, DecimalField, FloatField, IntegerField, NullBooleanField, TextField
|
||||
)
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.db.utils import IntegrityError
|
||||
from django.template import defaultfilters
|
||||
@@ -126,6 +128,9 @@ class CourseOverview(TimeStampedModel):
|
||||
marketing_url = TextField(null=True)
|
||||
eligible_for_financial_aid = BooleanField(default=True)
|
||||
|
||||
# Course highlight info, used to guide course update emails
|
||||
has_highlights = NullBooleanField(default=None) # if None, you have to look up the answer yourself
|
||||
|
||||
language = TextField(null=True)
|
||||
|
||||
history = HistoricalRecords()
|
||||
@@ -229,6 +234,8 @@ class CourseOverview(TimeStampedModel):
|
||||
course_overview.course_video_url = CourseDetails.fetch_video_url(course.id)
|
||||
course_overview.self_paced = course.self_paced
|
||||
|
||||
course_overview.has_highlights = cls._get_course_has_highlights(course)
|
||||
|
||||
if not CatalogIntegration.is_enabled():
|
||||
course_overview.language = course.language
|
||||
|
||||
@@ -413,6 +420,12 @@ class CourseOverview(TimeStampedModel):
|
||||
overviews[course_id] = None
|
||||
return overviews
|
||||
|
||||
@classmethod
|
||||
def _get_course_has_highlights(cls, course):
|
||||
# Avoid circular import here
|
||||
from openedx.core.djangoapps.schedules.content_highlights import course_has_highlights
|
||||
return course_has_highlights(course)
|
||||
|
||||
def clean_id(self, padding_char='='):
|
||||
"""
|
||||
Returns a unique deterministic base32-encoded ID for the course.
|
||||
|
||||
@@ -166,22 +166,16 @@ class ScheduleAdmin(admin.ModelAdmin): # lint-amnesty, pylint: disable=missing-
|
||||
|
||||
|
||||
class ScheduleConfigAdminForm(forms.ModelForm): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
|
||||
def clean_hold_back_ratio(self):
|
||||
hold_back_ratio = self.cleaned_data["hold_back_ratio"]
|
||||
if hold_back_ratio < 0 or hold_back_ratio > 1:
|
||||
raise forms.ValidationError("Invalid hold back ratio, the value must be between 0 and 1.")
|
||||
return hold_back_ratio
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(models.ScheduleConfig)
|
||||
class ScheduleConfigAdmin(admin.ModelAdmin): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
search_fields = ('site',)
|
||||
list_display = (
|
||||
'site', 'create_schedules',
|
||||
'site',
|
||||
'enqueue_recurring_nudge', 'deliver_recurring_nudge',
|
||||
'enqueue_upgrade_reminder', 'deliver_upgrade_reminder',
|
||||
'enqueue_course_update', 'deliver_course_update',
|
||||
'hold_back_ratio',
|
||||
)
|
||||
form = ScheduleConfigAdminForm
|
||||
|
||||
@@ -2,31 +2,10 @@
|
||||
Contains configuration for schedules app
|
||||
"""
|
||||
|
||||
from edx_toggles.toggles import LegacyWaffleSwitch, LegacyWaffleSwitchNamespace, WaffleFlag
|
||||
|
||||
from edx_toggles.toggles import (
|
||||
WaffleFlag,
|
||||
LegacyWaffleFlagNamespace,
|
||||
LegacyWaffleSwitch,
|
||||
LegacyWaffleSwitchNamespace
|
||||
)
|
||||
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
||||
|
||||
WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='schedules')
|
||||
WAFFLE_SWITCH_NAMESPACE = LegacyWaffleSwitchNamespace(name='schedules')
|
||||
|
||||
CREATE_SCHEDULE_WAFFLE_FLAG = CourseWaffleFlag(
|
||||
waffle_namespace=WAFFLE_FLAG_NAMESPACE,
|
||||
flag_name='create_schedules_for_course',
|
||||
module_name=__name__,
|
||||
)
|
||||
|
||||
COURSE_UPDATE_WAFFLE_FLAG = CourseWaffleFlag(
|
||||
waffle_namespace=WAFFLE_FLAG_NAMESPACE,
|
||||
flag_name='send_updates_for_course',
|
||||
module_name=__name__,
|
||||
)
|
||||
|
||||
# .. toggle_name: schedules.enable_debugging
|
||||
# .. toggle_implementation: WaffleFlag
|
||||
# .. toggle_default: False
|
||||
|
||||
@@ -7,7 +7,6 @@ schedule experience built on the Schedules app.
|
||||
import logging
|
||||
|
||||
from openedx.core.djangoapps.course_date_signals.utils import spaced_out_sections
|
||||
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
|
||||
from openedx.core.djangoapps.schedules.exceptions import CourseUpdateDoesNotExist
|
||||
from openedx.core.lib.request_utils import get_request_or_stub
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -15,16 +14,16 @@ from xmodule.modulestore.django import modulestore
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def course_has_highlights(course_key):
|
||||
def course_has_highlights(course):
|
||||
"""
|
||||
Does the course have any highlights for any section/week in it?
|
||||
This ignores access checks, since highlights may be lurking in currently
|
||||
inaccessible content.
|
||||
"""
|
||||
try:
|
||||
course = _get_course_with_highlights(course_key)
|
||||
|
||||
except CourseUpdateDoesNotExist:
|
||||
Arguments:
|
||||
course (CourseDescriptor): course object to check
|
||||
"""
|
||||
if not course.highlights_enabled_for_messaging:
|
||||
return False
|
||||
|
||||
else:
|
||||
@@ -36,12 +35,28 @@ def course_has_highlights(course_key):
|
||||
|
||||
if not highlights_are_available:
|
||||
log.warning(
|
||||
'Course team enabled highlights and provided no highlights in {}'.format(course_key)
|
||||
'Course team enabled highlights and provided no highlights in {}'.format(course.id)
|
||||
)
|
||||
|
||||
return highlights_are_available
|
||||
|
||||
|
||||
def course_has_highlights_from_store(course_key):
|
||||
"""
|
||||
Does the course have any highlights for any section/week in it?
|
||||
This ignores access checks, since highlights may be lurking in currently
|
||||
inaccessible content.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): course to lookup from the modulestore
|
||||
"""
|
||||
try:
|
||||
course = _get_course_descriptor(course_key)
|
||||
except CourseUpdateDoesNotExist:
|
||||
return False
|
||||
return course_has_highlights(course)
|
||||
|
||||
|
||||
def get_week_highlights(user, course_key, week_num):
|
||||
"""
|
||||
Get highlights (list of unicode strings) for a given week.
|
||||
@@ -76,11 +91,6 @@ def get_next_section_highlights(user, course_key, start_date, target_date):
|
||||
|
||||
def _get_course_with_highlights(course_key):
|
||||
""" Gets Course descriptor iff highlights are enabled for the course """
|
||||
if not COURSE_UPDATE_WAFFLE_FLAG.is_enabled(course_key):
|
||||
raise CourseUpdateDoesNotExist(
|
||||
'{} Course Update Messages waffle flag is disabled.'.format(course_key)
|
||||
)
|
||||
|
||||
course_descriptor = _get_course_descriptor(course_key)
|
||||
if not course_descriptor.highlights_enabled_for_messaging:
|
||||
raise CourseUpdateDoesNotExist(
|
||||
|
||||
@@ -236,28 +236,6 @@ Configuration Flags
|
||||
Configuring Schedule Creation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Make sure a Site has been created at ``<lms_url>/admin/sites/site``.
|
||||
|
||||
ScheduleConfig
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
In the Django admin panel at
|
||||
``<lms_url>/admin/schedules/scheduleconfig/`` create a ScheduleConfig
|
||||
and link it to the Site. Make sure to enable all of the settings:
|
||||
|
||||
- ``create_schedules``: enables creating new Schedules when new Course
|
||||
Enrollments are created.
|
||||
- ``hold_back_ratio``: ratio of all new Course Enrollments that should
|
||||
NOT have a Schedule created.
|
||||
|
||||
Roll-out Waffle Flag
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
There is one roll-out related course waffle flag that we plan to delete
|
||||
called ``schedules.create_schedules_for_course``, which, if the
|
||||
``ScheduleConfig.create_schedules`` is disabled, will enable schedule
|
||||
creation on a per-course basis.
|
||||
|
||||
Self-paced Configuration
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -324,25 +302,6 @@ configure enqueueing and delivering emails per message type:
|
||||
- ``deliver_*``: allows delivering emails through ACE for this message
|
||||
type.
|
||||
|
||||
.. roll-out-waffle-flag-1:
|
||||
|
||||
Roll-out Waffle Flag
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Another roll-out related course waffle flag that we plan to delete
|
||||
called ``schedules.send_updates_for_course`` will enable sending
|
||||
specifically the course updates email per-course.
|
||||
|
||||
Configuring Highlights UI in Studio
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The button and modal on the course outline page that allows course
|
||||
authors to enter section highlights can be toggled globally by going to
|
||||
``<lms_url>/admin/waffle/switch/`` and adding an active switch called
|
||||
``dynamic_pacing.studio_course_update``.
|
||||
|
||||
This is a roll-out related waffle switch that we will eventually delete.
|
||||
|
||||
Configuring a Learner’s Schedule
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -463,10 +422,10 @@ To begin using Litmus, follow these steps:
|
||||
1. Make sure that ACE is configured to use Sailthru (see instructions above).
|
||||
2. Go to the `Litmus checklist page <https://litmus.com/checklist>`__ and start
|
||||
a new checklist.
|
||||
3. The checklist will provide you with an email address to which you will send
|
||||
3. The checklist will provide you with an email address to which you will send
|
||||
a test email.
|
||||
4. Send an email. Use one of the management commands with the
|
||||
`--override-recipient-email` flag. Use the Litmus email you got in step 3
|
||||
4. Send an email. Use one of the management commands with the
|
||||
`--override-recipient-email` flag. Use the Litmus email you got in step 3
|
||||
as the flag value.
|
||||
|
||||
::
|
||||
|
||||
@@ -12,9 +12,7 @@ from edx_ace.utils.date import serialize
|
||||
from mock import patch
|
||||
from six.moves import range
|
||||
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from openedx.core.djangoapps.schedules import resolvers, tasks
|
||||
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
|
||||
from openedx.core.djangoapps.schedules.management.commands import send_course_update as nudge
|
||||
from openedx.core.djangoapps.schedules.management.commands.tests.send_email_base import (
|
||||
ExperienceTest,
|
||||
@@ -59,13 +57,12 @@ class TestSendCourseUpdate(ScheduleUpsellTestMixin, ScheduleSendEmailTestMixin,
|
||||
mock_highlights.return_value = [u'Highlight {}'.format(num + 1) for num in range(3)]
|
||||
self.addCleanup(self.stop_highlights_patcher)
|
||||
|
||||
def prepare_course_data(self, mock_get_current_site, is_self_paced=True):
|
||||
def prepare_course_data(self, is_self_paced=True):
|
||||
"""
|
||||
Prepare course data with highlights
|
||||
"""
|
||||
self.highlights_patcher.stop()
|
||||
self.highlights_patcher = None
|
||||
mock_get_current_site.return_value = self.site_config.site
|
||||
|
||||
course = CourseFactory(highlights_enabled_for_messaging=True, self_paced=is_self_paced)
|
||||
with self.store.bulk_operations(course.id):
|
||||
@@ -96,10 +93,8 @@ class TestSendCourseUpdate(ScheduleUpsellTestMixin, ScheduleSendEmailTestMixin,
|
||||
def test_schedule_in_different_experience(self, test_config):
|
||||
self._check_if_email_sent_for_experience(test_config)
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
@patch('openedx.core.djangoapps.schedules.signals.get_current_site')
|
||||
def test_with_course_data(self, mock_get_current_site):
|
||||
offset, target_day, enrollment = self.prepare_course_data(mock_get_current_site)
|
||||
def test_with_course_data(self):
|
||||
offset, target_day, enrollment = self.prepare_course_data()
|
||||
|
||||
with patch.object(tasks, 'ace') as mock_ace:
|
||||
self.task().apply(kwargs=dict(
|
||||
@@ -111,14 +106,12 @@ class TestSendCourseUpdate(ScheduleUpsellTestMixin, ScheduleSendEmailTestMixin,
|
||||
|
||||
assert mock_ace.send.called
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
@patch('openedx.core.djangoapps.schedules.signals.get_current_site')
|
||||
def test_template_for_instructor_led_courses(self, mock_get_current_site):
|
||||
def test_template_for_instructor_led_courses(self):
|
||||
"""
|
||||
Test that InstructorLedCourseUpdate template is picked for instructor led
|
||||
courses
|
||||
"""
|
||||
offset, target_day, enrollment = self.prepare_course_data(mock_get_current_site, is_self_paced=False)
|
||||
offset, target_day, enrollment = self.prepare_course_data(is_self_paced=False)
|
||||
|
||||
self.task().apply(kwargs=dict(
|
||||
site_id=self.site_config.site.id,
|
||||
|
||||
@@ -54,14 +54,14 @@ class ScheduleConfig(ConfigurationModel):
|
||||
KEY_FIELDS = ('site',)
|
||||
|
||||
site = models.ForeignKey(Site, on_delete=models.CASCADE)
|
||||
create_schedules = models.BooleanField(default=False)
|
||||
create_schedules = models.BooleanField(default=False) # deprecated, do not use
|
||||
enqueue_recurring_nudge = models.BooleanField(default=False)
|
||||
deliver_recurring_nudge = models.BooleanField(default=False)
|
||||
enqueue_upgrade_reminder = models.BooleanField(default=False)
|
||||
deliver_upgrade_reminder = models.BooleanField(default=False)
|
||||
enqueue_course_update = models.BooleanField(default=False)
|
||||
deliver_course_update = models.BooleanField(default=False)
|
||||
hold_back_ratio = models.FloatField(default=0)
|
||||
hold_back_ratio = models.FloatField(default=0) # deprecated, do not use
|
||||
|
||||
|
||||
class ScheduleExperience(models.Model):
|
||||
|
||||
@@ -4,7 +4,6 @@ CourseEnrollment related signal handlers.
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import random
|
||||
|
||||
import six
|
||||
from django.db.models.signals import post_save
|
||||
@@ -18,16 +17,13 @@ from lms.djangoapps.courseware.models import (
|
||||
OrgDynamicUpgradeDeadlineConfiguration
|
||||
)
|
||||
from openedx.core.djangoapps.content.course_overviews.signals import COURSE_START_DATE_CHANGED
|
||||
from openedx.core.djangoapps.schedules.content_highlights import course_has_highlights
|
||||
from openedx.core.djangoapps.schedules.content_highlights import course_has_highlights_from_store
|
||||
from openedx.core.djangoapps.schedules.models import ScheduleExperience
|
||||
from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule
|
||||
from openedx.core.djangoapps.theming.helpers import get_current_site
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.signals import ENROLL_STATUS_CHANGE, ENROLLMENT_TRACK_UPDATED # lint-amnesty, pylint: disable=unused-import
|
||||
from common.djangoapps.track import segment
|
||||
from common.djangoapps.student.signals import ENROLLMENT_TRACK_UPDATED
|
||||
|
||||
from .config import CREATE_SCHEDULE_WAFFLE_FLAG
|
||||
from .models import Schedule, ScheduleConfig
|
||||
from .models import Schedule
|
||||
from .tasks import update_course_schedules
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -139,39 +135,6 @@ def _get_upgrade_deadline_delta_setting(course_id): # lint-amnesty, pylint: dis
|
||||
return delta
|
||||
|
||||
|
||||
def _should_randomly_suppress_schedule_creation( # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
schedule_config,
|
||||
enrollment,
|
||||
upgrade_deadline,
|
||||
experience_type,
|
||||
content_availability_date,
|
||||
):
|
||||
# The hold back ratio is always between 0 and 1. A value of 0 indicates that schedules should be created for all
|
||||
# schedules. A value of 1 indicates that no schedules should be created for any enrollments. A value of 0.2 would
|
||||
# mean that 20% of enrollments should *not* be given schedules.
|
||||
|
||||
# This allows us to measure the impact of the dynamic schedule experience by comparing this "control" group that
|
||||
# does not receive any of benefits of the feature against the group that does.
|
||||
if random.random() < schedule_config.hold_back_ratio:
|
||||
log.debug('Schedules: Enrollment held back from dynamic schedule experiences.')
|
||||
upgrade_deadline_str = None
|
||||
if upgrade_deadline:
|
||||
upgrade_deadline_str = upgrade_deadline.isoformat()
|
||||
segment.track(
|
||||
user_id=enrollment.user.id,
|
||||
event_name='edx.bi.schedule.suppressed',
|
||||
properties={
|
||||
'course_id': six.text_type(enrollment.course_id),
|
||||
'experience_type': experience_type,
|
||||
'upgrade_deadline': upgrade_deadline_str,
|
||||
'content_availability_date': content_availability_date.isoformat(),
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _create_schedule(enrollment, enrollment_created):
|
||||
"""
|
||||
Checks configuration and creates Schedule with ScheduleExperience for this enrollment.
|
||||
@@ -180,34 +143,12 @@ def _create_schedule(enrollment, enrollment_created):
|
||||
# only create schedules when enrollment records are created
|
||||
return
|
||||
|
||||
current_site = get_current_site()
|
||||
if current_site is None:
|
||||
log.debug('Schedules: No current site')
|
||||
return
|
||||
|
||||
schedule_config = ScheduleConfig.current(current_site)
|
||||
if (
|
||||
not schedule_config.create_schedules and
|
||||
not CREATE_SCHEDULE_WAFFLE_FLAG.is_enabled(enrollment.course_id)
|
||||
):
|
||||
log.debug('Schedules: Creation not enabled for this course or for this site')
|
||||
return
|
||||
|
||||
# This represents the first date at which the learner can access the content. This will be the latter of
|
||||
# either the enrollment date or the course's start date.
|
||||
content_availability_date = max(enrollment.created, enrollment.course_overview.start)
|
||||
upgrade_deadline = _calculate_upgrade_deadline(enrollment.course_id, content_availability_date)
|
||||
experience_type = _get_experience_type(enrollment)
|
||||
|
||||
if _should_randomly_suppress_schedule_creation(
|
||||
schedule_config,
|
||||
enrollment,
|
||||
upgrade_deadline,
|
||||
experience_type,
|
||||
content_availability_date,
|
||||
):
|
||||
return
|
||||
|
||||
schedule = Schedule.objects.create(
|
||||
enrollment=enrollment,
|
||||
start_date=content_availability_date,
|
||||
@@ -229,7 +170,11 @@ def _get_experience_type(enrollment):
|
||||
|
||||
Schedules will receive the Course Updates experience if the course has any section highlights defined.
|
||||
"""
|
||||
if course_has_highlights(enrollment.course_id):
|
||||
has_highlights = enrollment.course_overview.has_highlights
|
||||
if has_highlights is None: # old course that doesn't have this info cached in the overview
|
||||
has_highlights = course_has_highlights_from_store(enrollment.course_id)
|
||||
|
||||
if has_highlights:
|
||||
return ScheduleExperience.EXPERIENCES.course_updates
|
||||
else:
|
||||
return ScheduleExperience.EXPERIENCES.default
|
||||
|
||||
@@ -33,11 +33,9 @@ class ScheduleConfigFactory(factory.DjangoModelFactory): # lint-amnesty, pylint
|
||||
model = models.ScheduleConfig
|
||||
|
||||
site = factory.SubFactory(SiteFactory)
|
||||
create_schedules = True
|
||||
enqueue_recurring_nudge = True
|
||||
deliver_recurring_nudge = True
|
||||
enqueue_upgrade_reminder = True
|
||||
deliver_upgrade_reminder = True
|
||||
enqueue_course_update = True
|
||||
deliver_course_update = True
|
||||
hold_back_ratio = 0
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
import datetime
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
|
||||
from openedx.core.djangoapps.schedules.content_highlights import (
|
||||
course_has_highlights,
|
||||
course_has_highlights_from_store,
|
||||
get_next_section_highlights,
|
||||
get_week_highlights
|
||||
)
|
||||
@@ -44,35 +42,22 @@ class TestContentHighlights(ModuleStoreTestCase): # lint-amnesty, pylint: disab
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_non_existent_course_raises_exception(self):
|
||||
nonexistent_course_key = self.course_key.replace(run='no_such_run')
|
||||
with pytest.raises(CourseUpdateDoesNotExist):
|
||||
get_week_highlights(self.user, nonexistent_course_key, week_num=1)
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_empty_course_raises_exception(self):
|
||||
with pytest.raises(CourseUpdateDoesNotExist):
|
||||
get_week_highlights(self.user, self.course_key, week_num=1)
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, False)
|
||||
def test_flag_disabled(self):
|
||||
with self.store.bulk_operations(self.course_key):
|
||||
self._create_chapter(highlights=['highlights'])
|
||||
|
||||
assert not course_has_highlights(self.course_key)
|
||||
with pytest.raises(CourseUpdateDoesNotExist):
|
||||
get_week_highlights(self.user, self.course_key, week_num=1)
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_flag_enabled(self):
|
||||
def test_happy_path(self):
|
||||
highlights = ['highlights']
|
||||
with self.store.bulk_operations(self.course_key):
|
||||
self._create_chapter(highlights=highlights)
|
||||
assert course_has_highlights(self.course_key)
|
||||
assert course_has_highlights_from_store(self.course_key)
|
||||
assert get_week_highlights(self.user, self.course_key, week_num=1) == highlights
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_highlights_disabled_for_messaging(self):
|
||||
highlights = ['A test highlight.']
|
||||
with self.store.bulk_operations(self.course_key):
|
||||
@@ -80,7 +65,7 @@ class TestContentHighlights(ModuleStoreTestCase): # lint-amnesty, pylint: disab
|
||||
self.course.highlights_enabled_for_messaging = False
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
assert not course_has_highlights(self.course_key)
|
||||
assert not course_has_highlights_from_store(self.course_key)
|
||||
|
||||
with pytest.raises(CourseUpdateDoesNotExist):
|
||||
get_week_highlights(
|
||||
@@ -89,7 +74,6 @@ class TestContentHighlights(ModuleStoreTestCase): # lint-amnesty, pylint: disab
|
||||
week_num=1,
|
||||
)
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_course_with_no_highlights(self):
|
||||
with self.store.bulk_operations(self.course_key):
|
||||
self._create_chapter(display_name=u"Week 1")
|
||||
@@ -98,25 +82,23 @@ class TestContentHighlights(ModuleStoreTestCase): # lint-amnesty, pylint: disab
|
||||
self.course = self.store.get_course(self.course_key) # lint-amnesty, pylint: disable=attribute-defined-outside-init
|
||||
assert len(self.course.get_children()) == 2
|
||||
|
||||
assert not course_has_highlights(self.course_key)
|
||||
assert not course_has_highlights_from_store(self.course_key)
|
||||
with pytest.raises(CourseUpdateDoesNotExist):
|
||||
get_week_highlights(self.user, self.course_key, week_num=1)
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_course_with_highlights(self):
|
||||
with self.store.bulk_operations(self.course_key):
|
||||
self._create_chapter(highlights=['a', 'b', 'á'])
|
||||
self._create_chapter(highlights=[])
|
||||
self._create_chapter(highlights=['skipped a week'])
|
||||
|
||||
assert course_has_highlights(self.course_key)
|
||||
assert course_has_highlights_from_store(self.course_key)
|
||||
|
||||
assert get_week_highlights(self.user, self.course_key, week_num=1) == ['a', 'b', 'á']
|
||||
assert get_week_highlights(self.user, self.course_key, week_num=2) == ['skipped a week']
|
||||
with pytest.raises(CourseUpdateDoesNotExist):
|
||||
get_week_highlights(self.user, self.course_key, week_num=3)
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_staff_only(self):
|
||||
with self.store.bulk_operations(self.course_key):
|
||||
self._create_chapter(
|
||||
@@ -124,11 +106,10 @@ class TestContentHighlights(ModuleStoreTestCase): # lint-amnesty, pylint: disab
|
||||
visible_to_staff_only=True,
|
||||
)
|
||||
|
||||
assert course_has_highlights(self.course_key)
|
||||
assert course_has_highlights_from_store(self.course_key)
|
||||
with pytest.raises(CourseUpdateDoesNotExist):
|
||||
get_week_highlights(self.user, self.course_key, week_num=1)
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
@patch('openedx.core.djangoapps.course_date_signals.utils.get_expected_duration')
|
||||
def test_get_next_section_highlights(self, mock_duration):
|
||||
# All of the dates chosen here are to make things easy and clean to calculate with date offsets
|
||||
@@ -170,7 +151,6 @@ class TestContentHighlights(ModuleStoreTestCase): # lint-amnesty, pylint: disab
|
||||
with pytest.raises(CourseUpdateDoesNotExist):
|
||||
get_next_section_highlights(self.user, self.course_key, two_days_ago, six_days.date())
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
@patch('lms.djangoapps.courseware.module_render.get_module_for_descriptor')
|
||||
def test_get_highlights_without_module(self, mock_get_module):
|
||||
mock_get_module.return_value = None
|
||||
|
||||
@@ -4,7 +4,7 @@ Tests for schedules resolvers
|
||||
|
||||
|
||||
import datetime
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import Mock
|
||||
|
||||
import ddt
|
||||
from django.test import TestCase
|
||||
@@ -12,8 +12,6 @@ from django.test.utils import override_settings
|
||||
from testfixtures import LogCapture
|
||||
from waffle.testutils import override_switch
|
||||
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
|
||||
from openedx.core.djangoapps.schedules.models import Schedule
|
||||
from openedx.core.djangoapps.schedules.resolvers import (
|
||||
LOG,
|
||||
@@ -111,9 +109,7 @@ class TestCourseUpdateResolver(SchedulesResolverTestMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
Creates a CourseUpdateResolver with an enrollment to schedule.
|
||||
"""
|
||||
with patch('openedx.core.djangoapps.schedules.signals.get_current_site') as mock_get_current_site:
|
||||
mock_get_current_site.return_value = self.site_config.site
|
||||
enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode='audit')
|
||||
enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode='audit')
|
||||
|
||||
return CourseUpdateResolver(
|
||||
async_send_task=Mock(name='async_send_task'),
|
||||
@@ -125,7 +121,6 @@ class TestCourseUpdateResolver(SchedulesResolverTestMixin, ModuleStoreTestCase):
|
||||
|
||||
@override_settings(CONTACT_MAILING_ADDRESS='123 Sesame Street')
|
||||
@override_settings(LOGO_URL_PNG='https://www.logo.png')
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_schedule_context(self):
|
||||
resolver = self.create_resolver()
|
||||
schedules = list(resolver.schedules_for_bin())
|
||||
@@ -149,14 +144,12 @@ class TestCourseUpdateResolver(SchedulesResolverTestMixin, ModuleStoreTestCase):
|
||||
}
|
||||
assert schedules == [(self.user, None, expected_context)]
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
@override_switch('schedules.course_update_show_unsubscribe', True)
|
||||
def test_schedule_context_show_unsubscribe(self):
|
||||
resolver = self.create_resolver()
|
||||
schedules = list(resolver.schedules_for_bin())
|
||||
assert 'optout' in schedules[0][2]['unsubscribe_url']
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_get_schedules_with_target_date_by_bin_and_orgs_filter_inactive_users(self):
|
||||
"""Tests that schedules of inactive users are excluded"""
|
||||
resolver = self.create_resolver()
|
||||
@@ -194,9 +187,7 @@ class TestCourseNextSectionUpdateResolver(SchedulesResolverTestMixin, ModuleStor
|
||||
"""
|
||||
Creates a CourseNextSectionUpdateResolver with an enrollment to schedule.
|
||||
"""
|
||||
with patch('openedx.core.djangoapps.schedules.signals.get_current_site') as mock_get_current_site:
|
||||
mock_get_current_site.return_value = self.site_config.site
|
||||
CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode='audit')
|
||||
CourseEnrollmentFactory(course_id=self.course.id, user=self.user, mode='audit')
|
||||
|
||||
# Need to update the user's schedule so the due date for the chapter we want
|
||||
# matches with the user's schedule and the target date. The numbers are based on the
|
||||
@@ -214,7 +205,6 @@ class TestCourseNextSectionUpdateResolver(SchedulesResolverTestMixin, ModuleStor
|
||||
|
||||
@override_settings(CONTACT_MAILING_ADDRESS='123 Sesame Street')
|
||||
@override_settings(LOGO_URL_PNG='https://www.logo.png')
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_schedule_context(self):
|
||||
resolver = self.create_resolver()
|
||||
# using this to make sure the select_related stays intact
|
||||
@@ -242,14 +232,12 @@ class TestCourseNextSectionUpdateResolver(SchedulesResolverTestMixin, ModuleStor
|
||||
}
|
||||
assert schedules == [(self.user, None, expected_context)]
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
@override_switch('schedules.course_update_show_unsubscribe', True)
|
||||
def test_schedule_context_show_unsubscribe(self):
|
||||
resolver = self.create_resolver()
|
||||
schedules = list(resolver.get_schedules())
|
||||
assert 'optout' in schedules[0][2]['unsubscribe_url']
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_schedule_context_error(self):
|
||||
resolver = self.create_resolver(user_start_date_offset=29)
|
||||
with LogCapture(LOG.name) as log_capture:
|
||||
@@ -258,7 +246,6 @@ class TestCourseNextSectionUpdateResolver(SchedulesResolverTestMixin, ModuleStor
|
||||
'There are no more highlights for {}'.format(self.course.id))
|
||||
log_capture.check_present((LOG.name, 'WARNING', log_message))
|
||||
|
||||
@override_waffle_flag(COURSE_UPDATE_WAFFLE_FLAG, True)
|
||||
def test_no_updates_if_course_ended(self):
|
||||
self.course.end = self.yesterday
|
||||
self.course = self.update_course(self.course, self.user.id)
|
||||
|
||||
@@ -7,16 +7,14 @@ import datetime
|
||||
|
||||
import ddt
|
||||
import pytest
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from mock import patch
|
||||
from pytz import utc
|
||||
from testfixtures import LogCapture
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from lms.djangoapps.courseware.models import DynamicUpgradeDeadlineConfiguration
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.schedules.models import ScheduleExperience
|
||||
from openedx.core.djangoapps.schedules.signals import CREATE_SCHEDULE_WAFFLE_FLAG, log
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
@@ -30,7 +28,6 @@ from ..tests.factories import ScheduleConfigFactory
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@patch('openedx.core.djangoapps.schedules.signals.get_current_site')
|
||||
@skip_unless_lms
|
||||
class CreateScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
|
||||
@@ -57,115 +54,39 @@ class CreateScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, pylint: d
|
||||
with pytest.raises(Schedule.DoesNotExist):
|
||||
enrollment.schedule # lint-amnesty, pylint: disable=pointless-statement
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
def test_create_schedule(self, mock_get_current_site):
|
||||
site = SiteFactory.create()
|
||||
mock_get_current_site.return_value = site
|
||||
ScheduleConfigFactory.create(site=site)
|
||||
def test_create_schedule(self):
|
||||
self.assert_schedule_created()
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
def test_no_current_site(self, mock_get_current_site):
|
||||
mock_get_current_site.return_value = None
|
||||
self.assert_schedule_not_created()
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
def test_schedule_config_disabled_waffle_enabled(self, mock_get_current_site):
|
||||
site = SiteFactory.create()
|
||||
mock_get_current_site.return_value = site
|
||||
ScheduleConfigFactory.create(site=site, create_schedules=False)
|
||||
self.assert_schedule_created()
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, False)
|
||||
def test_schedule_config_enabled_waffle_disabled(self, mock_get_current_site):
|
||||
site = SiteFactory.create()
|
||||
mock_get_current_site.return_value = site
|
||||
ScheduleConfigFactory.create(site=site, create_schedules=True)
|
||||
self.assert_schedule_created()
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, False)
|
||||
def test_schedule_config_disabled_waffle_disabled(self, mock_get_current_site):
|
||||
site = SiteFactory.create()
|
||||
mock_get_current_site.return_value = site
|
||||
ScheduleConfigFactory.create(site=site, create_schedules=False)
|
||||
with LogCapture(log.name) as log_capture:
|
||||
self.assert_schedule_not_created()
|
||||
log_capture.check((log.name, 'DEBUG', 'Schedules: Creation not enabled for this course or for this site'))
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
@patch('openedx.core.djangoapps.schedules.signals.course_has_highlights')
|
||||
def test_schedule_config_creation_enabled_instructor_paced(self, mock_course_has_highlights, mock_get_current_site):
|
||||
site = SiteFactory.create()
|
||||
mock_course_has_highlights.return_value = True
|
||||
mock_get_current_site.return_value = site
|
||||
@patch.object(CourseOverview, '_get_course_has_highlights', return_value=True)
|
||||
def test_schedule_config_creation_enabled_instructor_paced(self, _mock_highlights):
|
||||
self.assert_schedule_created(is_self_paced=False, experience_type=ScheduleExperience.EXPERIENCES.course_updates)
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
@patch('openedx.core.djangoapps.schedules.signals.course_has_highlights')
|
||||
def test_create_schedule_course_updates_experience(self, mock_course_has_highlights, mock_get_current_site):
|
||||
site = SiteFactory.create()
|
||||
mock_course_has_highlights.return_value = True
|
||||
mock_get_current_site.return_value = site
|
||||
@patch.object(CourseOverview, '_get_course_has_highlights', return_value=True)
|
||||
def test_create_schedule_course_updates_experience(self, _mock_highlights):
|
||||
self.assert_schedule_created(experience_type=ScheduleExperience.EXPERIENCES.course_updates)
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
@patch('openedx.core.djangoapps.schedules.signals.segment.track')
|
||||
@patch('openedx.core.djangoapps.schedules.signals.random.random', return_value=0.2)
|
||||
@ddt.data(
|
||||
(0, True),
|
||||
(0.1, True),
|
||||
(0.3, False),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_create_schedule_hold_backs(
|
||||
self,
|
||||
hold_back_ratio,
|
||||
expect_schedule_created,
|
||||
mock_random, # lint-amnesty, pylint: disable=unused-argument
|
||||
mock_track,
|
||||
mock_get_current_site
|
||||
):
|
||||
schedule_config = ScheduleConfigFactory.create(enabled=True, hold_back_ratio=hold_back_ratio)
|
||||
mock_get_current_site.return_value = schedule_config.site
|
||||
if expect_schedule_created:
|
||||
self.assert_schedule_created()
|
||||
assert not mock_track.called
|
||||
else:
|
||||
self.assert_schedule_not_created()
|
||||
mock_track.assert_called_once()
|
||||
assert mock_track.call_args[1].get('event_name') == 'edx.bi.schedule.suppressed'
|
||||
|
||||
@patch('openedx.core.djangoapps.schedules.signals.log.exception')
|
||||
@patch('openedx.core.djangoapps.schedules.signals.Schedule.objects.create')
|
||||
def test_create_schedule_error(self, mock_create_schedule, mock_log, mock_get_current_site):
|
||||
site = SiteFactory.create()
|
||||
mock_get_current_site.return_value = site
|
||||
ScheduleConfigFactory.create(site=site)
|
||||
def test_create_schedule_error(self, mock_create_schedule, mock_log):
|
||||
mock_create_schedule.side_effect = ValueError('Fake error')
|
||||
self.assert_schedule_not_created()
|
||||
mock_log.assert_called_once()
|
||||
assert 'Encountered error in creating a Schedule for CourseEnrollment' in mock_log.call_args[0][0]
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
def test_course_start_date_in_future(self, mock_get_current_site):
|
||||
def test_course_start_date_in_future(self):
|
||||
"""
|
||||
Test that the schedule start date will be set to course's start date
|
||||
if course starts after enrollment
|
||||
"""
|
||||
site = SiteFactory.create()
|
||||
mock_get_current_site.return_value = site
|
||||
course = _create_course_run(self_paced=True, start_day_offset=5) # course starts in future
|
||||
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
|
||||
assert _strip_secs(enrollment.schedule.start_date) == _strip_secs(course.start)
|
||||
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
def test_course_already_started(self, mock_get_current_site):
|
||||
def test_course_already_started(self):
|
||||
"""
|
||||
Test that the schedule start date will be set to the date enrollment was
|
||||
created if course has already started
|
||||
"""
|
||||
site = SiteFactory.create()
|
||||
mock_get_current_site.return_value = site
|
||||
course = _create_course_run(self_paced=True, start_day_offset=-5) # course already started
|
||||
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
|
||||
assert _strip_secs(enrollment.schedule.start_date) == _strip_secs(enrollment.created)
|
||||
@@ -173,7 +94,6 @@ class CreateScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, pylint: d
|
||||
|
||||
@ddt.ddt
|
||||
@skip_unless_lms
|
||||
@patch('openedx.core.djangoapps.schedules.signals.get_current_site')
|
||||
class UpdateScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
VERIFICATION_DEADLINE_DAYS = 14
|
||||
@@ -189,9 +109,7 @@ class UpdateScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, pylint: d
|
||||
deadline_delta = datetime.timedelta(days=self.VERIFICATION_DEADLINE_DAYS)
|
||||
assert _strip_secs(schedule.upgrade_deadline) == _strip_secs(expected_start) + deadline_delta
|
||||
|
||||
def test_updated_when_course_not_started(self, mock_get_current_site):
|
||||
mock_get_current_site.return_value = self.site
|
||||
|
||||
def test_updated_when_course_not_started(self):
|
||||
course = _create_course_run(self_paced=True, start_day_offset=5) # course starts in future
|
||||
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
|
||||
self.assert_schedule_dates(enrollment.schedule, enrollment.course.start)
|
||||
@@ -201,9 +119,7 @@ class UpdateScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, pylint: d
|
||||
enrollment = CourseEnrollment.objects.get(id=enrollment.id)
|
||||
self.assert_schedule_dates(enrollment.schedule, course.start) # start set to new course start
|
||||
|
||||
def test_updated_when_course_already_started(self, mock_get_current_site):
|
||||
mock_get_current_site.return_value = self.site
|
||||
|
||||
def test_updated_when_course_already_started(self):
|
||||
course = _create_course_run(self_paced=True, start_day_offset=-5) # course starts in past
|
||||
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
|
||||
self.assert_schedule_dates(enrollment.schedule, enrollment.created)
|
||||
@@ -213,9 +129,7 @@ class UpdateScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, pylint: d
|
||||
enrollment = CourseEnrollment.objects.get(id=enrollment.id)
|
||||
self.assert_schedule_dates(enrollment.schedule, course.start) # start set to new course start
|
||||
|
||||
def test_updated_when_new_start_in_past(self, mock_get_current_site):
|
||||
mock_get_current_site.return_value = self.site
|
||||
|
||||
def test_updated_when_new_start_in_past(self):
|
||||
course = _create_course_run(self_paced=True, start_day_offset=5) # course starts in future
|
||||
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
|
||||
previous_start = enrollment.course.start
|
||||
@@ -228,17 +142,11 @@ class UpdateScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, pylint: d
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
|
||||
class ResetScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.config = ScheduleConfigFactory(create_schedules=True)
|
||||
|
||||
site_patch = patch('openedx.core.djangoapps.schedules.signals.get_current_site', return_value=self.config.site)
|
||||
self.addCleanup(site_patch.stop)
|
||||
site_patch.start()
|
||||
|
||||
self.config = ScheduleConfigFactory()
|
||||
self.course = _create_course_run(self_paced=True)
|
||||
self.enrollment = CourseEnrollmentFactory(
|
||||
course_id=self.course.id,
|
||||
|
||||
@@ -5,10 +5,9 @@ Tests for schedules utils
|
||||
import datetime
|
||||
|
||||
import ddt
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from mock import patch # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from pytz import utc # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from pytz import utc
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from openedx.core.djangoapps.schedules.models import Schedule
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory
|
||||
from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule
|
||||
@@ -22,11 +21,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@skip_unless_lms
|
||||
class ResetSelfPacedScheduleTests(SharedModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
def create_schedule(self, offset=0): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
self.config = ScheduleConfigFactory(create_schedules=True) # lint-amnesty, pylint: disable=attribute-defined-outside-init
|
||||
|
||||
site_patch = patch('openedx.core.djangoapps.schedules.signals.get_current_site', return_value=self.config.site)
|
||||
self.addCleanup(site_patch.stop)
|
||||
site_patch.start()
|
||||
self.config = ScheduleConfigFactory() # lint-amnesty, pylint: disable=attribute-defined-outside-init
|
||||
|
||||
start = datetime.datetime.now(utc) - datetime.timedelta(days=100)
|
||||
self.course = CourseFactory.create(start=start, self_paced=True) # lint-amnesty, pylint: disable=attribute-defined-outside-init
|
||||
|
||||
@@ -14,7 +14,7 @@ from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.courseware.models import DynamicUpgradeDeadlineConfiguration
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.djangoapps.schedules.models import Schedule
|
||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from openedx.features.course_duration_limits.access import (
|
||||
@@ -113,10 +113,7 @@ class TestAccess(CacheIsolationTestCase):
|
||||
course_id=enrollment.course.id,
|
||||
mode_slug=CourseMode.AUDIT,
|
||||
)
|
||||
ScheduleFactory.create(
|
||||
enrollment=enrollment,
|
||||
upgrade_deadline=schedule_upgrade_deadline,
|
||||
)
|
||||
Schedule.objects.update(upgrade_deadline=schedule_upgrade_deadline)
|
||||
|
||||
duration_limit_upgrade_deadline = get_user_course_expiration_date(enrollment.user, enrollment.course)
|
||||
assert duration_limit_upgrade_deadline is not None
|
||||
@@ -155,10 +152,7 @@ class TestAccess(CacheIsolationTestCase):
|
||||
course_id=enrollment.course.id,
|
||||
mode_slug=CourseMode.AUDIT,
|
||||
)
|
||||
ScheduleFactory.create(
|
||||
enrollment=enrollment,
|
||||
start_date=datetime(2017, 1, 1, tzinfo=UTC),
|
||||
)
|
||||
Schedule.objects.update(start_date=datetime(2017, 1, 1, tzinfo=UTC))
|
||||
|
||||
content_availability_date = max(enrollment.created, enrollment.course.start)
|
||||
access_duration = get_user_course_duration(enrollment.user, enrollment.course)
|
||||
|
||||
@@ -30,7 +30,7 @@ from openedx.core.djangoapps.django_comment_common.models import (
|
||||
FORUM_ROLE_GROUP_MODERATOR,
|
||||
FORUM_ROLE_MODERATOR
|
||||
)
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.djangoapps.schedules.models import Schedule
|
||||
from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID, CONTENT_TYPE_GATE_GROUP_IDS
|
||||
from openedx.features.course_duration_limits.access import get_user_course_expiration_date
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
@@ -337,12 +337,12 @@ class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
|
||||
else:
|
||||
expired_staff = role_factory.create(password=TEST_PASSWORD, course_key=self.course.id)
|
||||
|
||||
ScheduleFactory(
|
||||
start_date=self.THREE_YEARS_AGO,
|
||||
enrollment__mode=CourseMode.AUDIT,
|
||||
enrollment__course_id=self.course.id,
|
||||
enrollment__user=expired_staff
|
||||
CourseEnrollmentFactory.create(
|
||||
mode=CourseMode.AUDIT,
|
||||
course_id=self.course.id,
|
||||
user=expired_staff,
|
||||
)
|
||||
Schedule.objects.update(start_date=self.THREE_YEARS_AGO)
|
||||
CourseDurationLimitConfig.objects.create(
|
||||
enabled=True,
|
||||
course=CourseOverview.get_from_id(self.course.id),
|
||||
@@ -385,12 +385,12 @@ class CourseExpirationTestCase(ModuleStoreTestCase, MasqueradeMixin):
|
||||
role = RoleFactory(name=role_name, course_id=self.course.id)
|
||||
role.users.add(expired_staff)
|
||||
|
||||
ScheduleFactory(
|
||||
start_date=self.THREE_YEARS_AGO,
|
||||
enrollment__mode=CourseMode.AUDIT,
|
||||
enrollment__course_id=self.course.id,
|
||||
enrollment__user=expired_staff
|
||||
CourseEnrollmentFactory.create(
|
||||
mode=CourseMode.AUDIT,
|
||||
course_id=self.course.id,
|
||||
user=expired_staff,
|
||||
)
|
||||
Schedule.objects.update(start_date=self.THREE_YEARS_AGO)
|
||||
|
||||
CourseDurationLimitConfig.objects.create(
|
||||
enabled=True,
|
||||
|
||||
@@ -44,15 +44,12 @@ class ResetCourseDeadlinesViewTests(EventTestMixin, BaseCourseHomeTests, Masquer
|
||||
student_username = self.user.username
|
||||
student_user_id = self.user.id
|
||||
student_enrollment = CourseEnrollment.enroll(self.user, course.id)
|
||||
student_schedule = ScheduleFactory.create(
|
||||
start_date=timezone.now() - datetime.timedelta(days=100),
|
||||
enrollment=student_enrollment
|
||||
)
|
||||
staff_schedule = ScheduleFactory(
|
||||
start_date=timezone.now() - datetime.timedelta(days=30),
|
||||
enrollment__course__id=course.id,
|
||||
enrollment__user=self.staff_user,
|
||||
)
|
||||
student_enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100)
|
||||
student_enrollment.schedule.save()
|
||||
|
||||
staff_enrollment = CourseEnrollment.enroll(self.staff_user, course.id)
|
||||
staff_enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=30)
|
||||
staff_enrollment.schedule.save()
|
||||
|
||||
self.switch_to_staff()
|
||||
self.update_masquerade(course=course, username=student_username)
|
||||
@@ -60,10 +57,10 @@ class ResetCourseDeadlinesViewTests(EventTestMixin, BaseCourseHomeTests, Masquer
|
||||
with patch('openedx.features.course_experience.api.v1.views.dates_banner_should_display',
|
||||
return_value=(True, False)):
|
||||
self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': course.id})
|
||||
updated_schedule = Schedule.objects.get(id=student_schedule.id)
|
||||
updated_schedule = Schedule.objects.get(id=student_enrollment.schedule.id)
|
||||
assert updated_schedule.start_date.date() == datetime.datetime.today().date()
|
||||
updated_staff_schedule = Schedule.objects.get(id=staff_schedule.id)
|
||||
assert updated_staff_schedule.start_date == staff_schedule.start_date
|
||||
updated_staff_schedule = Schedule.objects.get(id=staff_enrollment.schedule.id)
|
||||
assert updated_staff_schedule.start_date == staff_enrollment.schedule.start_date
|
||||
self.assert_event_emitted(
|
||||
'edx.ui.lms.reset_deadlines.clicked',
|
||||
courserun_key=str(course.id),
|
||||
|
||||
@@ -44,7 +44,7 @@ from openedx.core.djangoapps.django_comment_common.models import (
|
||||
FORUM_ROLE_GROUP_MODERATOR,
|
||||
FORUM_ROLE_MODERATOR
|
||||
)
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
|
||||
from openedx.core.djangoapps.schedules.models import Schedule
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
|
||||
@@ -448,12 +448,8 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
url = course_home_url(course)
|
||||
|
||||
user = UserFactory.create(password=self.TEST_PASSWORD)
|
||||
ScheduleFactory(
|
||||
start_date=THREE_YEARS_AGO,
|
||||
enrollment__mode=CourseMode.VERIFIED,
|
||||
enrollment__course_id=course.id,
|
||||
enrollment__user=user
|
||||
)
|
||||
CourseEnrollment.enroll(user, self.course.id, mode=CourseMode.VERIFIED)
|
||||
Schedule.objects.update(start_date=THREE_YEARS_AGO)
|
||||
|
||||
# ensure that the user who has indefinite access
|
||||
self.client.login(username=user.username, password=self.TEST_PASSWORD)
|
||||
@@ -478,12 +474,8 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
url = course_home_url(course)
|
||||
|
||||
user = role_factory.create(password=self.TEST_PASSWORD, course_key=course.id)
|
||||
ScheduleFactory(
|
||||
start_date=THREE_YEARS_AGO,
|
||||
enrollment__mode=CourseMode.AUDIT,
|
||||
enrollment__course_id=course.id,
|
||||
enrollment__user=user
|
||||
)
|
||||
CourseEnrollment.enroll(user, self.course.id, mode=CourseMode.AUDIT)
|
||||
Schedule.objects.update(start_date=THREE_YEARS_AGO)
|
||||
|
||||
# ensure that the user has indefinite access
|
||||
self.client.login(username=user.username, password=self.TEST_PASSWORD)
|
||||
@@ -526,12 +518,8 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
url = course_home_url(course)
|
||||
|
||||
user = role_factory.create(password=self.TEST_PASSWORD)
|
||||
ScheduleFactory(
|
||||
start_date=THREE_YEARS_AGO,
|
||||
enrollment__mode=CourseMode.AUDIT,
|
||||
enrollment__course_id=course.id,
|
||||
enrollment__user=user
|
||||
)
|
||||
CourseEnrollment.enroll(user, self.course.id, mode=CourseMode.AUDIT)
|
||||
Schedule.objects.update(start_date=THREE_YEARS_AGO)
|
||||
|
||||
# ensure that the user who has indefinite access
|
||||
self.client.login(username=user.username, password=self.TEST_PASSWORD)
|
||||
@@ -557,7 +545,6 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
audit_enrollment = CourseEnrollment.enroll(audit_user, course.id, mode=CourseMode.AUDIT)
|
||||
audit_enrollment.created = THREE_YEARS_AGO + timedelta(days=1)
|
||||
audit_enrollment.save()
|
||||
ScheduleFactory(enrollment=audit_enrollment)
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
@@ -627,7 +614,7 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
audit_user = UserFactory(password=self.TEST_PASSWORD)
|
||||
self.client.login(username=audit_user.username, password=self.TEST_PASSWORD)
|
||||
audit_enrollment = CourseEnrollment.enroll(audit_user, course.id, mode=CourseMode.AUDIT)
|
||||
ScheduleFactory(start_date=THREE_YEARS_AGO, enrollment=audit_enrollment)
|
||||
Schedule.objects.update(start_date=THREE_YEARS_AGO)
|
||||
FBEEnrollmentExclusion.objects.create(
|
||||
enrollment=audit_enrollment
|
||||
)
|
||||
|
||||
@@ -128,11 +128,8 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase, MasqueradeMixin):
|
||||
"""Set up and enroll our fake user in the course."""
|
||||
cls.user = UserFactory(password=TEST_PASSWORD)
|
||||
for course in cls.courses:
|
||||
enrollment = CourseEnrollment.enroll(cls.user, course.id)
|
||||
ScheduleFactory.create(
|
||||
start_date=timezone.now() - datetime.timedelta(days=1),
|
||||
enrollment=enrollment
|
||||
)
|
||||
CourseEnrollment.enroll(cls.user, course.id)
|
||||
Schedule.objects.update(start_date=timezone.now() - datetime.timedelta(days=1))
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
@@ -238,53 +235,41 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase, MasqueradeMixin):
|
||||
@override_experiment_waffle_flag(RELATIVE_DATES_FLAG, active=True)
|
||||
def test_reset_course_deadlines(self):
|
||||
course = self.courses[0]
|
||||
enrollment = CourseEnrollment.objects.get(course_id=course.id)
|
||||
enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=30)
|
||||
enrollment.schedule.save()
|
||||
|
||||
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(days=30),
|
||||
enrollment__course__id=course.id,
|
||||
enrollment__user=staff,
|
||||
)
|
||||
CourseEnrollment.enroll(staff, course.id)
|
||||
|
||||
start_date = timezone.now() - datetime.timedelta(days=30)
|
||||
Schedule.objects.update(start_date=start_date)
|
||||
|
||||
self.client.login(username=staff.username, password=TEST_PASSWORD)
|
||||
self.update_masquerade(course=course, username=self.user.username)
|
||||
|
||||
post_dict = {'course_id': str(course.id)}
|
||||
self.client.post(reverse(RESET_COURSE_DEADLINES_NAME), post_dict)
|
||||
updated_schedule = Schedule.objects.get(id=student_schedule.id)
|
||||
updated_schedule = Schedule.objects.get(enrollment__user=self.user, enrollment__course_id=course.id)
|
||||
assert updated_schedule.start_date.date() == datetime.datetime.today().date()
|
||||
updated_staff_schedule = Schedule.objects.get(id=staff_schedule.id)
|
||||
assert updated_staff_schedule.start_date == staff_schedule.start_date
|
||||
updated_staff_schedule = Schedule.objects.get(enrollment__user=staff, enrollment__course_id=course.id)
|
||||
assert updated_staff_schedule.start_date == start_date
|
||||
|
||||
@override_experiment_waffle_flag(RELATIVE_DATES_FLAG, active=True)
|
||||
def test_reset_course_deadlines_masquerade_generic_student(self):
|
||||
course = self.courses[0]
|
||||
|
||||
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(days=30),
|
||||
enrollment__course__id=course.id,
|
||||
enrollment__user=staff,
|
||||
)
|
||||
CourseEnrollment.enroll(staff, course.id)
|
||||
|
||||
start_date = timezone.now() - datetime.timedelta(days=30)
|
||||
Schedule.objects.update(start_date=start_date)
|
||||
|
||||
self.client.login(username=staff.username, password=TEST_PASSWORD)
|
||||
self.update_masquerade(course=course)
|
||||
|
||||
post_dict = {'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)
|
||||
assert updated_student_schedule.start_date == student_schedule.start_date
|
||||
updated_staff_schedule = Schedule.objects.get(id=staff_schedule.id)
|
||||
updated_student_schedule = Schedule.objects.get(enrollment__user=self.user, enrollment__course_id=course.id)
|
||||
assert updated_student_schedule.start_date == start_date
|
||||
updated_staff_schedule = Schedule.objects.get(enrollment__user=staff, enrollment__course_id=course.id)
|
||||
assert updated_staff_schedule.start_date.date() == datetime.date.today()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user