Switch ContentTypeGatingConfig.enabled_as_of and CourseDurationLimitConfig.enabled_as_of to datetimes

This commit is contained in:
Calen Pennington
2018-11-28 14:13:55 -05:00
parent af86f7856c
commit 31047260cc
20 changed files with 194 additions and 112 deletions

View File

@@ -5,7 +5,7 @@ import itertools
import json
import re
import unittest
from datetime import timedelta, date
from datetime import timedelta, datetime
import ddt
from completion.test_utils import submit_completions_for_testing, CompletionWaffleTestMixin
@@ -730,7 +730,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
Links will be removed from the course title, course image and button (View Course/Resume Course).
The course card should have an access expired message.
"""
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
self.override_waffle_switch(True)
course = CourseFactory.create(start=self.THREE_YEARS_AGO)

View File

@@ -3,7 +3,7 @@ Test the partitions and partitions service
"""
from datetime import date
from datetime import datetime
from django.test import TestCase
from mock import Mock, patch
@@ -439,7 +439,7 @@ class PartitionServiceBaseClass(PartitionTestCase):
ContentTypeGatingConfig.objects.create(
enabled=True,
enabled_as_of=date(2018, 1, 1),
enabled_as_of=datetime(2018, 1, 1),
studio_override_enabled=True
)
self.course = Mock(id=CourseLocator('org_0', 'course_0', 'run_0'))

View File

@@ -3,7 +3,7 @@
Performance tests for field overrides.
"""
import itertools
from datetime import datetime, date
from datetime import datetime
import ddt
import mock
@@ -204,7 +204,7 @@ class FieldOverridePerformanceTestCase(FieldOverrideTestMixin, ProceduralCourseT
"""
ContentTypeGatingConfig.objects.create(
enabled=True,
enabled_as_of=date(2018, 1, 1),
enabled_as_of=datetime(2018, 1, 1),
)
providers = {

View File

@@ -828,7 +828,7 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
@ddt.unpack
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_course_catalog_access_num_queries(self, user_attr_name, action, course_attr_name):
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime.date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime.datetime(2018, 1, 1))
course = getattr(self, course_attr_name)

View File

@@ -2,7 +2,7 @@
"""
Test the course_info xblock
"""
from datetime import date
from datetime import datetime
import ddt
import mock
from django.conf import settings
@@ -418,7 +418,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
def setUp(self):
super(SelfPacedCourseInfoTestCase, self).setUp()
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
self.setup_user()

View File

