Merge pull request #19369 from edx/bfiller/REVE-88

Add feature-based enrollments search to support form
This commit is contained in:
Bill Filler
2018-12-13 14:01:45 -05:00
committed by GitHub
8 changed files with 379 additions and 1 deletions

View File

@@ -7,6 +7,9 @@ StackedConfigurationModel: A ConfigurationModel that can be overridden at site,
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from collections import defaultdict
from enum import Enum
from django.conf import settings
from django.db import models
from django.db.models import Q, F
@@ -21,6 +24,17 @@ from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
class Provenance(Enum):
"""
Provenance enum
"""
course = _('Course')
org = _('Org')
site = _('Site')
global_ = _('Global')
default = _('Default')
class StackedConfigurationModel(ConfigurationModel):
"""
A ConfigurationModel that stacks Global, Site, Org, and Course level
@@ -134,16 +148,91 @@ class StackedConfigurationModel(ConfigurationModel):
F('site').desc(nulls_first=True),
)
provenances = defaultdict(lambda: Provenance.default)
for override in overrides:
for field in stackable_fields:
value = field.value_from_object(override)
if value != field_defaults[field.name]:
values[field.name] = value
if override.course_id is not None:
provenances[field.name] = Provenance.course
elif override.org is not None:
provenances[field.name] = Provenance.org
elif override.site_id is not None:
provenances[field.name] = Provenance.site
else:
provenances[field.name] = Provenance.global_
current = cls(**values)
current.provenances = {field.name: provenances[field.name] for field in stackable_fields} # pylint: disable=attribute-defined-outside-init
cache.set(cache_key_name, current, cls.cache_timeout)
return current
@classmethod
def all_current_course_configs(cls):
"""
Return configuration for all courses
"""
all_courses = CourseOverview.objects.all()
all_site_configs = SiteConfiguration.objects.filter(
values__contains='course_org_filter', enabled=True
).select_related('site')
try:
default_site = Site.objects.get(id=settings.SITE_ID)
except Site.DoesNotExist:
default_site = RequestSite(crum.get_current_request())
sites_by_org = defaultdict(lambda: default_site)
site_cfg_org_filters = (
(site_cfg.site, site_cfg.values['course_org_filter'])
for site_cfg in all_site_configs
)
sites_by_org.update({
org: site
for (site, orgs) in site_cfg_org_filters
for org in (orgs if isinstance(orgs, list) else [orgs])
})
all_overrides = cls.objects.current_set()
overrides = {
(override.site_id, override.org, override.course_id): override
for override in all_overrides
}
stackable_fields = [cls._meta.get_field(field_name) for field_name in cls.STACKABLE_FIELDS]
field_defaults = {
field.name: field.get_default()
for field in stackable_fields
}
def provenance(course, field):
"""
Return provenance for given field
"""
for (config_key, provenance) in [
((None, None, course.id), Provenance.course),
((None, course.id.org, None), Provenance.org),
((sites_by_org[course.id.org].id, None, None), Provenance.site),
((None, None, None), Provenance.global_),
]:
config = overrides.get(config_key)
if config is None:
continue
value = field.value_from_object(config)
if value != field_defaults[field.name]:
return (value, provenance)
return (field_defaults[field.name], Provenance.default)
return {
course.id: {
field.name: provenance(course, field)
for field in stackable_fields
}
for course in all_courses
}
@classmethod
def cache_key_name(cls, site, org, course=None, course_key=None): # pylint: disable=arguments-differ
if course is not None and course_key is not None:

View File

@@ -4,9 +4,12 @@ import itertools
import ddt
from django.utils import timezone
from mock import Mock
import pytz
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
from opaque_keys.edx.locator import CourseLocator
from openedx.core.djangoapps.config_model_utils.models import Provenance
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
@@ -173,6 +176,62 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase):
self.assertEqual(expected_org_setting, ContentTypeGatingConfig.current(org=test_course.org).enabled)
self.assertEqual(expected_course_setting, ContentTypeGatingConfig.current(course_key=test_course.id).enabled)
def test_all_current_course_configs(self):
# Set up test objects
for global_setting in (True, False, None):
ContentTypeGatingConfig.objects.create(enabled=global_setting, enabled_as_of=datetime(2018, 1, 1))
for site_setting in (True, False, None):
test_site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': []})
ContentTypeGatingConfig.objects.create(site=test_site_cfg.site, enabled=site_setting, enabled_as_of=datetime(2018, 1, 1))
for org_setting in (True, False, None):
test_org = "{}-{}".format(test_site_cfg.id, org_setting)
test_site_cfg.values['course_org_filter'].append(test_org)
test_site_cfg.save()
ContentTypeGatingConfig.objects.create(org=test_org, enabled=org_setting, enabled_as_of=datetime(2018, 1, 1))
for course_setting in (True, False, None):
test_course = CourseOverviewFactory.create(
org=test_org,
id=CourseLocator(test_org, 'test_course', 'run-{}'.format(course_setting))
)
ContentTypeGatingConfig.objects.create(course=test_course, enabled=course_setting, enabled_as_of=datetime(2018, 1, 1))
with self.assertNumQueries(4):
all_configs = ContentTypeGatingConfig.all_current_course_configs()
# Deliberatly using the last all_configs that was checked after the 3rd pass through the global_settings loop
# We should be creating 3^4 courses (3 global values * 3 site values * 3 org values * 3 course values)
# Plus 1 for the edX/toy/2012_Fall course
self.assertEqual(len(all_configs), 3**4 + 1)
# Point-test some of the final configurations
self.assertEqual(
all_configs[CourseLocator('7-True', 'test_course', 'run-None')],
{
'enabled': (True, Provenance.org),
'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.course),
'studio_override_enabled': (None, Provenance.default),
}
)
self.assertEqual(
all_configs[CourseLocator('7-True', 'test_course', 'run-False')],
{
'enabled': (False, Provenance.course),
'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.course),
'studio_override_enabled': (None, Provenance.default),
}
)
self.assertEqual(
all_configs[CourseLocator('7-None', 'test_course', 'run-None')],
{
'enabled': (True, Provenance.site),
'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.course),
'studio_override_enabled': (None, Provenance.default),
}
)
def test_caching_global(self):
global_config = ContentTypeGatingConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
global_config.save()

View File

@@ -8,6 +8,10 @@ import itertools
import ddt
from django.utils import timezone
from mock import Mock
import pytz
from opaque_keys.edx.locator import CourseLocator
from openedx.core.djangoapps.config_model_utils.models import Provenance
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
@@ -199,6 +203,65 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
self.assertEqual(expected_org_setting, CourseDurationLimitConfig.current(org=test_course.org).enabled)
self.assertEqual(expected_course_setting, CourseDurationLimitConfig.current(course_key=test_course.id).enabled)
def test_all_current_course_configs(self):
# Set up test objects
for global_setting in (True, False, None):
CourseDurationLimitConfig.objects.create(enabled=global_setting, enabled_as_of=datetime(2018, 1, 1))
for site_setting in (True, False, None):
test_site_cfg = SiteConfigurationFactory.create(values={'course_org_filter': []})
CourseDurationLimitConfig.objects.create(
site=test_site_cfg.site, enabled=site_setting, enabled_as_of=datetime(2018, 1, 1)
)
for org_setting in (True, False, None):
test_org = "{}-{}".format(test_site_cfg.id, org_setting)
test_site_cfg.values['course_org_filter'].append(test_org)
test_site_cfg.save()
CourseDurationLimitConfig.objects.create(
org=test_org, enabled=org_setting, enabled_as_of=datetime(2018, 1, 1)
)
for course_setting in (True, False, None):
test_course = CourseOverviewFactory.create(
org=test_org,
id=CourseLocator(test_org, 'test_course', 'run-{}'.format(course_setting))
)
CourseDurationLimitConfig.objects.create(
course=test_course, enabled=course_setting, enabled_as_of=datetime(2018, 1, 1)
)
with self.assertNumQueries(4):
all_configs = CourseDurationLimitConfig.all_current_course_configs()
# Deliberatly using the last all_configs that was checked after the 3rd pass through the global_settings loop
# We should be creating 3^4 courses (3 global values * 3 site values * 3 org values * 3 course values)
# Plus 1 for the edX/toy/2012_Fall course
self.assertEqual(len(all_configs), 3**4 + 1)
# Point-test some of the final configurations
self.assertEqual(
all_configs[CourseLocator('7-True', 'test_course', 'run-None')],
{
'enabled': (True, Provenance.org),
'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.course),
}
)
self.assertEqual(
all_configs[CourseLocator('7-True', 'test_course', 'run-False')],
{
'enabled': (False, Provenance.course),
'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.course),
}
)
self.assertEqual(
all_configs[CourseLocator('7-None', 'test_course', 'run-None')],
{
'enabled': (True, Provenance.site),
'enabled_as_of': (datetime(2018, 1, 1, 5, tzinfo=pytz.UTC), Provenance.course),
}
)
def test_caching_global(self):
global_config = CourseDurationLimitConfig(enabled=True, enabled_as_of=datetime(2018, 1, 1))
global_config.save()