Merge pull request #26663 from edx/mikix/enable-schedules

feat: turn on schedule creation by default
This commit is contained in:
Michael Terry
2021-02-23 14:34:11 -05:00
committed by GitHub
36 changed files with 184 additions and 529 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1408,7 +1408,7 @@ class ProgressPageTests(ProgressPageBaseTests):
self.assertContains(resp, u"Download Your Certificate")
@ddt.data(
(True, 55),
(True, 54),
(False, 54),
)
@ddt.unpack

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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