@@ -5,7 +5,7 @@ Tests courseware views.py
import itertools
import json
import unittest
from datetime import datetime, timedelta, date
from datetime import datetime, timedelta
from HTMLParser import HTMLParser
from urllib import quote, urlencode
from uuid import uuid4
@@ -218,7 +218,7 @@ class IndexQueryTestCase(ModuleStoreTestCase):
)
@ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
with self.store.default_store(store_type):
course = CourseFactory.create()
with self.store.bulk_operations(course.id):
@@ -1445,7 +1445,7 @@ class ProgressPageTests(ProgressPageBaseTests):
@ddt.unpack
def test_progress_queries_paced_courses(self, self_paced, query_count):
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
self.setup_course(self_paced=self_paced)
with self.assertNumQueries(query_count, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(1):
self._get_progress_page()
@@ -1457,7 +1457,7 @@ class ProgressPageTests(ProgressPageBaseTests):
)
@ddt.unpack
def test_progress_queries(self, enable_waffle, initial, subsequent):
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
self.setup_course()
with grades_waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=enable_waffle):
with self.assertNumQueries(
@@ -1668,7 +1668,7 @@ class ProgressPageTests(ProgressPageBaseTests):
Verify that expired banner message appears on progress page, if learner is enrolled
in audit mode.
"""
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
user = UserFactory.create()
self.assertTrue(self.client.login(username=user.username, password='test'))
add_course_mode(self.course, upgrade_deadline_expired=False)
@@ -2715,7 +2715,7 @@ class TestIndexViewWithCourseDurationLimits(ModuleStoreTestCase):
Test that the courseware contains the course expiration banner
when course_duration_limits are enabled.
"""
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
self.assertTrue(self.client.login(username=self.user.username, password='test'))
add_course_mode(self.course, upgrade_deadline_expired=False)
response = self.client.get(

View File

@@ -1,6 +1,6 @@
import json
import logging
from datetime import datetime, date
from datetime import datetime
import ddt
from django.urls import reverse
@@ -456,7 +456,7 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
num_cached_sql_queries,
mock_request
):
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
with modulestore().default_store(default_store):
course = CourseFactory.create(discussion_topics={'dummy discussion': {'id': 'dummy_discussion_id'}})

View File

@@ -299,7 +299,7 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest
Test that expired courses are only returned in v1 of API
when waffle flag enabled, and un-expired courses always returned
'''
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime.date(2018, 1, 1))
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime.datetime(2018, 1, 1))
courses = self._get_enrollment_data(api_version, expired)
self._assert_enrollment_results(api_version, courses, num_courses_returned)

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-11-28 19:07
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content_type_gating', '0002_auto_20181119_0959'),
]
operations = [
migrations.AlterField(
model_name='contenttypegatingconfig',
name='enabled_as_of',
field=models.DateTimeField(blank=True, default=None, help_text='If the configuration is Enabled, then all enrollments created after this date (UTC) will be affected.', null=True, verbose_name='Enabled As Of'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-11-28 20:21
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content_type_gating', '0003_auto_20181128_1407'),
]
operations = [
migrations.AlterField(
model_name='contenttypegatingconfig',
name='enabled_as_of',
field=models.DateTimeField(blank=True, default=None, help_text='If the configuration is Enabled, then all enrollments created after this date and time (UTC) will be affected.', null=True, verbose_name='Enabled As Of'),
),
]

View File

@@ -5,11 +5,11 @@ Content Type Gating Configuration Models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from experiments.models import ExperimentData
from student.models import CourseEnrollment
@@ -29,14 +29,14 @@ class ContentTypeGatingConfig(StackedConfigurationModel):
STACKABLE_FIELDS = ('enabled', 'enabled_as_of', 'studio_override_enabled')
enabled_as_of = models.DateField(
enabled_as_of = models.DateTimeField(
default=None,
null=True,
verbose_name=_('Enabled As Of'),
blank=True,
help_text=_(
'If the configuration is Enabled, then all enrollments '
'created after this date (UTC) will be affected.'
'created after this date and time (UTC) will be affected.'
)
)
studio_override_enabled = models.NullBooleanField(
@@ -87,7 +87,7 @@ class ContentTypeGatingConfig(StackedConfigurationModel):
# enrollment might be None if the user isn't enrolled. In that case,
# return enablement as if the user enrolled today
if enrollment is None:
return cls.enabled_for_course(course_key=course_key, target_date=datetime.utcnow().date())
return cls.enabled_for_course(course_key=course_key, target_datetime=timezone.now())
else:
# TODO: clean up as part of REV-100
experiment_data_holdback_key = EXPERIMENT_DATA_HOLDBACK_KEY.format(user)
@@ -104,48 +104,48 @@ class ContentTypeGatingConfig(StackedConfigurationModel):
if is_in_holdback:
return False
current_config = cls.current(course_key=enrollment.course_id)
return current_config.enabled_as_of_date(target_date=enrollment.created.date())
return current_config.enabled_as_of_datetime(target_datetime=enrollment.created)
@classmethod
def enabled_for_course(cls, course_key, target_date=None):
def enabled_for_course(cls, course_key, target_datetime=None):
"""
Return whether Content Type Gating is enabled for this course as of a particular date.
Content Type Gating is enabled for a course on a date if it is enabled either specifically,
or via a containing context, such as the org, site, or globally, and if the configuration
is specified to be ``enabled_as_of`` before ``target_date``.
is specified to be ``enabled_as_of`` before ``target_datetime``.
Only one of enrollment and (user, course_key) may be specified at a time.
Arguments:
course_key: The CourseKey of the course being queried.
target_date: The date to checked enablement as of. Defaults to the current date.
target_datetime: The datetime to checked enablement as of. Defaults to the current date and time.
"""
if CONTENT_TYPE_GATING_FLAG.is_enabled():
return True
if target_date is None:
target_date = datetime.utcnow().date()
if target_datetime is None:
target_datetime = timezone.now()
current_config = cls.current(course_key=course_key)
return current_config.enabled_as_of_date(target_date=target_date)
return current_config.enabled_as_of_datetime(target_datetime=target_datetime)
def clean(self):
if self.enabled and self.enabled_as_of is None:
raise ValidationError({'enabled_as_of': _('enabled_as_of must be set when enabled is True')})
def enabled_as_of_date(self, target_date):
def enabled_as_of_datetime(self, target_datetime):
"""
Return whether this Content Type Gating configuration context is enabled as of a date.
Return whether this Content Type Gating configuration context is enabled as of a date and time.
Arguments:
target_date (:class:`datetime.date`): The date that ``enabled_as_of`` must be equal to or before
target_datetime (:class:`datetime.datetime`): The datetime that ``enabled_as_of`` must be equal to or before
"""
if CONTENT_TYPE_GATING_FLAG.is_enabled():
return True
# Explicitly cast this to bool, so that when self.enabled is None the method doesn't return None
return bool(self.enabled and self.enabled_as_of <= target_date)
return bool(self.enabled and self.enabled_as_of <= target_datetime)
def __str__(self):
return "ContentTypeGatingConfig(enabled={!r}, enabled_as_of={!r}, studio_override_enabled={!r})".format(

View File

@@ -2,7 +2,7 @@
Test audit user's access to various content based on content-gating features.
"""
from datetime import date
from datetime import datetime
import ddt
from django.conf import settings
from django.test.client import RequestFactory
@@ -225,7 +225,7 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
course_id=self.courses['audit_only']['course'].id,
mode='audit'
)
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
@classmethod
def _create_course(cls, run, display_name, modes, component_types):
@@ -478,7 +478,7 @@ class TestConditionalContentAccess(TestConditionalContent):
def setUpClass(cls):
super(TestConditionalContentAccess, cls).setUpClass()
cls.factory = RequestFactory()
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
def setUp(self):
super(TestConditionalContentAccess, self).setUp()

View File

@@ -1,6 +1,8 @@
import ddt
from datetime import timedelta, date, datetime, time
from datetime import timedelta, datetime
import itertools
import ddt
from django.utils import timezone
from mock import Mock
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
@@ -38,12 +40,12 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
enrolled_before_enabled,
):
# Tweak the day to enable the config so that it is either before
# or after today (which is when the enrollment will be created)
# Tweak the datetime to enable the config so that it is either before
# or after now (which is when the enrollment will be created)
if enrolled_before_enabled:
enabled_as_of = date.today() + timedelta(days=1)
enabled_as_of = datetime.now() + timedelta(days=1)
else:
enabled_as_of = date.today() - timedelta(days=1)
enabled_as_of = datetime.now() - timedelta(days=1)
config = ContentTypeGatingConfig.objects.create(
enabled=True,
@@ -102,15 +104,15 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
config = ContentTypeGatingConfig.objects.create(
enabled=True,
course=self.course_overview,
enabled_as_of=date(2018, 1, 1),
enabled_as_of=timezone.now(),
)
# Tweak the day to check for course enablement so it is either
# Tweak the datetime to check for course enablement so it is either
# before or after when the configuration was enabled
if before_enabled:
target_date = config.enabled_as_of - timedelta(days=1)
target_datetime = config.enabled_as_of - timedelta(days=1)
else:
target_date = config.enabled_as_of + timedelta(days=1)
target_datetime = config.enabled_as_of + timedelta(days=1)
course_key = self.course_overview.id
@@ -118,7 +120,7 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
not before_enabled,
ContentTypeGatingConfig.enabled_for_course(
course_key=course_key,
target_date=target_date,
target_datetime=target_datetime,
)
)
@@ -141,21 +143,21 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
non_test_site_cfg_enabled = SiteConfigurationFactory.create(values={'course_org_filter': non_test_course_enabled.org})
non_test_site_cfg_disabled = SiteConfigurationFactory.create(values={'course_org_filter': non_test_course_disabled.org})
ContentTypeGatingConfig.objects.create(course=non_test_course_enabled, enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(course=non_test_course_enabled, enabled=True, enabled_as_of=datetime(2018, 1, 1))
ContentTypeGatingConfig.objects.create(course=non_test_course_disabled, enabled=False)
ContentTypeGatingConfig.objects.create(org=non_test_course_enabled.org, enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(org=non_test_course_enabled.org, enabled=True, enabled_as_of=datetime(2018, 1, 1))
ContentTypeGatingConfig.objects.create(org=non_test_course_disabled.org, enabled=False)
ContentTypeGatingConfig.objects.create(site=non_test_site_cfg_enabled.site, enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(site=non_test_site_cfg_enabled.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
ContentTypeGatingConfig.objects.create(site=non_test_site_cfg_disabled.site, enabled=False)
# Set up test objects
test_course = CourseOverviewFactory.create(org='test-org')
test_site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': test_course.org})
ContentTypeGatingConfig.objects.create(enabled=global_setting, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(course=test_course, enabled=course_setting, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(org=test_course.org, enabled=org_setting, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(site=test_site_cfg.site, enabled=site_setting, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=global_setting, enabled_as_of=datetime(2018, 1, 1))
ContentTypeGatingConfig.objects.create(course=test_course, enabled=course_setting, enabled_as_of=datetime(2018, 1, 1))
ContentTypeGatingConfig.objects.create(org=test_course.org, enabled=org_setting, enabled_as_of=datetime(2018, 1, 1))
ContentTypeGatingConfig.objects.create(site=test_site_cfg.site, enabled=site_setting, enabled_as_of=datetime(2018, 1, 1))
all_settings = [global_setting, site_setting, org_setting, course_setting]
expected_global_setting = self._resolve_settings([global_setting])
@@ -169,7 +171,7 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
self.assertEqual(expected_course_setting, ContentTypeGatingConfig.current(course_key=test_course.id).enabled)
def test_caching_global(self):
global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
global_config.save()
# Check that the global value is not retrieved from cache after save
@@ -189,7 +191,7 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
def test_caching_site(self):
site_cfg = SiteConfigurationFactory()
site_config = ContentTypeGatingConfig(site=site_cfg.site, enabled=True, enabled_as_of=date(2018, 1, 1))
site_config = ContentTypeGatingConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
site_config.save()
# Check that the site value is not retrieved from cache after save
@@ -207,7 +209,7 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
with self.assertNumQueries(1):
self.assertFalse(ContentTypeGatingConfig.current(site=site_cfg.site).enabled)
global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
global_config.save()
# Check that the site value is not updated in cache by changing the global value
@@ -217,7 +219,7 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
def test_caching_org(self):
course = CourseOverviewFactory.create(org='test-org')
site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': course.org})
org_config = ContentTypeGatingConfig(org=course.org, enabled=True, enabled_as_of=date(2018, 1, 1))
org_config = ContentTypeGatingConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1))
org_config.save()
# Check that the org value is not retrieved from cache after save
@@ -235,14 +237,14 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
with self.assertNumQueries(2):
self.assertFalse(ContentTypeGatingConfig.current(org=course.org).enabled)
global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
global_config.save()
# Check that the org value is not updated in cache by changing the global value
with self.assertNumQueries(0):
self.assertFalse(ContentTypeGatingConfig.current(org=course.org).enabled)
site_config = ContentTypeGatingConfig(site=site_cfg.site, enabled=True, enabled_as_of=date(2018, 1, 1))
site_config = ContentTypeGatingConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
site_config.save()
# Check that the org value is not updated in cache by changing the site value
@@ -252,7 +254,7 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
def test_caching_course(self):
course = CourseOverviewFactory.create(org='test-org')
site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': course.org})
course_config = ContentTypeGatingConfig(course=course, enabled=True, enabled_as_of=date(2018, 1, 1))
course_config = ContentTypeGatingConfig(course=course, enabled=True, enabled_as_of=datetime(2018, 1, 1))
course_config.save()
# Check that the org value is not retrieved from cache after save
@@ -270,21 +272,21 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
with self.assertNumQueries(2):
self.assertFalse(ContentTypeGatingConfig.current(course_key=course.id).enabled)
global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
global_config.save()
# Check that the org value is not updated in cache by changing the global value
with self.assertNumQueries(0):
self.assertFalse(ContentTypeGatingConfig.current(course_key=course.id).enabled)
site_config = ContentTypeGatingConfig(site=site_cfg.site, enabled=True, enabled_as_of=date(2018, 1, 1))
site_config = ContentTypeGatingConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
site_config.save()
# Check that the org value is not updated in cache by changing the site value
with self.assertNumQueries(0):
self.assertFalse(ContentTypeGatingConfig.current(course_key=course.id).enabled)
org_config = ContentTypeGatingConfig(org=course.org, enabled=True, enabled_as_of=date(2018, 1, 1))
org_config = ContentTypeGatingConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1))
org_config.save()
# Check that the org value is not updated in cache by changing the site value

View File

@@ -1,4 +1,4 @@
from datetime import date
from datetime import datetime
from mock import Mock, patch
from opaque_keys.edx.keys import CourseKey
@@ -15,7 +15,7 @@ class TestContentTypeGatingPartition(CacheIsolationTestCase):
def test_create_content_gating_partition_happy_path(self):
mock_course = Mock(id=self.course_key, user_partitions={})
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
with patch('openedx.features.content_type_gating.partitions.ContentTypeGatingPartitionScheme.create_user_partition') as mock_create:
partition = create_content_gating_partition(mock_course)
@@ -37,7 +37,7 @@ class TestContentTypeGatingPartition(CacheIsolationTestCase):
def test_create_content_gating_partition_no_scheme_installed(self):
mock_course = Mock(id=self.course_key, user_partitions={})
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
with patch('openedx.features.content_type_gating.partitions.UserPartition.get_scheme', side_effect=UserPartitionError):
partition = create_content_gating_partition(mock_course)
@@ -46,7 +46,7 @@ class TestContentTypeGatingPartition(CacheIsolationTestCase):
def test_create_content_gating_partition_partition_id_used(self):
mock_course = Mock(id=self.course_key, user_partitions={Mock(name='partition', id=CONTENT_GATING_PARTITION_ID): object()})
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
with patch('openedx.features.content_type_gating.partitions.LOG') as mock_log:
partition = create_content_gating_partition(mock_course)

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-11-28 19:07
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course_duration_limits', '0002_auto_20181119_0959'),
]
operations = [
migrations.AlterField(
model_name='coursedurationlimitconfig',
name='enabled_as_of',
field=models.DateTimeField(blank=True, default=None, help_text='If the configuration is Enabled, then all enrollments created after this date (UTC) will be affected.', null=True, verbose_name='Enabled As Of'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-11-28 20:21
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course_duration_limits', '0003_auto_20181128_1407'),
]
operations = [
migrations.AlterField(
model_name='coursedurationlimitconfig',
name='enabled_as_of',
field=models.DateTimeField(blank=True, default=None, help_text='If the configuration is Enabled, then all enrollments created after this date and time (UTC) will be affected.', null=True, verbose_name='Enabled As Of'),
),
]

View File

@@ -5,11 +5,11 @@ Course Duration Limit Configuration Models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from experiments.models import ExperimentData
from student.models import CourseEnrollment
@@ -29,14 +29,14 @@ class CourseDurationLimitConfig(StackedConfigurationModel):
STACKABLE_FIELDS = ('enabled', 'enabled_as_of')
enabled_as_of = models.DateField(
enabled_as_of = models.DateTimeField(
default=None,
null=True,
verbose_name=_('Enabled As Of'),
blank=True,
help_text=_(
'If the configuration is Enabled, then all enrollments '
'created after this date (UTC) will be affected.'
'created after this date and time (UTC) will be affected.'
)
)
@@ -78,7 +78,7 @@ class CourseDurationLimitConfig(StackedConfigurationModel):
# enrollment might be None if the user isn't enrolled. In that case,
# return enablement as if the user enrolled today
if enrollment is None:
return cls.enabled_for_course(course_key=course_key, target_date=datetime.utcnow().date())
return cls.enabled_for_course(course_key=course_key, target_datetime=timezone.now())
else:
# TODO: clean up as part of REV-100
experiment_data_holdback_key = EXPERIMENT_DATA_HOLDBACK_KEY.format(user)
@@ -95,48 +95,48 @@ class CourseDurationLimitConfig(StackedConfigurationModel):
if is_in_holdback:
return False
current_config = cls.current(course_key=enrollment.course_id)
return current_config.enabled_as_of_date(target_date=enrollment.created.date())
return current_config.enabled_as_of_datetime(target_datetime=enrollment.created)
@classmethod
def enabled_for_course(cls, course_key, target_date=None):
def enabled_for_course(cls, course_key, target_datetime=None):
"""
Return whether Course Duration Limits are enabled for this course as of a particular date.
Course Duration Limits are enabled for a course on a date if they are enabled either specifically,
or via a containing context, such as the org, site, or globally, and if the configuration
is specified to be ``enabled_as_of`` before ``target_date``.
is specified to be ``enabled_as_of`` before ``target_datetime``.
Only one of enrollment and (user, course_key) may be specified at a time.
Arguments:
course_key: The CourseKey of the course being queried.
target_date: The date to checked enablement as of. Defaults to the current date.
target_datetime: The datetime to checked enablement as of. Defaults to the current date and time.
"""
if CONTENT_TYPE_GATING_FLAG.is_enabled():
return True
if target_date is None:
target_date = datetime.utcnow().date()
if target_datetime is None:
target_datetime = timezone.now()
current_config = cls.current(course_key=course_key)
return current_config.enabled_as_of_date(target_date=target_date)
return current_config.enabled_as_of_datetime(target_datetime=target_datetime)
def clean(self):
if self.enabled and self.enabled_as_of is None:
raise ValidationError({'enabled_as_of': _('enabled_as_of must be set when enabled is True')})
def enabled_as_of_date(self, target_date):
def enabled_as_of_datetime(self, target_datetime):
"""
Return whether this Course Duration Limit configuration context is enabled as of a date.
Return whether this Course Duration Limit configuration context is enabled as of a date and time.
Arguments:
target_date (:class:`datetime.date`): The date that ``enabled_as_of`` must be equal to or before
target_datetime (:class:`datetime.datetime`): The datetime that ``enabled_as_of`` must be equal to or before
"""
if CONTENT_TYPE_GATING_FLAG.is_enabled():
return True
# Explicitly cast this to bool, so that when self.enabled is None the method doesn't return None
return bool(self.enabled and self.enabled_as_of <= target_date)
return bool(self.enabled and self.enabled_as_of <= target_datetime)
def __str__(self):
return "CourseDurationLimits(enabled={!r}, enabled_as_of={!r})".format(

View File

@@ -2,12 +2,12 @@
Tests of CourseDurationLimitConfig.
"""
from datetime import timedelta, date
from datetime import timedelta, datetime
import itertools
import ddt
from django.utils import timezone
from mock import Mock
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
@@ -46,12 +46,12 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
enrolled_before_enabled,
):
# Tweak the day to enable the config so that it is either before
# or after today (which is when the enrollment will be created)
# Tweak the datetime to enable the config so that it is either before
# or after now (which is when the enrollment will be created)
if enrolled_before_enabled:
enabled_as_of = date.today() + timedelta(days=1)
enabled_as_of = timezone.now() + timedelta(days=1)
else:
enabled_as_of = date.today() - timedelta(days=1)
enabled_as_of = timezone.now() - timedelta(days=1)
CourseDurationLimitConfig.objects.create(
enabled=True,
@@ -130,15 +130,15 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
config = CourseDurationLimitConfig.objects.create(
enabled=True,
course=self.course_overview,
enabled_as_of=date.today(),
enabled_as_of=timezone.now(),
)
# Tweak the day to check for course enablement so it is either
# Tweak the datetime to check for course enablement so it is either
# before or after when the configuration was enabled
if before_enabled:
target_date = config.enabled_as_of - timedelta(days=1)
target_datetime = config.enabled_as_of - timedelta(days=1)
else:
target_date = config.enabled_as_of + timedelta(days=1)
target_datetime = config.enabled_as_of + timedelta(days=1)
course_key = self.course_overview.id
@@ -146,7 +146,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
not before_enabled,
CourseDurationLimitConfig.enabled_for_course(
course_key=course_key,
target_date=target_date,
target_datetime=target_datetime,
)
)
@@ -200,7 +200,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
self.assertEqual(expected_course_setting, CourseDurationLimitConfig.current(course_key=test_course.id).enabled)
def test_caching_global(self):
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
global_config.save()
# Check that the global value is not retrieved from cache after save
@@ -220,7 +220,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
def test_caching_site(self):
site_cfg = SiteConfigurationFactory()
site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=date(2018, 1, 1))
site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
site_config.save()
# Check that the site value is not retrieved from cache after save
@@ -238,7 +238,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
with self.assertNumQueries(1):
self.assertFalse(CourseDurationLimitConfig.current(site=site_cfg.site).enabled)
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
global_config.save()
# Check that the site value is not updated in cache by changing the global value
@@ -248,7 +248,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
def test_caching_org(self):
course = CourseOverviewFactory.create(org='test-org')
site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': course.org})
org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=date(2018, 1, 1))
org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1))
org_config.save()
# Check that the org value is not retrieved from cache after save
@@ -266,14 +266,14 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
with self.assertNumQueries(2):
self.assertFalse(CourseDurationLimitConfig.current(org=course.org).enabled)
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
global_config.save()
# Check that the org value is not updated in cache by changing the global value
with self.assertNumQueries(0):
self.assertFalse(CourseDurationLimitConfig.current(org=course.org).enabled)
site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=date(2018, 1, 1))
site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
site_config.save()
# Check that the org value is not updated in cache by changing the site value
@@ -283,7 +283,7 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
def test_caching_course(self):
course = CourseOverviewFactory.create(org='test-org')
site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': course.org})
course_config = CourseDurationLimitConfig(course=course, enabled=True, enabled_as_of=date(2018, 1, 1))
course_config = CourseDurationLimitConfig(course=course, enabled=True, enabled_as_of=datetime(2018, 1, 1))
course_config.save()
# Check that the org value is not retrieved from cache after save
@@ -301,21 +301,21 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
with self.assertNumQueries(2):
self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=date(2018, 1, 1))
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
global_config.save()
# Check that the org value is not updated in cache by changing the global value
with self.assertNumQueries(0):
self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)
site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=date(2018, 1, 1))
site_config = CourseDurationLimitConfig(site=site_cfg.site, enabled=True, enabled_as_of=datetime(2018, 1, 1))
site_config.save()
# Check that the org value is not updated in cache by changing the site value
with self.assertNumQueries(0):
self.assertFalse(CourseDurationLimitConfig.current(course_key=course.id).enabled)
org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=date(2018, 1, 1))
org_config = CourseDurationLimitConfig(org=course.org, enabled=True, enabled_as_of=datetime(2018, 1, 1))
org_config.save()
# Check that the org value is not updated in cache by changing the site value

View File

@@ -2,7 +2,7 @@
"""
Tests for the course home page.
"""
from datetime import datetime, timedelta, date
from datetime import datetime, timedelta
import ddt
import mock
@@ -189,7 +189,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
"""
Verify that the view's query count doesn't regress.
"""
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
# Pre-fetch the view to populate any caches
course_home_url(self.course)
@@ -435,7 +435,7 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
Ensure that a user accessing an expired course sees a redirect to
the student dashboard, not a 404.
"""
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=date(2010, 1, 1))
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2010, 1, 1))
course = CourseFactory.create(start=THREE_YEARS_AGO)
url = course_home_url(course)
@@ -468,7 +468,7 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
Verify that enrolled users are NOT shown the course expiration banner and can
access the course home page if course audit only
"""
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=date(2010, 1, 1))
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2010, 1, 1))
audit_only_course = CourseFactory.create()
self.create_user_for_course(audit_only_course, CourseUserType.ENROLLED)
response = self.client.get(course_home_url(audit_only_course))
@@ -483,7 +483,7 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
Ensure that a user accessing an expired course that is in the holdback
does not get redirected to the student dashboard, not a 404.
"""
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=date(2010, 1, 1))
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2010, 1, 1))
course = CourseFactory.create(start=THREE_YEARS_AGO)
url = course_home_url(course)
@@ -584,7 +584,7 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
config = CourseDurationLimitConfig(
course=CourseOverview.get_from_id(self.course.id),
enabled=True,
enabled_as_of=date(2018, 1, 1)
enabled_as_of=datetime(2018, 1, 1)
)
config.save()

View File

@@ -1,7 +1,7 @@
"""
Tests for the course updates page.
"""
from datetime import date
from datetime import datetime
from courseware.courses import get_course_info_usage_key
from django.urls import reverse
@@ -122,7 +122,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
self.assertContains(response, 'Second Message')
def test_queries(self):
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=date(2018, 1, 1))
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
create_course_update(self.course, self.user, 'First Message')
# Pre-fetch the view to populate any caches