From 6b00eea9533aad4b733450ac14ff09250f52205a Mon Sep 17 00:00:00 2001 From: Andrey Canon Date: Thu, 12 Dec 2019 13:00:53 -0500 Subject: [PATCH] Create django model proxies, override current site middleware and override platform models. --- eox_tenant/apps.py | 3 + eox_tenant/backends/__init__.py | 0 eox_tenant/backends/base.py | 298 ---------------- eox_tenant/backends/database.py | 336 ------------------ .../backends/configuration_helpers_h_v1.py | 7 - .../site_configuration_module_i_v1.py | 17 + ...y => site_configuration_module_test_v1.py} | 6 + .../edxapp_wrapper/configuration_helpers.py | 10 - .../site_configuration_module.py | 17 + eox_tenant/middleware.py | 19 +- eox_tenant/models.py | 15 +- eox_tenant/monkey_patch/__init__.py | 16 + .../monkey_patch/monkey_patch_proxys.py | 160 +++++++++ eox_tenant/receivers_helpers.py | 49 +++ eox_tenant/settings/aws.py | 16 +- eox_tenant/settings/common.py | 5 +- eox_tenant/settings/test.py | 4 +- eox_tenant/signals.py | 14 +- eox_tenant/templatetags/ednx.py | 2 +- .../tenant_aware_functions/enrollments.py | 2 +- eox_tenant/test/test_auth_backend.py | 8 +- eox_tenant/test/test_backends.py | 30 -- eox_tenant/test/test_middleware.py | 33 ++ eox_tenant/test/test_models.py | 1 + eox_tenant/test/test_monkey_patching.py | 118 ++++++ eox_tenant/test/test_receivers_helpers.py | 79 ++++ eox_tenant/test/test_signals.py | 10 +- eox_tenant/test/test_wrappers.py | 6 +- eox_tenant/test_utils.py | 18 + requirements.in | 1 + requirements.txt | 1 + 31 files changed, 585 insertions(+), 716 deletions(-) delete mode 100644 eox_tenant/backends/__init__.py delete mode 100644 eox_tenant/backends/base.py delete mode 100644 eox_tenant/backends/database.py delete mode 100644 eox_tenant/edxapp_wrapper/backends/configuration_helpers_h_v1.py create mode 100644 eox_tenant/edxapp_wrapper/backends/site_configuration_module_i_v1.py rename eox_tenant/edxapp_wrapper/backends/{configuration_helpers_test_v1.py => site_configuration_module_test_v1.py} (62%) delete mode 100644 eox_tenant/edxapp_wrapper/configuration_helpers.py create mode 100644 eox_tenant/edxapp_wrapper/site_configuration_module.py create mode 100644 eox_tenant/monkey_patch/__init__.py create mode 100644 eox_tenant/monkey_patch/monkey_patch_proxys.py create mode 100644 eox_tenant/receivers_helpers.py delete mode 100644 eox_tenant/test/test_backends.py create mode 100644 eox_tenant/test/test_monkey_patching.py create mode 100644 eox_tenant/test/test_receivers_helpers.py diff --git a/eox_tenant/apps.py b/eox_tenant/apps.py index defbf379..d58b7ec8 100644 --- a/eox_tenant/apps.py +++ b/eox_tenant/apps.py @@ -59,3 +59,6 @@ def ready(self): """ from eox_tenant.permissions import load_permissions load_permissions() + + from eox_tenant.monkey_patch import load_monkey_patchs_overrides + load_monkey_patchs_overrides() diff --git a/eox_tenant/backends/__init__.py b/eox_tenant/backends/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/eox_tenant/backends/base.py b/eox_tenant/backends/base.py deleted file mode 100644 index 3e917c94..00000000 --- a/eox_tenant/backends/base.py +++ /dev/null @@ -1,298 +0,0 @@ -""" -Microsite configuration backend module. - -Contains the base classes for microsite backends. - -AbstractBaseMicrositeBackend is Abstract Base Class for the microsite configuration backend. -BaseMicrositeBackend is Base Class for microsite configuration backend. -""" - -from __future__ import absolute_import - -import abc -import os.path -import threading - -from django.conf import settings - -from eox_tenant.edxapp_wrapper.get_common_util import strip_port_from_host - - -# pylint: disable=unused-argument -class AbstractBaseMicrositeBackend(object): - """ - Abstract Base Class for the microsite backends. - """ - __metaclass__ = abc.ABCMeta - - def __init__(self, **kwargs): - pass - - @abc.abstractmethod - def set_config_by_domain(self, domain): - """ - For a given request domain, find a match in our microsite configuration - and make it available to the complete django request process - """ - raise NotImplementedError() - - @abc.abstractmethod - def get_value(self, val_name, default=None, **kwargs): - """ - Returns a value associated with the request's microsite, if present - """ - raise NotImplementedError() - - @abc.abstractmethod - def get_dict(self, dict_name, default=None, **kwargs): - """ - Returns a dictionary product of merging the request's microsite and - the default value. - This can be used, for example, to return a merged dictonary from the - settings.FEATURES dict, including values defined at the microsite - """ - raise NotImplementedError() - - @abc.abstractmethod - def is_request_in_microsite(self): - """ - This will return True/False if the current request is a request within a microsite - """ - raise NotImplementedError() - - @abc.abstractmethod - def has_override_value(self, val_name): - """ - Returns True/False whether a Microsite has a definition for the - specified named value - """ - raise NotImplementedError() - - @abc.abstractmethod - def get_all_config(self): - """ - This returns a set of orgs that are considered within all microsites. - This can be used, for example, to do filtering - """ - raise NotImplementedError() - - @abc.abstractmethod - def get_value_for_org(self, org, val_name, default=None): - """ - This returns a configuration value for a microsite which has an org_filter that matches - what is passed in - """ - raise NotImplementedError() - - @abc.abstractmethod - def get_all_orgs(self): - """ - This returns a set of orgs that are considered within a microsite. This can be used, - for example, to do filtering - """ - raise NotImplementedError() - - @abc.abstractmethod - def clear(self): - """ - Clears out any microsite configuration from the current request/thread - """ - raise NotImplementedError() - - -class BaseMicrositeBackend(AbstractBaseMicrositeBackend): - """ - Base class for Microsite backends. - """ - - def __init__(self, **kwargs): - super(BaseMicrositeBackend, self).__init__(**kwargs) - self.current_request_configuration = threading.local() - self.current_request_configuration.data = {} - self.current_request_configuration.cache = {} - - def has_configuration_set(self): - """ - Returns whether there is any Microsite configuration settings - """ - return getattr(settings, "MICROSITE_CONFIGURATION", False) - - def get_configuration(self): - """ - Returns the current request's microsite configuration. - if request's microsite configuration is not present returns empty dict. - """ - if not hasattr(self.current_request_configuration, 'data'): - return {} - - return self.current_request_configuration.data - - def get_key_from_cache(self, key): - """ - Retrieves a key from a cache scoped to the thread - """ - if hasattr(self.current_request_configuration, 'cache'): - return self.current_request_configuration.cache.get(key) - - def set_key_to_cache(self, key, value): - """ - Stores a key value pair in a cache scoped to the thread - """ - if not hasattr(self.current_request_configuration, 'cache'): - self.current_request_configuration.cache = {} - - self.current_request_configuration.cache[key] = value - - def set_config_by_domain(self, domain): - """ - For a given request domain, find a match in our microsite configuration - and then assign it to the thread local in order to make it available - to the complete Django request processing - """ - if not self.has_configuration_set() or not domain: - return - - for key, value in settings.MICROSITE_CONFIGURATION.items(): - subdomain = value.get('domain_prefix') - if subdomain and domain.startswith(subdomain): - self._set_microsite_config(key, subdomain, domain) - return - - # if no match on subdomain then see if there is a 'default' microsite defined - # if so, then use that - if 'default' in settings.MICROSITE_CONFIGURATION: - self._set_microsite_config('default', subdomain, domain) - return - - def get_value(self, val_name, default=None, **kwargs): - """ - Returns a value associated with the request's microsite, if present - """ - configuration = self.get_configuration() - return configuration.get(val_name, default) - - def get_dict(self, dict_name, default=None, **kwargs): - """ - Returns a dictionary product of merging the request's microsite and - the default value. - Supports storing a cache of the merged value to improve performance - """ - cached_dict = self.get_key_from_cache(dict_name) - if cached_dict: - return cached_dict - - default = default or {} - output = default.copy() - output.update(self.get_value(dict_name, {})) - - self.set_key_to_cache(dict_name, output) - return output - - def is_request_in_microsite(self): - """ - This will return if current request is a request within a microsite - """ - return bool(self.get_configuration()) - - def has_override_value(self, val_name): - """ - Will return True/False whether a Microsite has a definition for the - specified val_name - """ - configuration = self.get_configuration() - return val_name in configuration - - def get_all_config(self): - """ - This returns all configuration for all microsites - """ - config = {} - - for key, value in settings.MICROSITE_CONFIGURATION.iteritems(): - config[key] = value - - return config - - def get_value_for_org(self, org, val_name, default=None): - """ - This returns a configuration value for a microsite which has an org_filter that matches - what is passed in - """ - - if not self.has_configuration_set(): - return default - - # Filter at the setting file - for value in settings.MICROSITE_CONFIGURATION.itervalues(): - org_filter = value.get('course_org_filter', None) - if org_filter == org: - return value.get(val_name, default) - return default - - def get_all_orgs(self): - """ - This returns a set of orgs that are considered within a microsite. This can be used, - for example, to do filtering - """ - org_filter_set = set() - - if not self.has_configuration_set(): - return org_filter_set - - # Get the orgs in the db - for microsite in settings.MICROSITE_CONFIGURATION.itervalues(): - org_filter = microsite.get('course_org_filter') - if org_filter: - org_filter_set.add(org_filter) - - return org_filter_set - - def _set_microsite_config(self, microsite_config_key, subdomain, domain): - """ - Helper internal method to actually find the microsite configuration - """ - config = settings.MICROSITE_CONFIGURATION[microsite_config_key].copy() - config['subdomain'] = strip_port_from_host(subdomain) - config['microsite_config_key'] = microsite_config_key - config['site_domain'] = strip_port_from_host(domain) - - template_dir = settings.MICROSITE_ROOT_DIR / microsite_config_key / 'templates' - config['template_dir'] = template_dir - self.current_request_configuration.data = config - - def clear(self): - """ - Clears out any microsite configuration from the current request/thread - """ - self.current_request_configuration.data = {} - self.current_request_configuration.cache = {} - - def enable_microsites(self, log): - """ - Configure the paths for the microsites feature - """ - microsites_root = settings.MICROSITE_ROOT_DIR - - if os.path.isdir(microsites_root): - settings.STATICFILES_DIRS.insert(0, microsites_root) - settings.LOCALE_PATHS = [microsites_root / 'conf/locale'] + settings.LOCALE_PATHS - - log.info('Loading microsite path at %s', microsites_root) - else: - log.error( - 'Error loading %s. Directory does not exist', - microsites_root - ) - - def enable_microsites_pre_startup(self, log): - """ - The TEMPLATE_ENGINE directory to search for microsite templates - in non-mako templates must be loaded before the django startup - """ - microsites_root = settings.MICROSITE_ROOT_DIR - - if self.has_configuration_set(): - # BC-21: not completely sure this is required, it is related to comp_theming. - # 68312bdd2dac932b95c5720b3e7e42d0f788c8f0 - settings.MAKO_TEMPLATES['main'].insert(0, microsites_root) - settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].append(microsites_root) diff --git a/eox_tenant/backends/database.py b/eox_tenant/backends/database.py deleted file mode 100644 index d8182547..00000000 --- a/eox_tenant/backends/database.py +++ /dev/null @@ -1,336 +0,0 @@ -""" -Microsite backend that reads the configuration from the database -""" -from django.core.cache import cache -from django.conf import settings -from django.utils import six - -from eox_tenant.backends.base import BaseMicrositeBackend -from eox_tenant.edxapp_wrapper.get_common_util import strip_port_from_host - -MICROSITES_ALL_ORGS_CACHE_KEY = 'microsites.all_orgs_list' -MICROSITES_ALL_ORGS_CACHE_KEY_TIMEOUT = getattr( - settings, - 'MICROSITES_ALL_ORGS_CACHE_KEY_TIMEOUT', - 300 -) - - -class EdunextCompatibleDatabaseMicrositeBackend(BaseMicrositeBackend): - """ - Microsite backend that reads the microsites definitions from the database - using the custom models from edunext - """ - _microsite_manager = None - - @property - def microsite_manager(self): - """ - Return eox_tenant microsite manager. - """ - if not self._microsite_manager: - from eox_tenant.models import Microsite - self._microsite_manager = Microsite - return self._microsite_manager - - def has_configuration_set(self): - """ - We always require a configuration to function, so we can skip the query - """ - return settings.FEATURES.get('USE_MICROSITES', False) - - def iterate_sites(self): - """ - Return all the microsites from the database storing the results in the current request to avoid - quering the DB multiple times in the same request - """ - - cache_key = "all-microsites-iterator" - cached_list = self.get_key_from_cache(cache_key) - - if cached_list: - candidates = cached_list - else: - candidates = self.microsite_manager.objects.all() # pylint: disable=no-member - self.set_key_to_cache(cache_key, candidates) - - for microsite in candidates: - yield microsite - - def get_config_by_domain(self, domain): - """ - Return the configuration and key available for a given domain without applying it - to the local thread - """ - microsite = self.microsite_manager.get_microsite_for_domain(domain) - if microsite: - return microsite.values, microsite.key - else: - return {}, None - - def set_config_by_domain(self, domain): - """ - For a given request domain, find a match in our microsite configuration - and then assign it to the thread local in order to make it available - to the complete Django request processing - """ - if not self.has_configuration_set() or not domain: - return - microsite = self.microsite_manager.get_microsite_for_domain(domain) - if microsite: - self._set_microsite_config_from_obj(microsite.subdomain, domain, microsite) - return - - # if no match on subdomain then see if there is a 'default' microsite - # defined in the db. If so, then use it - try: - microsite = self.microsite_manager.objects.get(key='default') # pylint: disable=no-member - self._set_microsite_config_from_obj(microsite.subdomain, domain, microsite) - return - except self.microsite_manager.DoesNotExist: # pylint: disable=no-member - return - - def get_all_config(self): - """ - This returns all configuration for all microsites - """ - config = {} - - candidates = self.microsite_manager.objects.all() # pylint: disable=no-member - for microsite in candidates: - values = microsite.values - config[microsite.key] = values - - return config - - def get_value_for_org(self, org, val_name, default=None): - """ - Returns a configuration value for a microsite which has an org_filter that matches - what is passed in - """ - if not self.has_configuration_set(): - return default - - cache_key = "org-value-{}-{}".format(org, val_name) - cached_value = self.get_key_from_cache(cache_key) - if cached_value: - return cached_value - - # Filter at the db - for microsite in self.iterate_sites(): - current = microsite.values - org_filter = current.get('course_org_filter') - if org_filter: - if isinstance(org_filter, six.string_types): - org_filter = set([org_filter]) - if org in org_filter: - result = current.get(val_name, default) - self.set_key_to_cache(cache_key, result) - return result - - self.set_key_to_cache(cache_key, default) - return default - - def get_all_orgs(self): - """ - This returns a set of orgs that are considered within all microsites. - This can be used, for example, to do filtering - """ - # Check the cache first - org_filter_set = cache.get(MICROSITES_ALL_ORGS_CACHE_KEY) - if org_filter_set: - return org_filter_set - - org_filter_set = set() - if not self.has_configuration_set(): - return org_filter_set - - # Get the orgs in the db - for microsite in self.iterate_sites(): - current = microsite.values - org_filter = current.get('course_org_filter') - if org_filter and isinstance(org_filter, list): - for org in org_filter: - org_filter_set.add(org) - elif org_filter: - org_filter_set.add(org_filter) - - cache.set( - MICROSITES_ALL_ORGS_CACHE_KEY, - org_filter_set, - MICROSITES_ALL_ORGS_CACHE_KEY_TIMEOUT - ) - return org_filter_set - - def _set_microsite_config_from_obj(self, subdomain, domain, microsite_object): - """ - Helper internal method to actually find the microsite configuration - """ - config = microsite_object.values - config['subdomain'] = strip_port_from_host(subdomain) - config['site_domain'] = strip_port_from_host(domain) - config['microsite_config_key'] = microsite_object.key - self.current_request_configuration.data = config - - def set_key_to_cache(self, key, value): - """ - Stores a key value pair in a cache scoped to the thread - """ - if not hasattr(self.current_request_configuration, 'cache'): - self.current_request_configuration.cache = {} - - self.current_request_configuration.cache[key] = value - - -class TenantConfigCompatibleMicrositeBackend(EdunextCompatibleDatabaseMicrositeBackend): - """ - Backend that reads the configurations definitions from the database - using the custom models. - """ - - _backend_manager = None - TENANT_MICROSITES_ITERATOR_KEY = "tenant-microsites-iterator" - - @property - def backend_manager(self): - """ - Return eox_tenant microsite manager. - """ - if not self._backend_manager: - from eox_tenant.models import TenantConfig - self._backend_manager = TenantConfig - return self._backend_manager - - def iterate_sites_with_tenant(self): - """ - Return all the microsites from the database storing the results in the current request to avoid - quering the DB multiple times in the same request. - """ - - candidates = self.get_key_from_cache(self.TENANT_MICROSITES_ITERATOR_KEY) - - if not candidates: - candidates = self.backend_manager.objects.all() - self.set_key_to_cache(self.TENANT_MICROSITES_ITERATOR_KEY, candidates) - - for microsite in candidates: - yield microsite - - def get_config_by_domain(self, domain): - """ - Get the correct set of site configurations. - """ - from eox_tenant.models import TenantConfig - configurations, external_key = TenantConfig.get_configs_for_domain(domain) - - if not (configurations and external_key): - configurations, external_key = self._get_microsite_config_by_domain(domain) - - return configurations, external_key - - def _get_microsite_config_by_domain(self, domain): - """ - Return the configuration and key available for a given domain. - """ - from eox_tenant.models import Microsite - microsite = Microsite.get_microsite_for_domain(domain) - - if microsite: - return microsite.values, microsite.key - - return {}, None - - def get_all_orgs(self): - """ - Return a set of orgs that are considered within all microsites. - This can be used, for example, to do filtering. - """ - org_filter_set = super(TenantConfigCompatibleMicrositeBackend, self).get_all_orgs() - - # Get the orgs in the tenant config db model - for microsite in self.iterate_sites_with_tenant(): - current = microsite.lms_configs - org_filter = current.get('course_org_filter') - - if org_filter and isinstance(org_filter, list): - for org in org_filter: - org_filter_set.add(org) - elif org_filter: - org_filter_set.add(org_filter) - - return org_filter_set - - def set_config_by_domain(self, domain): - """ - For a given request domain, find a match in our microsite configuration - and then assign it to the thread local in order to make it available - to the complete Django request processing. - """ - if not self.has_configuration_set() or not domain: - return None - - config, tenant_key = self.get_config_by_domain(domain) - - if config and tenant_key: - self._set_config_from_obj(domain, config, tenant_key) - return None - - # If required, delegate to the old microsite backend method. - return super(TenantConfigCompatibleMicrositeBackend, self).set_config_by_domain(domain) - - def get_all_config(self): - """ - This returns all configuration for all microsites. - """ - config = super(TenantConfigCompatibleMicrositeBackend, self).get_all_config() - - # Apply also tenant configs (temporary compat layer). - for microsite in self.iterate_sites_with_tenant(): - config[microsite.external_key] = microsite.lms_configs - - return config - - def get_value_for_org(self, org, val_name, default=None): - """ - Returns a configuration value for a microsite which has an org_filter that matches - what is passed in. - """ - - if not self.has_configuration_set(): - return default - - cache_key = "org-value-{}-{}".format(org, val_name) - cached_value = self.get_key_from_cache(cache_key) - - if cached_value: - return cached_value - - # Filter at the db - for microsite in self.iterate_sites_with_tenant(): - current = microsite.lms_configs - org_filter = current.get('course_org_filter') - - if org_filter: - if isinstance(org_filter, six.string_types): - org_filter = set([org_filter]) - if org in org_filter: - result = current.get(val_name, default) - self.set_key_to_cache(cache_key, result) - return result - - # If required, delegate to the old microsite backend method. - return super(TenantConfigCompatibleMicrositeBackend, self).get_value_for_org( - org, - val_name, - default, - ) - - def _set_config_from_obj(self, domain, config, key): - """ - Helper internal method to actually find the microsite configuration. - """ - config = config - config['subdomain'] = strip_port_from_host(domain) - config['site_domain'] = strip_port_from_host(domain) - config['microsite_config_key'] = key - self.current_request_configuration.data = config diff --git a/eox_tenant/edxapp_wrapper/backends/configuration_helpers_h_v1.py b/eox_tenant/edxapp_wrapper/backends/configuration_helpers_h_v1.py deleted file mode 100644 index 3eff28ba..00000000 --- a/eox_tenant/edxapp_wrapper/backends/configuration_helpers_h_v1.py +++ /dev/null @@ -1,7 +0,0 @@ -""" backend """ -from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers # pylint: disable=import-error - - -def get_configuration_helpers(): - """ backend function """ - return configuration_helpers diff --git a/eox_tenant/edxapp_wrapper/backends/site_configuration_module_i_v1.py b/eox_tenant/edxapp_wrapper/backends/site_configuration_module_i_v1.py new file mode 100644 index 00000000..82b5595f --- /dev/null +++ b/eox_tenant/edxapp_wrapper/backends/site_configuration_module_i_v1.py @@ -0,0 +1,17 @@ +""" backend """ +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers # pylint: disable=import-error +from openedx.core.djangoapps.site_configuration import models # pylint: disable=import-error + + +def get_configuration_helpers(): + """ backend function """ + return configuration_helpers + + +def get_site_configuration_models(): + """ + Backend function. + + Return . + """ + return models diff --git a/eox_tenant/edxapp_wrapper/backends/configuration_helpers_test_v1.py b/eox_tenant/edxapp_wrapper/backends/site_configuration_module_test_v1.py similarity index 62% rename from eox_tenant/edxapp_wrapper/backends/configuration_helpers_test_v1.py rename to eox_tenant/edxapp_wrapper/backends/site_configuration_module_test_v1.py index bd594088..cc5d98d6 100644 --- a/eox_tenant/edxapp_wrapper/backends/configuration_helpers_test_v1.py +++ b/eox_tenant/edxapp_wrapper/backends/site_configuration_module_test_v1.py @@ -1,4 +1,5 @@ """ Backend test abstraction. """ +from eox_tenant.test_utils import TestSiteConfigurationModels def get_configuration_helpers(): @@ -8,3 +9,8 @@ def get_configuration_helpers(): except ImportError: configuration_helpers = object return configuration_helpers + + +def get_site_configuration_models(): + """ Backend to get the configuration helper. """ + return TestSiteConfigurationModels() diff --git a/eox_tenant/edxapp_wrapper/configuration_helpers.py b/eox_tenant/edxapp_wrapper/configuration_helpers.py deleted file mode 100644 index 6dc5e058..00000000 --- a/eox_tenant/edxapp_wrapper/configuration_helpers.py +++ /dev/null @@ -1,10 +0,0 @@ -""" Backend abstraction. """ -from importlib import import_module -from django.conf import settings - - -def get_configuration_helpers(*args, **kwargs): - """ Get BaseMicrositeBackend. """ - backend_function = settings.GET_CONFIGURATION_HELPERS - backend = import_module(backend_function) - return backend.get_configuration_helpers(*args, **kwargs) diff --git a/eox_tenant/edxapp_wrapper/site_configuration_module.py b/eox_tenant/edxapp_wrapper/site_configuration_module.py new file mode 100644 index 00000000..6fa5bd12 --- /dev/null +++ b/eox_tenant/edxapp_wrapper/site_configuration_module.py @@ -0,0 +1,17 @@ +""" Backend abstraction. """ +from importlib import import_module +from django.conf import settings + + +def get_configuration_helpers(*args, **kwargs): + """ Get configuration_helpers function. """ + backend_function = settings.GET_SITE_CONFIGURATION_MODULE + backend = import_module(backend_function) + return backend.get_configuration_helpers(*args, **kwargs) + + +def get_site_configuration_models(*args, **kwargs): + """ Get the module models from . """ + backend_function = settings.GET_SITE_CONFIGURATION_MODULE + backend = import_module(backend_function) + return backend.get_site_configuration_models(*args, **kwargs) diff --git a/eox_tenant/middleware.py b/eox_tenant/middleware.py index 619902d0..f4b74112 100644 --- a/eox_tenant/middleware.py +++ b/eox_tenant/middleware.py @@ -11,15 +11,18 @@ import re from django.conf import settings +from django.contrib.sites.shortcuts import get_current_site from django.http import Http404, HttpResponseNotFound from django.utils import six +from django.utils.deprecation import MiddlewareMixin from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey -from eox_tenant.edxapp_wrapper.configuration_helpers import get_configuration_helpers from eox_tenant.edxapp_wrapper.edxmako_module import get_edxmako_module +from eox_tenant.edxapp_wrapper.site_configuration_module import get_configuration_helpers from eox_tenant.edxapp_wrapper.theming_helpers import get_theming_helpers +from eox_tenant.monkey_patch.monkey_patch_proxys import TenantSiteConfigProxy configuration_helper = get_configuration_helpers() # pylint: disable=invalid-name theming_helper = get_theming_helpers() @@ -90,3 +93,17 @@ def process_request(self, request): {'domain': domain, } ) ) + + +class EoxTenantCurrentSiteMiddleware(MiddlewareMixin): + """ + Middleware class that define the site and its configuration. + """ + + def process_request(self, request): + """ + Get the current site for a given request a set the configuration. + """ + site = get_current_site(request) + site.configuration = TenantSiteConfigProxy() + request.site = site diff --git a/eox_tenant/models.py b/eox_tenant/models.py index 27396755..64b91d3b 100644 --- a/eox_tenant/models.py +++ b/eox_tenant/models.py @@ -6,11 +6,22 @@ import collections import json -from django.db import models, connection +from django.conf import settings +from django.db import connection, models from django.utils.translation import ugettext_lazy as _ - from jsonfield.fields import JSONField +from eox_tenant.edxapp_wrapper.site_configuration_module import get_site_configuration_models + +SiteConfigurationModels = get_site_configuration_models() +TENANT_ALL_ORGS_CACHE_KEY = "tenant.all_orgs_list" +EOX_TENANT_CACHE_KEY_TIMEOUT = getattr( + settings, + "EOX_TENANT__CACHE_KEY_TIMEOUT", + 300 +) +TENANT_MICROSITES_ITERATOR_KEY = "tenant-microsites-iterator" + class Microsite(models.Model): """ diff --git a/eox_tenant/monkey_patch/__init__.py b/eox_tenant/monkey_patch/__init__.py new file mode 100644 index 00000000..fdebfffd --- /dev/null +++ b/eox_tenant/monkey_patch/__init__.py @@ -0,0 +1,16 @@ +""" +Eox Tenant Monkey Patch +================== +This module makes it possible to override the some platform Models using new proxy models. +""" +from eox_tenant.monkey_patch.monkey_patch_proxys import TenantSiteConfigProxy +from eox_tenant.edxapp_wrapper.site_configuration_module import get_site_configuration_models + +SiteConfigurationModels = get_site_configuration_models() + + +def load_monkey_patchs_overrides(): + """ + Here are all the necessary overrides for the platform models. + """ + SiteConfigurationModels.SiteConfiguration = TenantSiteConfigProxy diff --git a/eox_tenant/monkey_patch/monkey_patch_proxys.py b/eox_tenant/monkey_patch/monkey_patch_proxys.py new file mode 100644 index 00000000..b2ab1527 --- /dev/null +++ b/eox_tenant/monkey_patch/monkey_patch_proxys.py @@ -0,0 +1,160 @@ +""" +Monkey patch proxys that allows to override the platform models. +""" +import logging +import json +from itertools import chain + +import six +from django.conf import settings +from django.core.cache import cache + +from eox_tenant.edxapp_wrapper.site_configuration_module import get_site_configuration_models +from eox_tenant.models import TenantConfig, Microsite + +SiteConfigurationModels = get_site_configuration_models() +TENANT_ALL_ORGS_CACHE_KEY = "tenant.all_orgs_list" +EOX_TENANT_CACHE_KEY_TIMEOUT = getattr( + settings, + "EOX_TENANT__CACHE_KEY_TIMEOUT", + 300 +) +TENANT_MICROSITES_ITERATOR_KEY = "tenant-microsites-iterator" +logger = logging.getLogger(__name__) + + +class TenantSiteConfigProxy(SiteConfigurationModels.SiteConfiguration): + """ + This a is Proxy model for SiteConfiguration from . + This allows to add or override methods using as base the SiteConfiguration model. + More information in https://docs.djangoproject.com/en/3.0/topics/db/models/#proxy-models + """ + + class Meta: + """ Set as a proxy model. """ + proxy = True + + def __unicode__(self): + key = getattr(settings, "USE_EOX_TENANT", "No tenant is active at the moment") + return u"".format(key) + + @property + def enabled(self): + """ + Return the value of USE_EOX_TENANT to activate site_configurations. + """ + return getattr(settings, "USE_EOX_TENANT", False) + + @enabled.setter + def enabled(self, value): + """ + We ignore the setter since this is a read proxy. + """ + pass + + def get_value(self, name, default=None): + """ + Return Configuration value from the Tenant loaded in the settings object + as if this was a SiteConfiguration class. + """ + try: + return getattr(settings, name, default) + except AttributeError as error: + logger.exception("Invalid data at the TenantConfigProxy get_value. \n [%s]", error) + + return default + + def save(self, *args, **kwargs): + """ + Don't allow to save TenantSiteConfigProxy model in database. + """ + pass + + @classmethod + def get_all_orgs(cls): + """ + This returns a set of orgs that are considered within all microsites and TenantConfig. + This can be used, for example, to do filtering + """ + # Check the cache first + org_filter_set = cache.get(TENANT_ALL_ORGS_CACHE_KEY) + if org_filter_set: + return org_filter_set + + org_filter_set = set() + if not cls.has_configuration_set(): + return org_filter_set + + tenant_config = TenantConfig.objects.values_list("lms_configs") + microsite_config = Microsite.objects.values_list("values") # pylint: disable=no-member + + for config in chain(tenant_config, microsite_config): + try: + current = json.loads(config[0]) + org_filter = current.get("course_org_filter", {}) + except IndexError: + continue + + if org_filter and isinstance(org_filter, list): + for org in org_filter: + org_filter_set.add(org) + elif org_filter: + org_filter_set.add(org_filter) + + cls.set_key_to_cache(TENANT_ALL_ORGS_CACHE_KEY, org_filter_set) + + return org_filter_set + + @classmethod + def get_value_for_org(cls, org, val_name, default=None): + """ + Returns a configuration value for a microsite or TenantConfig which has an org_filter that matches + what is passed in. + """ + + if not cls.has_configuration_set(): + return default + + cache_key = "org-value-{}-{}".format(org, val_name) + cached_value = cache.get(cache_key) + + if cached_value: + return cached_value + + tenant_config = TenantConfig.objects.values_list("lms_configs") + microsite_config = Microsite.objects.values_list("values") # pylint: disable=no-member + + for config in chain(tenant_config, microsite_config): + try: + current = json.loads(config[0]) + org_filter = current.get("course_org_filter", {}) + except IndexError: + continue + + if org_filter: + if isinstance(org_filter, six.string_types): + org_filter = set([org_filter]) + if org in org_filter: + result = current.get(val_name, default) + cls.set_key_to_cache(cache_key, result) + return result + + return default + + @classmethod + def has_configuration_set(cls): + """ + We always require a configuration to function, so we can skip the query + """ + return getattr(settings, "USE_EOX_TENANT", False) + + @classmethod + def set_key_to_cache(cls, key, value): + """ + Stores a key value pair in a cache scoped to the thread + """ + cache.set( + key, + value, + EOX_TENANT_CACHE_KEY_TIMEOUT + ) diff --git a/eox_tenant/receivers_helpers.py b/eox_tenant/receivers_helpers.py new file mode 100644 index 00000000..b3f65077 --- /dev/null +++ b/eox_tenant/receivers_helpers.py @@ -0,0 +1,49 @@ +""" +Common utilities for use along signals file. +""" + +from eox_tenant.models import TenantConfig, Microsite + + +def get_tenant_config_by_domain(domain): + """ + Reach for the configuration for a given domain. + + **Arguments** + domain: String parameter. + + **Returns** + configurations: dict + external_key: String + + **Example** + + configuration = { + "EDNX_USE_SIGNAL":True, + "ENABLE_MKTG_SITE":True, + "LMS_ROOT_URL":"http://courses.www.localhost:18000/", + "SESSION_COOKIE_DOMAIN":".www.localhost", + "SITE_NAME":"courses.www.localhost:18000", + "MKTG_URLS":{ + "ABOUT":"about", + "BLOG":"", + } + ... + } + + external_key = "this_is_my_key" + """ + configurations, external_key = TenantConfig.get_configs_for_domain(domain) + + if not (configurations and external_key): + + microsite = Microsite.get_microsite_for_domain(domain) + + if microsite: + configurations = microsite.values + external_key = microsite.key + else: + configurations = {} + external_key = None + + return configurations, external_key diff --git a/eox_tenant/settings/aws.py b/eox_tenant/settings/aws.py index 9e9391e9..697d0547 100644 --- a/eox_tenant/settings/aws.py +++ b/eox_tenant/settings/aws.py @@ -6,7 +6,7 @@ EDX_AUTH_BACKEND = 'openedx.core.djangoapps.oauth_dispatch.dot_overrides.backends.EdxRateLimitedAllowAllUsersModelBackend' # pylint: disable=line-too-long EOX_TENANT_AUTH_BACKEND = 'eox_tenant.auth.TenantAwareAuthBackend' -EDX_CMS_PRE_MICROSITE_MIDDLEWARE = 'openedx.core.djangoapps.header_control.middleware.HeaderControlMiddleware' +DJANGO_CURRENT_SITE_MIDDLEWARE = 'django.contrib.sites.middleware.CurrentSiteMiddleware' def plugin_settings(settings): # pylint: disable=function-redefined @@ -23,9 +23,9 @@ def plugin_settings(settings): # pylint: disable=function-redefined 'CHANGE_DOMAIN_DEFAULT_SITE_NAME', settings.CHANGE_DOMAIN_DEFAULT_SITE_NAME ) - settings.MICROSITES_ALL_ORGS_CACHE_KEY_TIMEOUT = getattr(settings, 'ENV_TOKENS', {}).get( - 'MICROSITES_ALL_ORGS_CACHE_KEY_TIMEOUT', - settings.MICROSITES_ALL_ORGS_CACHE_KEY_TIMEOUT + settings.EOX_TENANT_CACHE_KEY_TIMEOUT = getattr(settings, 'ENV_TOKENS', {}).get( + 'EOX_TENANT_CACHE_KEY_TIMEOUT', + settings.EOX_TENANT_CACHE_KEY_TIMEOUT ) settings.EOX_TENANT_EDX_AUTH_BACKEND = getattr(settings, 'ENV_TOKENS', {}).get( 'EOX_TENANT_EDX_AUTH_BACKEND', @@ -47,12 +47,20 @@ def plugin_settings(settings): # pylint: disable=function-redefined 'EOX_TENANT_ASYNC_TASKS_HANDLER_DICT', settings.EOX_TENANT_ASYNC_TASKS_HANDLER_DICT ) + settings.USE_EOX_TENANT = getattr(settings, 'ENV_TOKENS', {}).get( + 'USE_EOX_TENANT', + settings.USE_EOX_TENANT + ) + # Override the default site settings.SITE_ID = getattr(settings, 'ENV_TOKENS', {}).get( 'SITE_ID', settings.SITE_ID ) + if DJANGO_CURRENT_SITE_MIDDLEWARE in settings.MIDDLEWARE_CLASSES: + settings.MIDDLEWARE_CLASSES[settings.MIDDLEWARE_CLASSES.index(DJANGO_CURRENT_SITE_MIDDLEWARE)] = 'eox_tenant.middleware.EoxTenantCurrentSiteMiddleware' # pylint: disable=line-too-long + if settings.SERVICE_VARIANT == "lms": if settings.EOX_TENANT_APPEND_LMS_MIDDLEWARE_CLASSES: settings.MIDDLEWARE_CLASSES += [ diff --git a/eox_tenant/settings/common.py b/eox_tenant/settings/common.py index 42a7c65f..a60e3990 100644 --- a/eox_tenant/settings/common.py +++ b/eox_tenant/settings/common.py @@ -36,9 +36,9 @@ def plugin_settings(settings): More info: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/plugins/README.rst """ # Plugin settings. - settings.MICROSITES_ALL_ORGS_CACHE_KEY_TIMEOUT = 300 + settings.EOX_TENANT_CACHE_KEY_TIMEOUT = 300 settings.GET_BRANDING_API = 'eox_tenant.edxapp_wrapper.backends.branding_api_h_v1' - settings.GET_CONFIGURATION_HELPERS = 'eox_tenant.edxapp_wrapper.backends.configuration_helpers_h_v1' + settings.GET_SITE_CONFIGURATION_MODULE = 'eox_tenant.edxapp_wrapper.backends.site_configuration_module_i_v1' settings.GET_THEMING_HELPERS = 'eox_tenant.edxapp_wrapper.backends.theming_helpers_h_v1' settings.EOX_TENANT_EDX_AUTH_BACKEND = "eox_tenant.edxapp_wrapper.backends.edx_auth_i_v1" settings.EOX_TENANT_USERS_BACKEND = 'eox_tenant.edxapp_wrapper.backends.users_i_v1' @@ -48,6 +48,7 @@ def plugin_settings(settings): settings.CHANGE_DOMAIN_DEFAULT_SITE_NAME = "stage.edunext.co" settings.EOX_TENANT_LOAD_PERMISSIONS = True settings.EOX_TENANT_APPEND_LMS_MIDDLEWARE_CLASSES = False + settings.USE_EOX_TENANT = True settings.EOX_TENANT_ASYNC_TASKS_HANDLER_DICT = { "openedx.core.djangoapps.schedules.tasks.ScheduleRecurringNudge": "get_host_from_siteid", diff --git a/eox_tenant/settings/test.py b/eox_tenant/settings/test.py index 5e75db38..623e36bb 100644 --- a/eox_tenant/settings/test.py +++ b/eox_tenant/settings/test.py @@ -23,7 +23,7 @@ class SettingsClass(object): if app not in INSTALLED_APPS: INSTALLED_APPS.append(app) -GET_CONFIGURATION_HELPERS = 'eox_tenant.edxapp_wrapper.backends.configuration_helpers_test_v1' +GET_SITE_CONFIGURATION_MODULE = 'eox_tenant.edxapp_wrapper.backends.site_configuration_module_test_v1' GET_THEMING_HELPERS = 'eox_tenant.edxapp_wrapper.backends.theming_helpers_test_v1' COURSE_KEY_PATTERN = r'(?P[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+)' @@ -49,7 +49,7 @@ def plugin_settings(settings): # pylint: disable=function-redefined """ settings.FEATURES['USE_MICROSITE_AVAILABLE_SCREEN'] = False settings.FEATURES['USE_REDIRECTION_MIDDLEWARE'] = False - settings.GET_CONFIGURATION_HELPERS = 'eox_tenant.edxapp_wrapper.backends.configuration_helpers_test_v1' + settings.GET_SITE_CONFIGURATION_MODULE = 'eox_tenant.edxapp_wrapper.backends.site_configuration_module_test_v1' settings.GET_THEMING_HELPERS = 'eox_tenant.edxapp_wrapper.backends.theming_helpers_test_v1' settings.EOX_TENANT_SKIP_FILTER_FOR_TESTS = True settings.EOX_TENANT_LOAD_PERMISSIONS = False diff --git a/eox_tenant/signals.py b/eox_tenant/signals.py index 90734baf..183d6dd8 100644 --- a/eox_tenant/signals.py +++ b/eox_tenant/signals.py @@ -30,6 +30,7 @@ from eox_tenant.backends.database import TenantConfigCompatibleMicrositeBackend from eox_tenant.async_utils import AsyncTaskHandler +from eox_tenant.receivers_helpers import get_tenant_config_by_domain LOG = logging.getLogger(__name__) @@ -41,7 +42,7 @@ def _update_settings(domain): """ Perform the override procedure on the settings object """ - config, tenant_key = _get_tenant_config(domain) + config, tenant_key = get_tenant_config_by_domain(domain) if not config.get("EDNX_USE_SIGNAL"): LOG.info("Site %s, does not use eox_tenant signals", domain) @@ -156,17 +157,6 @@ def _ttl_reached(): return False -def _get_tenant_config(domain): - """ - Reach for the configuration for a given domain - - Using the model directly introduces a circular dependency. - That is why we go through the MicrositeBacked implementation. - """ - backend = TenantConfigCompatibleMicrositeBackend() - return backend.get_config_by_domain(domain) - - def start_tenant(sender, environ, **kwargs): # pylint: disable=unused-argument """ This function runs every time a request is started. diff --git a/eox_tenant/templatetags/ednx.py b/eox_tenant/templatetags/ednx.py index 1e5c5ee3..93d0af05 100644 --- a/eox_tenant/templatetags/ednx.py +++ b/eox_tenant/templatetags/ednx.py @@ -8,7 +8,7 @@ from django.contrib.staticfiles.storage import staticfiles_storage from django.utils.translation import get_language_bidi from eox_tenant.edxapp_wrapper.branding_api import get_branding_api -from eox_tenant.edxapp_wrapper.configuration_helpers import get_configuration_helpers +from eox_tenant.edxapp_wrapper.site_configuration_module import get_configuration_helpers from eox_tenant.edxapp_wrapper.theming_helpers import get_theming_helpers configuration_helpers = get_configuration_helpers() diff --git a/eox_tenant/tenant_aware_functions/enrollments.py b/eox_tenant/tenant_aware_functions/enrollments.py index d4fc6117..8592fdb5 100644 --- a/eox_tenant/tenant_aware_functions/enrollments.py +++ b/eox_tenant/tenant_aware_functions/enrollments.py @@ -4,7 +4,7 @@ from django.conf import settings -from eox_tenant.edxapp_wrapper.configuration_helpers import get_configuration_helpers +from eox_tenant.edxapp_wrapper.site_configuration_module import get_configuration_helpers from eox_tenant.edxapp_wrapper.theming_helpers import get_theming_helpers diff --git a/eox_tenant/test/test_auth_backend.py b/eox_tenant/test/test_auth_backend.py index f65181d0..525108fc 100644 --- a/eox_tenant/test/test_auth_backend.py +++ b/eox_tenant/test/test_auth_backend.py @@ -122,11 +122,12 @@ def test_authentication_with_signupsource(self, edx_get_current_request_mock, FEATURES={ 'EDNX_ENABLE_STRICT_LOGIN': True }) + @mock.patch('crum.get_current_request') @mock.patch('eox_tenant.edxapp_wrapper.auth.get_edx_auth_failed') @mock.patch('eox_tenant.edxapp_wrapper.auth.get_edx_auth_backend') @mock.patch('eox_tenant.test_utils.test_theming_helpers.get_current_request') def test_authentication_without_signupsource(self, edx_get_current_request_mock, - edx_auth_backend_mock, edx_auth_failed_mock): + edx_auth_backend_mock, edx_auth_failed_mock, crum_mock): """ Test if the user can authenticate in a domain where the user does not have signupsource """ @@ -139,6 +140,7 @@ def test_authentication_without_signupsource(self, edx_get_current_request_mock, request.META['HTTP_HOST'] = http_host edx_get_current_request_mock.return_value = request + crum_mock.return_value = request from eox_tenant.auth import TenantAwareAuthBackend @@ -187,11 +189,12 @@ def test_authentication_allowed_patterns(self, edx_get_current_request_mock, FEATURES={ 'EDNX_ENABLE_STRICT_LOGIN': True }) + @mock.patch('crum.get_current_request') @mock.patch('eox_tenant.edxapp_wrapper.auth.get_edx_auth_failed') @mock.patch('eox_tenant.edxapp_wrapper.auth.get_edx_auth_backend') @mock.patch('eox_tenant.test_utils.test_theming_helpers.get_current_request') def test_authentication_without_allowed_patterns(self, edx_get_current_request_mock, - edx_auth_backend_mock, edx_auth_failed_mock): + edx_auth_backend_mock, edx_auth_failed_mock, crum_mock): """ Test if the user can authenticate using an email that does not match with the allowed patterns """ @@ -204,6 +207,7 @@ def test_authentication_without_allowed_patterns(self, edx_get_current_request_m request.META['HTTP_HOST'] = http_host edx_get_current_request_mock.return_value = request + crum_mock.return_value = request from eox_tenant.auth import TenantAwareAuthBackend diff --git a/eox_tenant/test/test_backends.py b/eox_tenant/test/test_backends.py deleted file mode 100644 index 90d24ae2..00000000 --- a/eox_tenant/test/test_backends.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/python -""" -Module for backends tests. -""" -from django.test import TestCase - -from eox_tenant.backends.database import ( - EdunextCompatibleDatabaseMicrositeBackend, - TenantConfigCompatibleMicrositeBackend, -) - - -class BackendsTest(TestCase): - """ - Test backends methods. - """ - - def test_get_config_for_tenant(self): - """ - Test get configurations. - """ - backend = TenantConfigCompatibleMicrositeBackend() - backend.get_config_by_domain("example.domain") - - def test_get_config_for_microsite(self): - """ - Test get configurations. - """ - backend = EdunextCompatibleDatabaseMicrositeBackend() - backend.get_config_by_domain("example.domain") diff --git a/eox_tenant/test/test_middleware.py b/eox_tenant/test/test_middleware.py index c10eed0b..b08b26ba 100644 --- a/eox_tenant/test/test_middleware.py +++ b/eox_tenant/test/test_middleware.py @@ -4,12 +4,14 @@ """ import mock +from django.contrib.sites.models import Site from django.test import TestCase, RequestFactory, override_settings from django.http import Http404 from eox_tenant.middleware import ( MicrositeCrossBrandingFilterMiddleware, AvailableScreenMiddleware, + EoxTenantCurrentSiteMiddleware, ) @@ -118,3 +120,34 @@ def test_request_triggers_not_found( theming_helper_mock.is_request_in_themed_site.assert_called_once() http_resp_not_found_mock.assert_called_once() self.assertEqual(result, 'redirect_response') + + +class EoxTenantCurrentSiteMiddlewareTest(TestCase): + """ + Test EoxTenantCurrentSiteMiddleware. + """ + + def setUp(self): + """ Setup. """ + self.request_factory = RequestFactory(SERVER_NAME='test-domain.com') + self.middleware_instance = EoxTenantCurrentSiteMiddleware() + + Site.objects.create( + domain='test-domain.com', + name='test-site', + ) + + @mock.patch('eox_tenant.middleware.TenantSiteConfigProxy') + def test_override_site_configuration(self, create_site_mock): + """ + Test that the Site object has the right configuration. + """ + with self.settings(ALLOWED_HOSTS=['test-domain.com']): + request = self.request_factory.get('custom/path/') + new_configuration = 'new_configuration' + create_site_mock.return_value = new_configuration + + self.middleware_instance.process_request(request) + + create_site_mock.assert_called_once() + self.assertEqual(request.site.configuration, new_configuration) # pylint: disable=no-member diff --git a/eox_tenant/test/test_models.py b/eox_tenant/test/test_models.py index 73719fb0..55be9eda 100644 --- a/eox_tenant/test/test_models.py +++ b/eox_tenant/test/test_models.py @@ -2,6 +2,7 @@ """ TODO: add me """ +from __future__ import absolute_import from django.test import TestCase from django.core.exceptions import ValidationError diff --git a/eox_tenant/test/test_monkey_patching.py b/eox_tenant/test/test_monkey_patching.py new file mode 100644 index 00000000..b72888b3 --- /dev/null +++ b/eox_tenant/test/test_monkey_patching.py @@ -0,0 +1,118 @@ +""" +Test file to store the monkey_patch test module. +""" +from __future__ import absolute_import +from django.test import TestCase + +from eox_tenant.monkey_patch.monkey_patch_proxys import TenantSiteConfigProxy +from eox_tenant.models import Microsite, TenantConfig + + +class TenantSiteConfigProxyTest(TestCase): + """ + Test TenantSiteConfigProxy. + """ + + def setUp(self): + """ + This method creates Microsite, TenantConfig and Route objects and in database. + """ + Microsite.objects.create( # pylint: disable=no-member + subdomain="first.test.prod.edunext", + key="test_fake_key", + values={ + "course_org_filter": "test1-org", + "value-test": "Hello-World1", + } + ) + + Microsite.objects.create( # pylint: disable=no-member + subdomain="second.test.prod.edunext", + values={ + "course_org_filter": ["test2-org", "test3-org"], + "value-test": "Hello-World2", + } + ) + + TenantConfig.objects.create( + external_key="tenant-key1", + lms_configs={ + "course_org_filter": "test4-org" + }, + studio_configs={}, + theming_configs={}, + meta={}, + ) + + TenantConfig.objects.create( + external_key="tenant-key2", + lms_configs={ + "course_org_filter": ["test5-org", "test1-org"], + "value-test": "Hello-World3", + }, + studio_configs={}, + theming_configs={}, + meta={}, + ) + + def test_get_all_orgs(self): + """ + Test to get all the orgs for Microsite and TenantConfig objects. + """ + org_list = set([ + "test1-org", + "test2-org", + "test3-org", + "test4-org", + "test5-org", + ]) + + self.assertTrue(org_list == TenantSiteConfigProxy.get_all_orgs()) + + def test_get_value_for_org(self): + """ + Test to get an specific value for a given org. + """ + self.assertEqual( + TenantSiteConfigProxy.get_value_for_org( + org="test1-org", + val_name="value-test", + default=None, + ), + "Hello-World3", + ) + + self.assertEqual( + TenantSiteConfigProxy.get_value_for_org( + org="test3-org", + val_name="value-test", + default=None, + ), + "Hello-World2", + ) + + self.assertEqual( + TenantSiteConfigProxy.get_value_for_org( + org="test4-org", + val_name="value-test", + default="Default", + ), + "Default", + ) + + def test_create_site_configuration(self): + """ + Test that a new TenantSiteConfigProxy instance is created with + the current settings. + """ + with self.settings(USE_EOX_TENANT=False, EDNX_USE_SIGNAL=False): + site_configuration = TenantSiteConfigProxy() + self.assertFalse(site_configuration.enabled) + self.assertFalse(site_configuration.get_value("USE_EOX_TENANT")) + self.assertFalse(site_configuration.get_value("EDNX_USE_SIGNAL")) + + with self.settings(USE_EOX_TENANT=True, EDNX_USE_SIGNAL=True): + site_configuration = TenantSiteConfigProxy() + self.assertTrue(site_configuration.enabled) + self.assertTrue(site_configuration.get_value("USE_EOX_TENANT")) + self.assertTrue(site_configuration.get_value("EDNX_USE_SIGNAL")) diff --git a/eox_tenant/test/test_receivers_helpers.py b/eox_tenant/test/test_receivers_helpers.py new file mode 100644 index 00000000..ddab95af --- /dev/null +++ b/eox_tenant/test/test_receivers_helpers.py @@ -0,0 +1,79 @@ +""" +Module for receivers_helpers tests. +""" + +from __future__ import absolute_import +from django.test import TestCase + +from eox_tenant.models import ( + Microsite, + Route, + TenantConfig, +) +from eox_tenant.receivers_helpers import get_tenant_config_by_domain + + +class ReceiversHelpersTests(TestCase): + """ + Test class for the methods inside the receivers_helpers file. + """ + + def setUp(self): + """ + Setup database. + """ + Microsite.objects.create( # pylint: disable=no-member + subdomain="first.test.prod.edunext", + key="test_fake_key", + values={ + "course_org_filter": "test1-org", + "value-test": "Hello-World1", + } + ) + + TenantConfig.objects.create( + external_key="tenant-key1", + lms_configs={ + "course_org_filter": ["test5-org", "test1-org"], + "value-test": "Hello-World3", + }, + studio_configs={}, + theming_configs={}, + meta={}, + ) + + Route.objects.create( # pylint: disable=no-member + domain="domain1", + config=TenantConfig.objects.get(external_key="tenant-key1"), + ) + + def test_tenant_get_config_by_domain(self): + """ + Test to get the configuration and external key for a given domain. + """ + configurations, external_key = get_tenant_config_by_domain("domain1") + + self.assertEqual(external_key, "tenant-key1") + self.assertDictEqual( + configurations, + { + "course_org_filter": ["test5-org", "test1-org"], + "value-test": "Hello-World3", + }, + ) + + configurations, external_key = get_tenant_config_by_domain("domain2") + + self.assertEqual(external_key, None) + self.assertDictEqual(configurations, {}) + + configurations, external_key = get_tenant_config_by_domain("first.test.prod.edunext") + + self.assertEqual(external_key, "test_fake_key") + self.assertDictEqual( + configurations, + { + "course_org_filter": "test1-org", + "value-test": "Hello-World1", + }, + ) diff --git a/eox_tenant/test/test_signals.py b/eox_tenant/test/test_signals.py index 80babcea..a427b5ba 100644 --- a/eox_tenant/test/test_signals.py +++ b/eox_tenant/test/test_signals.py @@ -209,7 +209,7 @@ def tearDown(self): from django.conf import settings settings._setup() # pylint: disable=protected-access - @patch('eox_tenant.signals._get_tenant_config') + @patch('eox_tenant.signals.get_tenant_config_by_domain') def test_udpate_settings_mark(self, _get_config_mock): """ Settings must only be updated if EDNX_USE_SIGNAL is present @@ -224,7 +224,7 @@ def test_udpate_settings_mark(self, _get_config_mock): with self.assertRaises(AttributeError): settings.EDNX_TENANT_KEY # pylint: disable=pointless-statement - @patch('eox_tenant.signals._get_tenant_config') + @patch('eox_tenant.signals.get_tenant_config_by_domain') def test_udpate_settings(self, _get_config_mock): """ Settings must only be updated if EDNX_USE_SIGNAL is present @@ -239,7 +239,7 @@ def test_udpate_settings(self, _get_config_mock): settings.EDNX_TENANT_KEY # pylint: disable=pointless-statement - @patch('eox_tenant.signals._get_tenant_config') + @patch('eox_tenant.signals.get_tenant_config_by_domain') def test_udpate_settings_with_property(self, _get_config_mock): """ Settings must only be updated if EDNX_USE_SIGNAL is present @@ -255,7 +255,7 @@ def test_udpate_settings_with_property(self, _get_config_mock): self.assertEqual(settings.TEST_PROPERTY, "My value") - @patch('eox_tenant.signals._get_tenant_config') + @patch('eox_tenant.signals.get_tenant_config_by_domain') def test_udpate_settings_with_dict(self, _get_config_mock): """ Settings must only be updated if EDNX_USE_SIGNAL is present @@ -273,7 +273,7 @@ def test_udpate_settings_with_dict(self, _get_config_mock): self.assertEqual(settings.TEST_DICT.get("TEST_PROPERTY"), "My value") - @patch('eox_tenant.signals._get_tenant_config') + @patch('eox_tenant.signals.get_tenant_config_by_domain') def test_udpate_settings_with_existing_dict(self, _get_config_mock): """ Settings must only be updated if EDNX_USE_SIGNAL is present diff --git a/eox_tenant/test/test_wrappers.py b/eox_tenant/test/test_wrappers.py index f634b238..3731add5 100644 --- a/eox_tenant/test/test_wrappers.py +++ b/eox_tenant/test/test_wrappers.py @@ -6,7 +6,7 @@ from mock import patch, Mock from eox_tenant.edxapp_wrapper import ( - configuration_helpers, + site_configuration_module, get_common_util, ) @@ -16,7 +16,7 @@ class ConfigurationHelpersTest(TestCase): Making sure that the configuration_helpers backend works """ - @patch('eox_tenant.edxapp_wrapper.configuration_helpers.import_module') + @patch('eox_tenant.edxapp_wrapper.site_configuration_module.import_module') def test_imported_module_is_used(self, import_mock): """ Testing the backend is imported and used @@ -24,7 +24,7 @@ def test_imported_module_is_used(self, import_mock): backend = Mock() import_mock.side_effect = backend - configuration_helpers.get_configuration_helpers() + site_configuration_module.get_configuration_helpers() import_mock.assert_called() backend.assert_called() diff --git a/eox_tenant/test_utils.py b/eox_tenant/test_utils.py index 5cc5fb09..35de27b4 100644 --- a/eox_tenant/test_utils.py +++ b/eox_tenant/test_utils.py @@ -3,6 +3,16 @@ """ +class SiteConfigurationTest(object): + """ + Test class for SiteConfigurationTest. + """ + + def __init__(self, enabled, values): + self.enabled = enabled + self.values = values + + class test_theming_helpers(object): """ Test class for theming helpers @@ -12,3 +22,11 @@ def get_current_request(self): Test method """ return object + + +class TestSiteConfigurationModels(object): + """ + Test class for SiteConfigurationModels. + """ + + SiteConfiguration = object diff --git a/requirements.in b/requirements.in index be0fcb8d..bf5ec0c0 100644 --- a/requirements.in +++ b/requirements.in @@ -7,3 +7,4 @@ pylint==1.9.3 pycodestyle==2.4.0 mock==2.0.0 path.py==8.2.1 +django-crum==0.7.3 diff --git a/requirements.txt b/requirements.txt index c2b92fca..b24d97d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ astroid==1.6.5 # via pylint backports.functools-lru-cache==1.5 # via astroid, pylint configparser==3.5.0 # via pylint coverage==4.5.1 +django-crum==0.7.3 django==1.11.23 edx-opaque-keys==0.4.4 enum34==1.1.6 # via astroid