Commit 9bcb1166 by Matt Drayer

Merge pull request #11073 from edx/ziafazal/WL-245

Ziafazal/wl-245: multiple backend support for microsite
parents fdf540d2 4742e661
......@@ -312,9 +312,6 @@ VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', [])
MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED = ENV_TOKENS.get("MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED", 5)
MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = ENV_TOKENS.get("MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS", 15 * 60)
MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {})
MICROSITE_ROOT_DIR = path(ENV_TOKENS.get('MICROSITE_ROOT_DIR', ''))
#### PASSWORD POLICY SETTINGS #####
PASSWORD_MIN_LENGTH = ENV_TOKENS.get("PASSWORD_MIN_LENGTH")
PASSWORD_MAX_LENGTH = ENV_TOKENS.get("PASSWORD_MAX_LENGTH")
......@@ -365,6 +362,19 @@ XBLOCK_SETTINGS.setdefault("VideoModule", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.g
PROCTORING_BACKEND_PROVIDER = AUTH_TOKENS.get("PROCTORING_BACKEND_PROVIDER", PROCTORING_BACKEND_PROVIDER)
PROCTORING_SETTINGS = ENV_TOKENS.get("PROCTORING_SETTINGS", PROCTORING_SETTINGS)
################# MICROSITE ####################
# microsite specific configurations.
MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {})
MICROSITE_ROOT_DIR = path(ENV_TOKENS.get('MICROSITE_ROOT_DIR', ''))
# this setting specify which backend to be used when pulling microsite specific configuration
MICROSITE_BACKEND = ENV_TOKENS.get("MICROSITE_BACKEND", MICROSITE_BACKEND)
# this setting specify which backend to be used when loading microsite specific templates
MICROSITE_TEMPLATE_BACKEND = ENV_TOKENS.get("MICROSITE_TEMPLATE_BACKEND", MICROSITE_TEMPLATE_BACKEND)
# TTL for microsite database template cache
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = ENV_TOKENS.get(
"MICROSITE_DATABASE_TEMPLATE_CACHE_TTL", MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
)
############################ OAUTH2 Provider ###################################
# OpenID Connect issuer ID. Normally the URL of the authentication endpoint.
......
......@@ -827,6 +827,10 @@ INSTALLED_APPS = (
# other apps that are. Django 1.8 wants to have imported models supported
# by installed apps.
'lms.djangoapps.verify_student',
# Microsite configuration application
'microsite_configuration',
)
......@@ -1129,6 +1133,21 @@ DEPRECATED_BLOCK_TYPES = [
'graphical_slider_tool',
]
################################ Settings for Microsites ################################
### Select an implementation for the microsite backend
# for MICROSITE_BACKEND possible choices are
# 1. microsite_configuration.backends.filebased.FilebasedMicrositeBackend
# 2. microsite_configuration.backends.database.DatabaseMicrositeBackend
MICROSITE_BACKEND = 'microsite_configuration.backends.filebased.FilebasedMicrositeBackend'
# for MICROSITE_TEMPLATE_BACKEND possible choices are
# 1. microsite_configuration.backends.filebased.FilebasedMicrositeTemplateBackend
# 2. microsite_configuration.backends.database.DatabaseMicrositeTemplateBackend
MICROSITE_TEMPLATE_BACKEND = 'microsite_configuration.backends.filebased.FilebasedMicrositeTemplateBackend'
# TTL for microsite database template cache
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = 5 * 60
#### PROCTORING CONFIGURATION DEFAULTS
PROCTORING_BACKEND_PROVIDER = {
......
......@@ -214,6 +214,8 @@ FEATURES['ENABLE_SERVICE_STATUS'] = True
FEATURES['EMBARGO'] = True
# set up some testing for microsites
FEATURES['USE_MICROSITES'] = True
MICROSITE_ROOT_DIR = COMMON_ROOT / 'test' / 'test_microsites'
MICROSITE_CONFIGURATION = {
"test_microsite": {
"domain_prefix": "testmicrosite",
......@@ -231,15 +233,51 @@ MICROSITE_CONFIGURATION = {
"show_homepage_promo_video": False,
"course_index_overlay_text": "This is a Test Microsite Overlay Text.",
"course_index_overlay_logo_file": "test_microsite/images/header-logo.png",
"homepage_overlay_html": "<h1>This is a Test Microsite Overlay HTML</h1>"
"homepage_overlay_html": "<h1>This is a Test Microsite Overlay HTML</h1>",
"ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER": False,
"COURSE_CATALOG_VISIBILITY_PERMISSION": "see_in_catalog",
"COURSE_ABOUT_VISIBILITY_PERMISSION": "see_about_page",
"ENABLE_SHOPPING_CART": True,
"ENABLE_PAID_COURSE_REGISTRATION": True,
"SESSION_COOKIE_DOMAIN": "test_microsite.localhost",
"urls": {
'ABOUT': 'testmicrosite/about',
'PRIVACY': 'testmicrosite/privacy',
'TOS_AND_HONOR': 'testmicrosite/tos-and-honor',
},
},
"microsite_with_logistration": {
"domain_prefix": "logistration",
"university": "logistration",
"platform_name": "Test logistration",
"logo_image_url": "test_microsite/images/header-logo.png",
"email_from_address": "test_microsite@edx.org",
"payment_support_email": "test_microsite@edx.org",
"ENABLE_MKTG_SITE": False,
"ENABLE_COMBINED_LOGIN_REGISTRATION": True,
"SITE_NAME": "test_microsite.localhost",
"course_org_filter": "LogistrationX",
"course_about_show_social_links": False,
"css_overrides_file": "test_microsite/css/test_microsite.css",
"show_partners": False,
"show_homepage_promo_video": False,
"course_index_overlay_text": "Logistration.",
"course_index_overlay_logo_file": "test_microsite/images/header-logo.png",
"homepage_overlay_html": "<h1>This is a Logistration HTML</h1>",
"ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER": False,
"COURSE_CATALOG_VISIBILITY_PERMISSION": "see_in_catalog",
"COURSE_ABOUT_VISIBILITY_PERMISSION": "see_about_page",
"ENABLE_SHOPPING_CART": True,
"ENABLE_PAID_COURSE_REGISTRATION": True,
"SESSION_COOKIE_DOMAIN": "test_logistration.localhost",
},
"default": {
"university": "default_university",
"domain_prefix": "www",
}
}
MICROSITE_ROOT_DIR = COMMON_ROOT / 'test' / 'test_microsites'
FEATURES['USE_MICROSITES'] = True
MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver'
MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver'
# For consistency in user-experience, keep the value of this setting in sync with
# the one in lms/envs/test.py
......
......@@ -10,6 +10,7 @@ import pkg_resources
from django.conf import settings
from mako.lookup import TemplateLookup
from microsite_configuration import microsite
from . import LOOKUP
......@@ -46,6 +47,18 @@ class DynamicTemplateLookup(TemplateLookup):
self._collection.clear()
self._uri_cache.clear()
def get_template(self, uri):
"""
Overridden method which will hand-off the template lookup to the microsite subsystem
"""
microsite_template = microsite.get_template(uri)
return (
microsite_template
if microsite_template
else super(DynamicTemplateLookup, self).get_template(uri)
)
def clear_lookups(namespace):
"""
......
"""
This file implements a class which is a handy utility to make any
call to the settings completely microsite aware by replacing the:
from django.conf import settings
with:
from microsite_configuration import settings
"""
from django.conf import settings as base_settings
from microsite_configuration import microsite
from .templatetags.microsite import page_title_breadcrumbs
class MicrositeAwareSettings(object):
"""
This class is a proxy object of the settings object from django.
It will try to get a value from the microsite and default to the
django settings
"""
def __getattr__(self, name):
try:
if isinstance(microsite.get_value(name), dict):
return microsite.get_dict(name, getattr(base_settings, name))
return microsite.get_value(name, getattr(base_settings, name))
except KeyError:
return getattr(base_settings, name)
settings = MicrositeAwareSettings() # pylint: disable=invalid-name
"""
Django admin page for microsite models
"""
from django.contrib import admin
from django import forms
from .models import (
Microsite,
MicrositeHistory,
MicrositeOrganizationMapping,
MicrositeTemplate
)
from util.organizations_helpers import get_organizations
class MicrositeAdmin(admin.ModelAdmin):
""" Admin interface for the Microsite object. """
list_display = ('key', 'site')
search_fields = ('site__domain', 'values')
class Meta(object): # pylint: disable=missing-docstring
model = Microsite
class MicrositeHistoryAdmin(admin.ModelAdmin):
""" Admin interface for the MicrositeHistory object. """
list_display = ('key', 'site', 'created')
search_fields = ('site__domain', 'values')
ordering = ['-created']
class Meta(object): # pylint: disable=missing-docstring
model = MicrositeHistory
def has_add_permission(self, request):
"""Don't allow adds"""
return False
def has_delete_permission(self, request, obj=None):
"""Don't allow deletes"""
return False
class MicrositeOrganizationMappingForm(forms.ModelForm):
"""
Django admin form for MicrositeOrganizationMapping model
"""
def __init__(self, *args, **kwargs):
super(MicrositeOrganizationMappingForm, self).__init__(*args, **kwargs)
organizations = get_organizations()
org_choices = [(org["short_name"], org["name"]) for org in organizations]
org_choices.insert(0, ('', 'None'))
self.fields['organization'] = forms.TypedChoiceField(
choices=org_choices, required=False, empty_value=None
)
class Meta(object):
model = MicrositeOrganizationMapping
fields = '__all__'
class MicrositeOrganizationMappingAdmin(admin.ModelAdmin):
""" Admin interface for the MicrositeOrganizationMapping object. """
list_display = ('organization', 'microsite')
search_fields = ('organization', 'microsite')
form = MicrositeOrganizationMappingForm
class Meta(object): # pylint: disable=missing-docstring
model = MicrositeOrganizationMapping
class MicrositeTemplateAdmin(admin.ModelAdmin):
""" Admin interface for the MicrositeTemplate object. """
list_display = ('microsite', 'template_uri')
search_fields = ('microsite', 'template_uri')
class Meta(object): # pylint: disable=missing-docstring
model = MicrositeTemplate
admin.site.register(Microsite, MicrositeAdmin)
admin.site.register(MicrositeHistory, MicrositeHistoryAdmin)
admin.site.register(MicrositeOrganizationMapping, MicrositeOrganizationMappingAdmin)
admin.site.register(MicrositeTemplate, MicrositeTemplateAdmin)
"""
Supported backends for microsites
1. filebased
This backend supports retrieval of microsite configurations/templates from filesystem.
2. database
This backend supports retrieval of microsite configurations/templates from database.
"""
"""
Microsite backend that reads the configuration from the database
"""
from mako.template import Template
from util.cache import cache
from django.conf import settings
from django.dispatch import receiver
from django.db.models.signals import post_save
from util.memcache import fasthash
from util.url import strip_port_from_host
from microsite_configuration.backends.base import (
BaseMicrositeBackend,
BaseMicrositeTemplateBackend,
)
from microsite_configuration.models import (
Microsite,
MicrositeOrganizationMapping,
MicrositeTemplate
)
from microsite_configuration.microsite import get_value as microsite_get_value
class DatabaseMicrositeBackend(BaseMicrositeBackend):
"""
Microsite backend that reads the microsites definitions
from a table in the database according to the models.py file
This backend would allow us to save microsite configurations
into database and load them in local storage when HTTRequest
is originated from microsite.
E.g. we have setup a microsite with key `monster-university-academy` and
We would have a DB entry like this in table created by Microsite model.
key = monster-university-academy
subdomain = mua.edx.org
values = {
"platform_name": "Monster University Academy".
"course_org_filter: "MonsterX"
}
While using DatabaseMicrositeBackend any request coming from mua.edx.org
would get microsite configurations from `values` column.
"""
def has_configuration_set(self):
"""
Returns whether there is any Microsite configuration settings
"""
if Microsite.objects.all()[:1].exists():
return True
else:
return False
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
# look up based on the HTTP request domain name
# this will need to be a full domain name match,
# not a 'startswith' match
microsite = Microsite.get_microsite_for_domain(domain)
if not microsite:
# if no match, then try to find a 'default' key in Microsites
try:
microsite = Microsite.objects.get(key='default')
except Microsite.DoesNotExist:
pass
if microsite:
# if we have a match, then set up the microsite thread local
# data
self._set_microsite_config_from_obj(microsite.site.domain, domain, microsite)
def get_all_config(self):
"""
This returns all configuration for all microsites
"""
config = {}
candidates = Microsite.objects.all()
for microsite in candidates:
values = microsite.values
config[microsite.key] = values
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
"""
microsite = MicrositeOrganizationMapping.get_microsite_for_organization(org)
if not microsite:
return default
# cdodge: This approach will not leverage any caching, although I think only Studio calls
# this
config = microsite.values
return config.get(val_name, 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
"""
# This should be cacheable (via memcache to keep consistent across a cluster)
# I believe this is called on the dashboard and catalog pages, so it'd be good to optimize
return set(MicrositeOrganizationMapping.objects.all().values_list('organization', flat=True))
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
# we take the list of ORGs associated with this microsite from the database mapping
# tables. NOTE, for now, we assume one ORG per microsite
organizations = microsite_object.get_organizations()
# we must have at least one ORG defined
if not organizations:
raise Exception(
'Configuration error. Microsite {key} does not have any ORGs mapped to it!'.format(
key=microsite_object.key
)
)
# just take the first one for now, we'll have to change the upstream logic to allow
# for more than one ORG binding
config['course_org_filter'] = organizations[0]
self.current_request_configuration.data = config
class DatabaseMicrositeTemplateBackend(BaseMicrositeTemplateBackend):
"""
Specialized class to pull templates from the database.
This Backend would allow us to save templates in DB and pull
them from there when required for a specific microsite.
This backend can be enabled by `MICROSITE_TEMPLATE_BACKEND` setting.
E.g. we have setup a microsite for subdomain `mua.edx.org` and
We have a DB entry like this in table created by MicrositeTemplate model.
microsite = Key for microsite(mua.edx.org)
template_uri = about.html
template = <html><body>Template from DB</body></html>
While using DatabaseMicrositeTemplateBackend any request coming from mua.edx.org/about.html
would get about.html template from DB and response would be the value of `template` column.
"""
def get_template_path(self, relative_path, **kwargs):
return relative_path
def get_template(self, uri):
"""
Override of the base class for us to look into the
database tables for a template definition, if we can't find
one we'll return None which means "use default means" (aka filesystem)
"""
cache_key = "template_cache." + fasthash(microsite_get_value('site_domain') + '.' + uri)
template_text = cache.get(cache_key) # pylint: disable=maybe-no-member
if not template_text:
# cache is empty so pull template from DB and fill cache.
template_obj = MicrositeTemplate.get_template_for_microsite(
microsite_get_value('site_domain'),
uri
)
if not template_obj:
# We need to set something in the cache to improve performance
# of the templates stored in the filesystem as well
cache.set( # pylint: disable=maybe-no-member
cache_key, '##none', settings.MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
)
return None
template_text = template_obj.template
cache.set( # pylint: disable=maybe-no-member
cache_key, template_text, settings.MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
)
if template_text == '##none':
return None
return Template(
text=template_text
)
@staticmethod
@receiver(post_save, sender=MicrositeTemplate)
def clear_cache(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Clear the cached template when the model is saved
"""
cache_key = "template_cache." + fasthash(instance.microsite.site.domain + '.' + instance.template_uri)
cache.delete(cache_key) # pylint: disable=maybe-no-member
"""
Microsite backend that reads the configuration from a file
"""
from microsite_configuration.backends.base import (
BaseMicrositeBackend,
BaseMicrositeTemplateBackend,
)
class FilebasedMicrositeBackend(BaseMicrositeBackend):
"""
Microsite backend that reads the microsites definitions
from a dictionary called MICROSITE_CONFIGURATION in the settings file.
"""
def __init__(self, **kwargs):
super(FilebasedMicrositeBackend, self).__init__(**kwargs)
class FilebasedMicrositeTemplateBackend(BaseMicrositeTemplateBackend):
"""
Microsite backend that loads templates from filesystem.
"""
pass
......@@ -6,79 +6,61 @@ A microsite enables the following features:
2) Present a landing page with a listing of courses that are specific to the 'brand'
3) Ability to swap out some branding elements in the website
"""
import threading
import os.path
import inspect
from importlib import import_module
from django.conf import settings
from microsite_configuration.backends.base import BaseMicrositeBackend, BaseMicrositeTemplateBackend
CURRENT_REQUEST_CONFIGURATION = threading.local()
CURRENT_REQUEST_CONFIGURATION.data = {}
__all__ = [
'is_request_in_microsite', 'get_value', 'has_override_value',
'get_template_path', 'get_value_for_org', 'get_all_orgs',
'clear', 'set_by_domain', 'enable_microsites', 'get_all_config',
'is_feature_enabled', 'enable_microsites_pre_startup',
]
def has_configuration_set():
"""
Returns whether there is any Microsite configuration settings
"""
return getattr(settings, "MICROSITE_CONFIGURATION", False)
BACKEND = None
TEMPLATES_BACKEND = None
def get_configuration():
def is_feature_enabled():
"""
Returns the current request's microsite configuration
Returns whether the feature flag to enable microsite has been set
"""
if not hasattr(CURRENT_REQUEST_CONFIGURATION, 'data'):
return {}
return CURRENT_REQUEST_CONFIGURATION.data
return settings.FEATURES.get('USE_MICROSITES', False)
def is_request_in_microsite():
"""
This will return if current request is a request within a microsite
"""
return bool(get_configuration())
return BACKEND.is_request_in_microsite()
def get_value(val_name, default=None):
def get_value(val_name, default=None, **kwargs):
"""
Returns a value associated with the request's microsite, if present
"""
configuration = get_configuration()
return configuration.get(val_name, default)
return BACKEND.get_value(val_name, default, **kwargs)
def has_override_value(val_name):
def get_dict(dict_name, default=None, **kwargs):
"""
Returns True/False whether a Microsite has a definition for the
specified named value
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
"""
configuration = get_configuration()
return val_name in configuration
return BACKEND.get_dict(dict_name, default, **kwargs)
def get_template_path(relative_path):
def has_override_value(val_name):
"""
Returns a path (string) to a Mako template, which can either be in
a microsite directory (as an override) or will just return what is passed in which is
expected to be a string
Returns True/False whether a Microsite has a definition for the
specified named value
"""
if not is_request_in_microsite():
return relative_path
microsite_template_path = str(get_value('template_dir'))
if microsite_template_path:
search_path = os.path.join(microsite_template_path, relative_path)
if os.path.isfile(search_path):
path = '/{0}/templates/{1}'.format(
get_value('microsite_name'),
relative_path
)
return path
return relative_path
return BACKEND.has_override_value(val_name)
def get_value_for_org(org, val_name, default=None):
......@@ -86,14 +68,7 @@ def get_value_for_org(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 has_configuration_set():
return default
for value in settings.MICROSITE_CONFIGURATION.values():
org_filter = value.get('course_org_filter', None)
if org_filter == org:
return value.get(val_name, default)
return default
return BACKEND.get_value_for_org(org, val_name, default)
def get_all_orgs():
......@@ -101,52 +76,96 @@ def get_all_orgs():
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 has_configuration_set():
return org_filter_set
return BACKEND.get_all_orgs()
for value in settings.MICROSITE_CONFIGURATION.values():
org_filter = value.get('course_org_filter')
if org_filter:
org_filter_set.add(org_filter)
return org_filter_set
def get_all_config():
"""
This returns a dict have all microsite configs. Each key in the dict represent a
microsite config.
"""
return BACKEND.get_all_config()
def clear():
"""
Clears out any microsite configuration from the current request/thread
"""
CURRENT_REQUEST_CONFIGURATION.data = {}
BACKEND.clear()
def _set_current_microsite(microsite_config_key, subdomain, domain):
def set_by_domain(domain):
"""
Helper internal method to actually put a microsite on the threadlocal
For a given request domain, find a match in our microsite configuration
and make it available to the complete django request process
"""
config = settings.MICROSITE_CONFIGURATION[microsite_config_key].copy()
config['subdomain'] = subdomain
config['microsite_config_key'] = microsite_config_key
config['site_domain'] = domain
CURRENT_REQUEST_CONFIGURATION.data = config
BACKEND.set_config_by_domain(domain)
def set_by_domain(domain):
def enable_microsites_pre_startup(log):
"""
For a given request domain, find a match in our microsite configuration and then assign
it to the thread local so that it is available throughout the entire
Django request processing
Prepare the feature settings that must be enabled before django.setup() or
autostartup() during the startup script
"""
if not has_configuration_set() or not domain:
return
if is_feature_enabled():
BACKEND.enable_microsites_pre_startup(log)
for key, value in settings.MICROSITE_CONFIGURATION.items():
subdomain = value.get('domain_prefix')
if subdomain and domain.startswith(subdomain):
_set_current_microsite(key, subdomain, domain)
def enable_microsites(log):
"""
Enable the use of microsites during the startup script
"""
if is_feature_enabled():
BACKEND.enable_microsites(log)
def get_template(uri):
"""
Returns a template for the specified URI, None if none exists or if caller should
use default templates/search paths
"""
if not is_request_in_microsite():
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:
_set_current_microsite('default', subdomain, domain)
return TEMPLATES_BACKEND.get_template(uri)
def get_template_path(relative_path, **kwargs):
"""
Returns a path (string) to a template
"""
if not is_request_in_microsite():
return relative_path
return TEMPLATES_BACKEND.get_template_path(relative_path, **kwargs)
def get_backend(name, expected_base_class, **kwds):
"""
Load a microsites backend and return an instance of it.
If backend is None (default) settings.MICROSITE_BACKEND is used.
Any additional args(kwds) will be used in the constructor of the backend.
"""
if not name:
return None
try:
parts = name.split('.')
module_name = '.'.join(parts[:-1])
class_name = parts[-1]
except IndexError:
raise ValueError('Invalid microsites backend %s' % name)
try:
module = import_module(module_name)
cls = getattr(module, class_name)
if not inspect.isclass(cls) or not issubclass(cls, expected_base_class):
raise TypeError
except (AttributeError, ValueError):
raise ValueError('Cannot find microsites backend %s' % module_name)
return cls(**kwds)
BACKEND = get_backend(settings.MICROSITE_BACKEND, BaseMicrositeBackend)
TEMPLATES_BACKEND = get_backend(settings.MICROSITE_TEMPLATE_BACKEND, BaseMicrositeTemplateBackend)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import jsonfield.fields
import django.db.models.deletion
from django.conf import settings
import model_utils.fields
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('sites', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='HistoricalMicrositeOrganizationMapping',
fields=[
('id', models.IntegerField(verbose_name='ID', db_index=True, auto_created=True, blank=True)),
('organization', models.CharField(max_length=63, db_index=True)),
('history_id', models.AutoField(serialize=False, primary_key=True)),
('history_date', models.DateTimeField()),
('history_type', models.CharField(max_length=1, choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')])),
('history_user', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True)),
],
options={
'ordering': ('-history_date', '-history_id'),
'get_latest_by': 'history_date',
'verbose_name': 'historical microsite organization mapping',
},
),
migrations.CreateModel(
name='HistoricalMicrositeTemplate',
fields=[
('id', models.IntegerField(verbose_name='ID', db_index=True, auto_created=True, blank=True)),
('template_uri', models.CharField(max_length=255, db_index=True)),
('template', models.TextField()),
('history_id', models.AutoField(serialize=False, primary_key=True)),
('history_date', models.DateTimeField()),
('history_type', models.CharField(max_length=1, choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')])),
('history_user', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True)),
],
options={
'ordering': ('-history_date', '-history_id'),
'get_latest_by': 'history_date',
'verbose_name': 'historical microsite template',
},
),
migrations.CreateModel(
name='Microsite',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('key', models.CharField(unique=True, max_length=63, db_index=True)),
('values', jsonfield.fields.JSONField(blank=True)),
('site', models.OneToOneField(related_name='microsite', to='sites.Site')),
],
),
migrations.CreateModel(
name='MicrositeHistory',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
('key', models.CharField(unique=True, max_length=63, db_index=True)),
('values', jsonfield.fields.JSONField(blank=True)),
('site', models.OneToOneField(related_name='microsite_history', to='sites.Site')),
],
options={
'verbose_name_plural': 'Microsite histories',
},
),
migrations.CreateModel(
name='MicrositeOrganizationMapping',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('organization', models.CharField(unique=True, max_length=63, db_index=True)),
('microsite', models.ForeignKey(to='microsite_configuration.Microsite')),
],
),
migrations.CreateModel(
name='MicrositeTemplate',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('template_uri', models.CharField(max_length=255, db_index=True)),
('template', models.TextField()),
('microsite', models.ForeignKey(to='microsite_configuration.Microsite')),
],
),
migrations.AddField(
model_name='historicalmicrositetemplate',
name='microsite',
field=models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.DO_NOTHING, db_constraint=False, blank=True, to='microsite_configuration.Microsite', null=True),
),
migrations.AddField(
model_name='historicalmicrositeorganizationmapping',
name='microsite',
field=models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.DO_NOTHING, db_constraint=False, blank=True, to='microsite_configuration.Microsite', null=True),
),
migrations.AlterUniqueTogether(
name='micrositetemplate',
unique_together=set([('microsite', 'template_uri')]),
),
]
"""
Model to store a microsite in the database.
The object is stored as a json representation of the python dict
that would have been used in the settings.
"""
import collections
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import pre_save, pre_delete
from django.db.models.base import ObjectDoesNotExist
from django.contrib.sites.models import Site
from jsonfield.fields import JSONField
from model_utils.models import TimeStampedModel
from simple_history.models import HistoricalRecords
class Microsite(models.Model):
"""
This is where the information about the microsite gets stored to the db.
To achieve the maximum flexibility, most of the fields are stored inside
a json field.
Notes:
- The key field was required for the dict definition at the settings, and it
is used in some of the microsite_configuration methods.
- The site field is django site.
- The values field must be validated on save to prevent the platform from crashing
badly in the case the string is not able to be loaded as json.
"""
site = models.OneToOneField(Site, related_name='microsite')
key = models.CharField(max_length=63, db_index=True, unique=True)
values = JSONField(null=False, blank=True, load_kwargs={'object_pairs_hook': collections.OrderedDict})
def __unicode__(self):
return self.key
def get_organizations(self):
"""
Helper method to return a list of organizations associated with our particular Microsite
"""
return MicrositeOrganizationMapping.get_organizations_for_microsite_by_pk(self.id) # pylint: disable=no-member
@classmethod
def get_microsite_for_domain(cls, domain):
"""
Returns the microsite associated with this domain. Note that we always convert to lowercase, or
None if no match
"""
# remove any port number from the hostname
domain = domain.split(':')[0]
microsites = cls.objects.filter(site__domain__iexact=domain)
return microsites[0] if microsites else None
class MicrositeHistory(TimeStampedModel):
"""
This is an archive table for Microsites model, so that we can maintain a history of changes. Note that the
key field is no longer unique
"""
site = models.OneToOneField(Site, related_name='microsite_history')
key = models.CharField(max_length=63, db_index=True, unique=True)
values = JSONField(null=False, blank=True, load_kwargs={'object_pairs_hook': collections.OrderedDict})
def __unicode__(self):
return self.key
class Meta(object):
""" Meta class for this Django model """
verbose_name_plural = "Microsite histories"
def _make_archive_copy(instance):
"""
Helper method to make a copy of a Microsite into the history table
"""
archive_object = MicrositeHistory(
key=instance.key,
site=instance.site,
values=instance.values,
)
archive_object.save()
@receiver(pre_delete, sender=Microsite)
def on_microsite_deleted(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Archive the exam attempt when the item is about to be deleted
Make a clone and populate in the History table
"""
_make_archive_copy(instance)
@receiver(pre_save, sender=Microsite)
def on_microsite_updated(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Archive the microsite on an update operation
"""
if instance.id:
# on an update case, get the original and archive it
original = Microsite.objects.get(id=instance.id)
_make_archive_copy(original)
class MicrositeOrganizationMapping(models.Model):
"""
Mapping of Organization to which Microsite it belongs
"""
organization = models.CharField(max_length=63, db_index=True, unique=True)
microsite = models.ForeignKey(Microsite, db_index=True)
# for archiving
history = HistoricalRecords()
def __unicode__(self):
"""String conversion"""
return u'{microsite_key}: {organization}'.format(
microsite_key=self.microsite.key,
organization=self.organization
)
@classmethod
def get_organizations_for_microsite_by_pk(cls, microsite_pk):
"""
Returns a list of organizations associated with the microsite key, returned as a set
"""
return cls.objects.filter(microsite_id=microsite_pk).values_list('organization', flat=True)
@classmethod
def get_microsite_for_organization(cls, org):
"""
Returns the microsite object for a given organization based on the table mapping, None if
no mapping exists
"""
try:
item = cls.objects.select_related('microsite').get(organization=org)
return item.microsite
except ObjectDoesNotExist:
return None
class MicrositeTemplate(models.Model):
"""
A HTML template that a microsite can use
"""
microsite = models.ForeignKey(Microsite, db_index=True)
template_uri = models.CharField(max_length=255, db_index=True)
template = models.TextField()
# for archiving
history = HistoricalRecords()
def __unicode__(self):
"""String conversion"""
return u'{microsite_key}: {template_uri}'.format(
microsite_key=self.microsite.key,
template_uri=self.template_uri
)
class Meta(object):
""" Meta class for this Django model """
unique_together = (('microsite', 'template_uri'),)
@classmethod
def get_template_for_microsite(cls, domain, template_uri):
"""
Returns the template object for the microsite, None if not found
"""
try:
return cls.objects.get(microsite__site__domain=domain, template_uri=template_uri)
except ObjectDoesNotExist:
return None
"""
Test Microsite base backends.
"""
from django.test import TestCase
from microsite_configuration.backends.base import (
AbstractBaseMicrositeBackend,
)
class NullBackend(AbstractBaseMicrositeBackend):
"""
A class that does nothing but inherit from the base class.
We created this class to test methods of AbstractBaseMicrositeBackend class.
Since abstract class cannot be instantiated we created this wrapper class.
"""
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
"""
return super(NullBackend, self).set_config_by_domain(domain)
def get_template_path(self, relative_path, **kwargs):
"""
Returns a path (string) to a Mako template, which can either be in
an override or will just return what is passed in which is expected to be a string
"""
return super(NullBackend, self).get_template_path(relative_path, **kwargs)
def get_value(self, val_name, default=None, **kwargs):
"""
Returns a value associated with the request's microsite, if present
"""
return super(NullBackend, self).get_value(val_name, default, **kwargs)
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
"""
return super(NullBackend, self).get_dict(dict_name, default, **kwargs)
def is_request_in_microsite(self):
"""
This will return True/False if the current request is a request within a microsite
"""
return super(NullBackend, self).is_request_in_microsite()
def has_override_value(self, val_name):
"""
Returns True/False whether a Microsite has a definition for the
specified named value
"""
return super(NullBackend, self).has_override_value(val_name)
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
"""
return super(NullBackend, self).get_all_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
"""
return super(NullBackend, self).get_value_for_org(org, val_name, 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
"""
return super(NullBackend, self).get_all_orgs()
def clear(self):
"""
Clears out any microsite configuration from the current request/thread
"""
return super(NullBackend, self).clear()
class AbstractBaseMicrositeBackendTests(TestCase):
"""
Go through and test the base abstract class
"""
def test_cant_create_instance(self):
"""
We shouldn't be able to create an instance of the base abstract class
"""
with self.assertRaises(TypeError):
AbstractBaseMicrositeBackend() # pylint: disable=abstract-class-instantiated
def test_not_yet_implemented(self):
"""
Make sure all base methods raise a NotImplementedError exception
"""
backend = NullBackend()
with self.assertRaises(NotImplementedError):
backend.set_config_by_domain(None)
with self.assertRaises(NotImplementedError):
backend.get_value(None, None)
with self.assertRaises(NotImplementedError):
backend.get_dict(None, None)
with self.assertRaises(NotImplementedError):
backend.is_request_in_microsite()
with self.assertRaises(NotImplementedError):
backend.has_override_value(None)
with self.assertRaises(NotImplementedError):
backend.get_all_config()
with self.assertRaises(NotImplementedError):
backend.clear()
with self.assertRaises(NotImplementedError):
backend.get_value_for_org(None, None, None)
with self.assertRaises(NotImplementedError):
backend.get_all_orgs()
"""
Test Microsite database backends.
"""
import logging
from mock import patch
from django.conf import settings
from microsite_configuration.backends.base import (
BaseMicrositeBackend,
BaseMicrositeTemplateBackend,
)
from microsite_configuration import microsite
from microsite_configuration.models import (
Microsite,
MicrositeHistory,
MicrositeTemplate,
)
from microsite_configuration.tests.tests import (
DatabaseMicrositeTestCase,
)
from microsite_configuration.tests.factories import (
SiteFactory,
MicrositeFactory,
MicrositeTemplateFactory,
)
log = logging.getLogger(__name__)
@patch(
'microsite_configuration.microsite.BACKEND',
microsite.get_backend(
'microsite_configuration.backends.database.DatabaseMicrositeBackend', BaseMicrositeBackend
)
)
class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase):
"""
Go through and test the DatabaseMicrositeBackend class
"""
def setUp(self):
super(DatabaseMicrositeBackendTests, self).setUp()
def tearDown(self):
super(DatabaseMicrositeBackendTests, self).tearDown()
microsite.clear()
def test_get_value(self):
"""
Tests microsite.get_value works as expected.
"""
microsite.set_by_domain(self.microsite.site.domain)
self.assertEqual(microsite.get_value('email_from_address'), self.microsite.values['email_from_address'])
def test_is_request_in_microsite(self):
"""
Tests microsite.is_request_in_microsite works as expected.
"""
microsite.set_by_domain(self.microsite.site.domain)
self.assertTrue(microsite.is_request_in_microsite())
def test_get_dict(self):
"""
Tests microsite.get_dict works as expected.
"""
microsite.set_by_domain(self.microsite.site.domain)
self.assertEqual(microsite.get_dict('nested_dict'), self.microsite.values['nested_dict'])
def test_has_override_value(self):
"""
Tests microsite.has_override_value works as expected.
"""
microsite.set_by_domain(self.microsite.site.domain)
self.assertTrue(microsite.has_override_value('platform_name'))
def test_get_value_for_org(self):
"""
Tests microsite.get_value_for_org works as expected.
"""
microsite.set_by_domain(self.microsite.site.domain)
self.assertEqual(
microsite.get_value_for_org(self.microsite.get_organizations()[0], 'platform_name'),
self.microsite.values['platform_name']
)
def test_get_all_orgs(self):
"""
Tests microsite.get_all_orgs works as expected.
"""
microsite.set_by_domain(self.microsite.site.domain)
self.assertEqual(
microsite.get_all_orgs(),
set(self.microsite.get_organizations())
)
def test_clear(self):
"""
Tests microsite.clear works as expected.
"""
microsite.set_by_domain(self.microsite.site.domain)
self.assertEqual(
microsite.get_value('platform_name'),
self.microsite.values['platform_name']
)
microsite.clear()
self.assertIsNone(microsite.get_value('platform_name'))
def test_enable_microsites_pre_startup(self):
"""
Tests microsite.test_enable_microsites_pre_startup works as expected.
"""
# remove microsite root directory paths first
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'] = [
path for path in settings.DEFAULT_TEMPLATE_ENGINE['DIRS']
if path != settings.MICROSITE_ROOT_DIR
]
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': False}):
microsite.enable_microsites_pre_startup(log)
self.assertNotIn(settings.MICROSITE_ROOT_DIR, settings.DEFAULT_TEMPLATE_ENGINE['DIRS'])
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': True}):
microsite.enable_microsites_pre_startup(log)
self.assertIn(settings.MICROSITE_ROOT_DIR, settings.DEFAULT_TEMPLATE_ENGINE['DIRS'])
@patch('edxmako.paths.add_lookup')
def test_enable_microsites(self, add_lookup):
"""
Tests microsite.enable_microsites works as expected.
"""
# remove microsite root directory paths first
settings.STATICFILES_DIRS = [
path for path in settings.STATICFILES_DIRS
if path != settings.MICROSITE_ROOT_DIR
]
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': False}):
microsite.enable_microsites(log)
self.assertNotIn(settings.MICROSITE_ROOT_DIR, settings.STATICFILES_DIRS)
add_lookup.assert_not_called()
with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': True}):
microsite.enable_microsites(log)
self.assertIn(settings.MICROSITE_ROOT_DIR, settings.STATICFILES_DIRS)
add_lookup.assert_called_once_with('main', settings.MICROSITE_ROOT_DIR)
def test_get_all_configs(self):
"""
Tests microsite.get_all_config works as expected.
"""
microsite.set_by_domain(self.microsite.site.domain)
configs = microsite.get_all_config()
self.assertEqual(len(configs.keys()), 1)
self.assertEqual(configs[self.microsite.key], self.microsite.values)
def test_set_config_by_domain(self):
"""
Tests microsite.set_config_by_domain works as expected.
"""
microsite.clear()
# if microsite config does not exist
microsite.set_by_domain('unknown')
self.assertIsNone(microsite.get_value('platform_name'))
# if no microsite exists
Microsite.objects.all().delete()
microsite.clear()
microsite.set_by_domain('unknown')
self.assertIsNone(microsite.get_value('platform_name'))
# if microsite site has no organization it should raise exception
new_microsite = MicrositeFactory.create(key="test_microsite2")
new_microsite.site = SiteFactory.create(domain='test.microsite2.com')
# This would update microsite so we test MicrositeHistory has old microsite
new_microsite.save()
self.assertEqual(MicrositeHistory.objects.all().count(), 2)
with self.assertRaises(Exception):
microsite.set_by_domain('test.microsite2.com')
@patch(
'microsite_configuration.microsite.TEMPLATES_BACKEND',
microsite.get_backend(
'microsite_configuration.backends.database.DatabaseMicrositeTemplateBackend', BaseMicrositeTemplateBackend
)
)
class DatabaseMicrositeTemplateBackendTests(DatabaseMicrositeTestCase):
"""
Go through and test the DatabaseMicrositeTemplateBackend class
"""
def setUp(self):
super(DatabaseMicrositeTemplateBackendTests, self).setUp()
MicrositeTemplateFactory.create(
microsite=self.microsite,
template_uri='about.html',
template="""
<html>
<body>
About this microsite.
</body>
</html>
""",
)
def tearDown(self):
super(DatabaseMicrositeTemplateBackendTests, self).tearDown()
microsite.clear()
def test_microsite_get_template_when_no_template_exists(self):
"""
Test microsite.get_template return None if there is not template in DB.
"""
MicrositeTemplate.objects.all().delete()
microsite.set_by_domain(self.microsite.site.domain)
template = microsite.get_template('about.html')
self.assertIsNone(template)
def test_microsite_get_template(self):
"""
Test microsite.get_template return appropriate template.
"""
microsite.set_by_domain(self.microsite.site.domain)
template = microsite.get_template('about.html')
self.assertIn('About this microsite', template.render())
"""
Test Microsite filebased backends.
"""
from mock import patch
from django.test import TestCase
from microsite_configuration.backends.base import (
BaseMicrositeBackend,
)
from microsite_configuration import microsite
@patch(
'microsite_configuration.microsite.BACKEND',
microsite.get_backend(
'microsite_configuration.backends.filebased.FilebasedMicrositeBackend', BaseMicrositeBackend
)
)
class FilebasedMicrositeBackendTests(TestCase):
"""
Go through and test the FilebasedMicrositeBackend class
"""
def setUp(self):
super(FilebasedMicrositeBackendTests, self).setUp()
self.microsite_subdomain = 'testmicrosite'
def tearDown(self):
super(FilebasedMicrositeBackendTests, self).tearDown()
microsite.clear()
def test_get_value(self):
"""
Tests microsite.get_value works as expected.
"""
microsite.set_by_domain(self.microsite_subdomain)
self.assertEqual(microsite.get_value('platform_name'), 'Test Microsite')
def test_is_request_in_microsite(self):
"""
Tests microsite.is_request_in_microsite works as expected.
"""
microsite.set_by_domain(self.microsite_subdomain)
self.assertTrue(microsite.is_request_in_microsite())
def test_has_override_value(self):
"""
Tests microsite.has_override_value works as expected.
"""
microsite.set_by_domain(self.microsite_subdomain)
self.assertTrue(microsite.has_override_value('platform_name'))
def test_get_value_for_org(self):
"""
Tests microsite.get_value_for_org works as expected.
"""
microsite.set_by_domain(self.microsite_subdomain)
self.assertEqual(
microsite.get_value_for_org('TestMicrositeX', 'platform_name'),
'Test Microsite'
)
# if no config is set
microsite.clear()
with patch('django.conf.settings.MICROSITE_CONFIGURATION', False):
self.assertEqual(
microsite.get_value_for_org('TestMicrositeX', 'platform_name', 'Default Value'),
'Default Value'
)
def test_get_all_orgs(self):
"""
Tests microsite.get_all_orgs works as expected.
"""
microsite.set_by_domain(self.microsite_subdomain)
self.assertEqual(
microsite.get_all_orgs(),
set(['TestMicrositeX', 'LogistrationX'])
)
# if no config is set
microsite.clear()
with patch('django.conf.settings.MICROSITE_CONFIGURATION', False):
self.assertEqual(
microsite.get_all_orgs(),
set()
)
def test_clear(self):
"""
Tests microsite.clear works as expected.
"""
microsite.set_by_domain(self.microsite_subdomain)
self.assertEqual(
microsite.get_value('platform_name'),
'Test Microsite'
)
microsite.clear()
self.assertIsNone(microsite.get_value('platform_name'))
def test_get_all_configs(self):
"""
Tests microsite.get_all_config works as expected.
"""
microsite.set_by_domain(self.microsite_subdomain)
configs = microsite.get_all_config()
self.assertEqual(len(configs.keys()), 3)
def test_set_config_by_domain(self):
"""
Tests microsite.set_config_by_domain works as expected.
"""
microsite.clear()
# if microsite config does not exist default config should be used
microsite.set_by_domain('unknown')
self.assertEqual(microsite.get_value('university'), 'default_university')
"""
Factories module to hold microsite factories
"""
import factory
from factory.django import DjangoModelFactory
from django.contrib.sites.models import Site
from microsite_configuration.models import (
Microsite,
MicrositeOrganizationMapping,
MicrositeTemplate,
)
class SiteFactory(DjangoModelFactory):
"""
Factory for django.contrib.sites.models.Site
"""
class Meta(object):
model = Site
name = "test microsite"
domain = "testmicrosite.testserver"
class MicrositeFactory(DjangoModelFactory):
"""
Factory for Microsite
"""
class Meta(object):
model = Microsite
key = "test_microsite"
site = factory.SubFactory(SiteFactory)
values = {
"domain_prefix": "testmicrosite",
"university": "test_microsite",
"platform_name": "Test Microsite DB",
"logo_image_url": "test_microsite/images/header-logo.png",
"email_from_address": "test_microsite_db@edx.org",
"payment_support_email": "test_microsit_dbe@edx.org",
"ENABLE_MKTG_SITE": False,
"SITE_NAME": "test_microsite.localhost",
"course_org_filter": "TestMicrositeX",
"course_about_show_social_links": False,
"css_overrides_file": "test_microsite/css/test_microsite.css",
"show_partners": False,
"show_homepage_promo_video": False,
"course_index_overlay_text": "This is a Test Microsite Overlay Text.",
"course_index_overlay_logo_file": "test_microsite/images/header-logo.png",
"homepage_overlay_html": "<h1>This is a Test Microsite Overlay HTML</h1>",
"ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER": False,
"COURSE_CATALOG_VISIBILITY_PERMISSION": "see_in_catalog",
"COURSE_ABOUT_VISIBILITY_PERMISSION": "see_about_page",
"ENABLE_SHOPPING_CART": True,
"ENABLE_PAID_COURSE_REGISTRATION": True,
"SESSION_COOKIE_DOMAIN": "test_microsite.localhost",
"nested_dict": {
"key 1": "value 1",
"key 2": "value 2",
}
}
class MicrositeOrganizationMappingFactory(DjangoModelFactory):
"""
Factory for MicrositeOrganizationMapping
"""
class Meta(object):
model = MicrositeOrganizationMapping
class MicrositeTemplateFactory(DjangoModelFactory):
"""
Factory for MicrositeTemplate
"""
class Meta(object):
model = MicrositeTemplate
......@@ -2,25 +2,45 @@
Some additional unit tests for Microsite logic. The LMS covers some of the Microsite testing, this adds
some additional coverage
"""
import django.test
import ddt
from mock import patch
from microsite_configuration.microsite import get_value_for_org
from microsite_configuration.microsite import (
get_value_for_org,
get_backend,
)
from microsite_configuration.backends.base import BaseMicrositeBackend
from microsite_configuration.tests.tests import (
DatabaseMicrositeTestCase,
MICROSITE_BACKENDS,
)
class TestMicrosites(django.test.TestCase):
@ddt.ddt
class TestMicrosites(DatabaseMicrositeTestCase):
"""
Run through some Microsite logic
"""
def test_get_value_for_org(self):
def setUp(self):
super(TestMicrosites, self).setUp()
@ddt.data(*MICROSITE_BACKENDS)
def test_get_value_for_org_when_microsite_has_no_org(self, site_backend):
"""
Make sure we can do lookups on Microsite configuration based on ORG fields
Make sure default value is returned if there's no Microsite ORG match
"""
# first make sure default value is returned if there's no Microsite ORG match
with patch('microsite_configuration.microsite.BACKEND',
get_backend(site_backend, BaseMicrositeBackend)):
value = get_value_for_org("BogusX", "university", "default_value")
self.assertEquals(value, "default_value")
# now test when we call in a value Microsite ORG, note this is defined in test.py configuration
@ddt.data(*MICROSITE_BACKENDS)
def test_get_value_for_org(self, site_backend):
"""
Make sure get_value_for_org return value of org if it present.
"""
with patch('microsite_configuration.microsite.BACKEND',
get_backend(site_backend, BaseMicrositeBackend)):
value = get_value_for_org("TestMicrositeX", "university", "default_value")
self.assertEquals(value, "test_microsite")
......@@ -4,38 +4,73 @@ Tests microsite_configuration templatetags and helper functions.
"""
from django.test import TestCase
from django.conf import settings
from microsite_configuration.templatetags import microsite
from microsite_configuration.templatetags import microsite as microsite_tags
from microsite_configuration import microsite
from microsite_configuration.backends.base import BaseMicrositeBackend
from microsite_configuration.backends.database import DatabaseMicrositeBackend
class MicroSiteTests(TestCase):
class MicrositeTests(TestCase):
"""
Make sure some of the helper functions work
"""
def test_breadcrumbs(self):
crumbs = ['my', 'less specific', 'Page']
expected = u'my | less specific | Page | edX'
title = microsite.page_title_breadcrumbs(*crumbs)
title = microsite_tags.page_title_breadcrumbs(*crumbs)
self.assertEqual(expected, title)
def test_unicode_title(self):
crumbs = [u'øo', u'π tastes gréât', u'驴']
expected = u'øo | π tastes gréât | 驴 | edX'
title = microsite.page_title_breadcrumbs(*crumbs)
title = microsite_tags.page_title_breadcrumbs(*crumbs)
self.assertEqual(expected, title)
def test_platform_name(self):
pname = microsite.platform_name()
pname = microsite_tags.platform_name()
self.assertEqual(pname, settings.PLATFORM_NAME)
def test_breadcrumb_tag(self):
crumbs = ['my', 'less specific', 'Page']
expected = u'my | less specific | Page | edX'
title = microsite.page_title_breadcrumbs_tag(None, *crumbs)
title = microsite_tags.page_title_breadcrumbs_tag(None, *crumbs)
self.assertEqual(expected, title)
def test_microsite_template_path(self):
"""
When an unexistent path is passed to the filter, it should return the same path
"""
path = microsite.microsite_template_path('footer.html')
path = microsite_tags.microsite_template_path('footer.html')
self.assertEqual("footer.html", path)
def test_get_backend_raise_error_for_invalid_class(self):
"""
Test get_backend returns None for invalid paths
and raises TypeError when invalid class or class name is a method.
"""
# invalid backend path
self.assertEqual(microsite.get_backend(None, BaseMicrositeBackend), None)
# invalid class or class name is a method
with self.assertRaises(TypeError):
microsite.get_backend('microsite_configuration.microsite.get_backend', BaseMicrositeBackend)
def test_get_backend_raise_error_when_module_has_no_class(self):
"""
Test get_backend raises ValueError when module does not have a class.
"""
# module does not have a class
with self.assertRaises(ValueError):
microsite.get_backend('microsite_configuration.microsite.invalid_method', BaseMicrositeBackend)
def test_get_backend_for_valid_class(self):
"""
Test get_backend loads class if class exists.
"""
# load a valid class
self.assertIsInstance(
microsite.get_backend(
'microsite_configuration.backends.database.DatabaseMicrositeBackend', BaseMicrositeBackend
),
DatabaseMicrositeBackend
)
......@@ -2,28 +2,39 @@
"""
Test Microsite middleware.
"""
import ddt
import unittest
from mock import patch
from django.test import TestCase
from django.conf import settings
from django.test.client import Client
from django.test.utils import override_settings
import unittest
from student.tests.factories import UserFactory
from microsite_configuration.microsite import (
get_backend,
)
from microsite_configuration.backends.base import BaseMicrositeBackend
from microsite_configuration.tests.tests import (
DatabaseMicrositeTestCase,
side_effect_for_get_value,
MICROSITE_BACKENDS,
)
# NOTE: We set SESSION_SAVE_EVERY_REQUEST to True in order to make sure
# Sessions are always started on every request
# pylint: disable=no-member, protected-access
@ddt.ddt
@override_settings(SESSION_SAVE_EVERY_REQUEST=True)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class MicroSiteSessionCookieTests(TestCase):
class MicrositeSessionCookieTests(DatabaseMicrositeTestCase):
"""
Tests regarding the session cookie management in the middlware for MicroSites
Tests regarding the session cookie management in the middlware for Microsites
"""
def setUp(self):
super(MicroSiteSessionCookieTests, self).setUp()
super(MicrositeSessionCookieTests, self).setUp()
# Create a test client, and log it in so that it will save some session
# data.
self.user = UserFactory.create()
......@@ -32,29 +43,39 @@ class MicroSiteSessionCookieTests(TestCase):
self.client = Client()
self.client.login(username=self.user.username, password="password")
def test_session_cookie_domain_no_microsite(self):
@ddt.data(*MICROSITE_BACKENDS)
def test_session_cookie_domain_no_microsite(self, site_backend):
"""
Tests that non-microsite behaves according to default behavior
"""
with patch('microsite_configuration.microsite.BACKEND',
get_backend(site_backend, BaseMicrositeBackend)):
response = self.client.get('/')
self.assertNotIn('test_microsite.localhost', str(response.cookies['sessionid'])) # pylint: disable=no-member
self.assertNotIn('Domain', str(response.cookies['sessionid'])) # pylint: disable=no-member
self.assertNotIn('test_microsite.localhost', str(response.cookies['sessionid']))
self.assertNotIn('Domain', str(response.cookies['sessionid']))
def test_session_cookie_domain(self):
@ddt.data(*MICROSITE_BACKENDS)
def test_session_cookie_domain(self, site_backend):
"""
Makes sure that the cookie being set in a Microsite
is the one specially overridden in configuration,
in this case in test.py
is the one specially overridden in configuration
"""
with patch('microsite_configuration.microsite.BACKEND',
get_backend(site_backend, BaseMicrositeBackend)):
response = self.client.get('/', HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME)
self.assertIn('test_microsite.localhost', str(response.cookies['sessionid'])) # pylint: disable=no-member
self.assertIn('test_microsite.localhost', str(response.cookies['sessionid']))
@patch.dict("django.conf.settings.MICROSITE_CONFIGURATION", {'test_microsite': {'SESSION_COOKIE_DOMAIN': None}})
def test_microsite_none_cookie_domain(self):
@ddt.data(*MICROSITE_BACKENDS)
def test_microsite_none_cookie_domain(self, site_backend):
"""
Tests to make sure that a Microsite that specifies None for 'SESSION_COOKIE_DOMAIN' does not
set a domain on the session cookie
"""
with patch('microsite_configuration.microsite.get_value') as mock_get_value:
mock_get_value.side_effect = side_effect_for_get_value('SESSION_COOKIE_DOMAIN', None)
with patch('microsite_configuration.microsite.BACKEND',
get_backend(site_backend, BaseMicrositeBackend)):
response = self.client.get('/', HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME)
self.assertNotIn('test_microsite.localhost', str(response.cookies['sessionid'])) # pylint: disable=no-member
self.assertNotIn('Domain', str(response.cookies['sessionid'])) # pylint: disable=no-member
self.assertNotIn('test_microsite.localhost', str(response.cookies['sessionid']))
self.assertNotIn('Domain', str(response.cookies['sessionid']))
"""
Holds base classes for microsite tests
"""
from mock import DEFAULT
from django.test import TestCase
from microsite_configuration.tests.factories import (
MicrositeFactory,
MicrositeOrganizationMappingFactory,
)
MICROSITE_BACKENDS = (
'microsite_configuration.backends.filebased.FilebasedMicrositeBackend',
'microsite_configuration.backends.database.DatabaseMicrositeBackend',
)
class DatabaseMicrositeTestCase(TestCase):
"""
Base class for microsite related tests.
"""
def setUp(self):
super(DatabaseMicrositeTestCase, self).setUp()
self.microsite = MicrositeFactory.create()
MicrositeOrganizationMappingFactory.create(microsite=self.microsite, organization='TestMicrositeX')
def side_effect_for_get_value(value, return_value):
"""
returns a side_effect with given return value for a given value
"""
def side_effect(*args, **kwargs): # pylint: disable=unused-argument
"""
A side effect for tests which returns a value based
on a given argument otherwise return actual function.
"""
if args[0] == value:
return return_value
else:
return DEFAULT
return side_effect
......@@ -20,3 +20,10 @@ def reload_django_url_config():
reloaded = import_module(urlconf)
reloaded_urls = reloaded.urlpatterns
set_urlconf(tuple(reloaded_urls))
def strip_port_from_host(host):
"""
Strips port number from host
"""
return host.split(':')[0]
......@@ -537,9 +537,6 @@ ORA2_FILE_PREFIX = ENV_TOKENS.get("ORA2_FILE_PREFIX", ORA2_FILE_PREFIX)
MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED = ENV_TOKENS.get("MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED", 5)
MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = ENV_TOKENS.get("MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS", 15 * 60)
MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {})
MICROSITE_ROOT_DIR = path(ENV_TOKENS.get('MICROSITE_ROOT_DIR', ''))
#### PASSWORD POLICY SETTINGS #####
PASSWORD_MIN_LENGTH = ENV_TOKENS.get("PASSWORD_MIN_LENGTH")
PASSWORD_MAX_LENGTH = ENV_TOKENS.get("PASSWORD_MAX_LENGTH")
......@@ -735,5 +732,17 @@ JWT_EXPIRATION = ENV_TOKENS.get('JWT_EXPIRATION', JWT_EXPIRATION)
PROCTORING_BACKEND_PROVIDER = AUTH_TOKENS.get("PROCTORING_BACKEND_PROVIDER", PROCTORING_BACKEND_PROVIDER)
PROCTORING_SETTINGS = ENV_TOKENS.get("PROCTORING_SETTINGS", PROCTORING_SETTINGS)
################# MICROSITE ####################
MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {})
MICROSITE_ROOT_DIR = path(ENV_TOKENS.get('MICROSITE_ROOT_DIR', ''))
# this setting specify which backend to be used when pulling microsite specific configuration
MICROSITE_BACKEND = ENV_TOKENS.get("MICROSITE_BACKEND", MICROSITE_BACKEND)
# this setting specify which backend to be used when loading microsite specific templates
MICROSITE_TEMPLATE_BACKEND = ENV_TOKENS.get("MICROSITE_TEMPLATE_BACKEND", MICROSITE_TEMPLATE_BACKEND)
# TTL for microsite database template cache
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = ENV_TOKENS.get(
"MICROSITE_DATABASE_TEMPLATE_CACHE_TTL", MICROSITE_DATABASE_TEMPLATE_CACHE_TTL
)
# Course Content Bookmarks Settings
MAX_BOOKMARKS_PER_COURSE = ENV_TOKENS.get('MAX_BOOKMARKS_PER_COURSE', MAX_BOOKMARKS_PER_COURSE)
......@@ -2641,6 +2641,22 @@ JWT_ISSUER = None
NOTIFICATION_EMAIL_CSS = "templates/credit_notifications/credit_notification.css"
NOTIFICATION_EMAIL_EDX_LOGO = "templates/credit_notifications/edx-logo-header.png"
################################ Settings for Microsites ################################
### Select an implementation for the microsite backend
# for MICROSITE_BACKEND possible choices are
# 1. microsite_configuration.backends.filebased.FilebasedMicrositeBackend
# 2. microsite_configuration.backends.database.DatabaseMicrositeBackend
MICROSITE_BACKEND = 'microsite_configuration.backends.filebased.FilebasedMicrositeBackend'
# for MICROSITE_TEMPLATE_BACKEND possible choices are
# 1. microsite_configuration.backends.filebased.FilebasedMicrositeTemplateBackend
# 2. microsite_configuration.backends.database.DatabaseMicrositeTemplateBackend
MICROSITE_TEMPLATE_BACKEND = 'microsite_configuration.backends.filebased.FilebasedMicrositeTemplateBackend'
# TTL for microsite database template cache
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = 5 * 60
#### PROCTORING CONFIGURATION DEFAULTS
PROCTORING_BACKEND_PROVIDER = {
......
......@@ -423,6 +423,8 @@ PLATFORM_NAME = "edX"
SITE_NAME = "edx.org"
# set up some testing for microsites
FEATURES['USE_MICROSITES'] = True
MICROSITE_ROOT_DIR = COMMON_ROOT / 'test' / 'test_microsites'
MICROSITE_CONFIGURATION = {
"test_microsite": {
"domain_prefix": "testmicrosite",
......@@ -483,15 +485,14 @@ MICROSITE_CONFIGURATION = {
"domain_prefix": "www",
}
}
MICROSITE_ROOT_DIR = COMMON_ROOT / 'test' / 'test_microsites'
MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver'
MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver'
FEATURES['USE_MICROSITES'] = True
# add extra template directory for test-only templates
MAKO_TEMPLATES['main'].extend([
COMMON_ROOT / 'test' / 'templates'
COMMON_ROOT / 'test' / 'templates',
COMMON_ROOT / 'test' / 'test_microsites'
])
......
......@@ -18,6 +18,8 @@ from monkey_patch import third_party_auth
import xmodule.x_module
import lms_xblock.runtime
from microsite_configuration import microsite
log = logging.getLogger(__name__)
......@@ -31,8 +33,10 @@ def run():
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH', False):
enable_third_party_auth()
if settings.FEATURES.get('USE_MICROSITES', False):
enable_microsites_pre_startup()
# We currently use 2 template rendering engines, mako and django_templates,
# and one of them (django templates), requires the directories be added
# before the django.setup().
microsite.enable_microsites_pre_startup(log)
django.setup()
......@@ -40,12 +44,12 @@ def run():
add_mimetypes()
# Mako requires the directories to be added after the django setup.
microsite.enable_microsites(log)
if settings.FEATURES.get('USE_CUSTOM_THEME', False):
enable_stanford_theme()
if settings.FEATURES.get('USE_MICROSITES', False):
enable_microsites()
# Initialize Segment analytics module by setting the write_key.
if settings.LMS_SEGMENT_KEY:
analytics.write_key = settings.LMS_SEGMENT_KEY
......@@ -119,56 +123,12 @@ def enable_stanford_theme():
settings.LOCALE_PATHS = (theme_root / 'conf/locale',) + settings.LOCALE_PATHS
def enable_microsites_pre_startup():
"""
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
microsite_config_dict = settings.MICROSITE_CONFIGURATION
if microsite_config_dict:
settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].append(microsites_root)
def enable_microsites():
"""
Enable the use of microsites, which are websites that allow
for subdomains for the edX platform, e.g. foo.edx.org
Calls the enable_microsites function in the microsite backend.
Here for backwards compatibility
"""
microsites_root = settings.MICROSITE_ROOT_DIR
microsite_config_dict = settings.MICROSITE_CONFIGURATION
for ms_name, ms_config in microsite_config_dict.items():
# Calculate the location of the microsite's files
ms_root = microsites_root / ms_name
ms_config = microsite_config_dict[ms_name]
# pull in configuration information from each
# microsite root
if ms_root.isdir():
# store the path on disk for later use
ms_config['microsite_root'] = ms_root
template_dir = ms_root / 'templates'
ms_config['template_dir'] = template_dir
ms_config['microsite_name'] = ms_name
log.info('Loading microsite %s', ms_root)
else:
# not sure if we have application logging at this stage of
# startup
log.error('Error loading microsite %s. Directory does not exist', ms_root)
# remove from our configuration as it is not valid
del microsite_config_dict[ms_name]
# if we have any valid microsites defined, let's wire in the Mako and STATIC_FILES search paths
if microsite_config_dict:
edxmako.paths.add_lookup('main', microsites_root)
settings.STATICFILES_DIRS.insert(0, microsites_root)
microsite.enable_microsites(log)
def enable_third_party_auth():
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment