Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/cdodge/alpha microsite2 #2061

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from contentstore import utils

from microsite_configuration.middleware import MicrositeConfiguration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"microsite_configuration" is pretty long, wouldn't "microsite" be enough?


__all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler',
'settings_handler',
'grading_handler',
Expand Down Expand Up @@ -413,15 +415,21 @@ def settings_handler(request, tag=None, package_id=None, branch=None, version_gu
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
upload_asset_url = locator.url_reverse('assets/')

# see if the ORG of this course can be attributed to a 'Microsite'. In that case, the
# course about page should be editable in Studio
about_page_editable = not MicrositeConfiguration.get_microsite_configuration_value_for_org(
course_module.location.org,
'ENABLE_MKTG_SITE',
settings.FEATURES.get('ENABLE_MKTG_SITE', False)
)

return render_to_response('settings.html', {
'context_course': course_module,
'course_locator': locator,
'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_module.location),
'course_image_url': utils.course_image_url(course_module),
'details_url': locator.url_reverse('/settings/details/'),
'about_page_editable': not settings.FEATURES.get(
'ENABLE_MKTG_SITE', False
),
'about_page_editable': about_page_editable,
'upload_asset_url': upload_asset_url
})
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
Expand Down
14 changes: 10 additions & 4 deletions cms/djangoapps/contentstore/views/public.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

from external_auth.views import ssl_login_shortcut

from microsite_configuration.middleware import MicrositeConfiguration

__all__ = ['signup', 'login_page', 'howitworks']


Expand All @@ -29,10 +31,14 @@ def login_page(request):
Display the login form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('login.html', {
'csrf': csrf_token,
'forgot_password_link': "//{base}/login#forgot-password-modal".format(base=settings.LMS_BASE),
})
return render_to_response(
'login.html',
{
'csrf': csrf_token,
'forgot_password_link': "//{base}/login#forgot-password-modal".format(base=settings.LMS_BASE),
'platform_name': MicrositeConfiguration.get_microsite_configuration_value('platform_name', settings.PLATFORM_NAME),
}
)


def howitworks(request):
Expand Down
15 changes: 14 additions & 1 deletion cms/envs/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import json

from .common import *

from logsettings import get_logger_config
import os

Expand Down Expand Up @@ -145,7 +146,6 @@
#Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)


ENV_FEATURES = ENV_TOKENS.get('FEATURES', ENV_TOKENS.get('MITX_FEATURES', {}))
for feature, value in ENV_FEATURES.items():
FEATURES[feature] = value
Expand Down Expand Up @@ -213,3 +213,16 @@

# Event tracking
TRACKING_BACKENDS.update(AUTH_TOKENS.get("TRACKING_BACKENDS", {}))

SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {})
VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', [])

MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {})
MICROSITE_ROOT_DIR = ENV_TOKENS.get('MICROSITE_ROOT_DIR')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chrisndodge - I believe this should have a default value, otherwise it could throw a key error if the configuration script does not define a value for this key.

if len(MICROSITE_CONFIGURATION.keys()) > 0:
enable_microsites(
MICROSITE_CONFIGURATION,
SUBDOMAIN_BRANDING,
VIRTUAL_UNIVERSITIES,
microsites_root=path(MICROSITE_ROOT_DIR)
)
2 changes: 1 addition & 1 deletion cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

import sys
import lms.envs.common
from lms.envs.common import USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG
from lms.envs.common import USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, enable_microsites
from path import path

from lms.lib.xblock.mixin import LmsBlockMixin
Expand Down
15 changes: 15 additions & 0 deletions cms/envs/microsite_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
This is a localdev test for the Microsite processing pipeline
"""
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614

from .dev import *
from .dev import SUBDOMAIN_BRANDING, VIRTUAL_UNIVERSITIES

MICROSITE_NAMES = ['openedx']
MICROSITE_CONFIGURATION = {}

if MICROSITE_NAMES and len(MICROSITE_NAMES) > 0:
enable_microsites(MICROSITE_NAMES, MICROSITE_CONFIGURATION, SUBDOMAIN_BRANDING, VIRTUAL_UNIVERSITIES)
19 changes: 17 additions & 2 deletions common/djangoapps/edxmako/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from django.http import HttpResponse
import logging

from microsite_configuration.middleware import MicrositeConfiguration

import edxmako
import edxmako.middleware
from django.conf import settings
Expand All @@ -35,13 +37,18 @@ def marketing_link(name):
# link_map maps URLs from the marketing site to the old equivalent on
# the Django site
link_map = settings.MKTG_URL_LINK_MAP
if settings.FEATURES.get('ENABLE_MKTG_SITE') and name in settings.MKTG_URLS:
enable_mktg_site = MicrositeConfiguration.get_microsite_configuration_value(
'ENABLE_MKTG_SITE',
settings.FEATURES.get('ENABLE_MKTG_SITE', False)
)

if enable_mktg_site and name in settings.MKTG_URLS:
# special case for when we only want the root marketing URL
if name == 'ROOT':
return settings.MKTG_URLS.get('ROOT')
return settings.MKTG_URLS.get('ROOT') + settings.MKTG_URLS.get(name)
# only link to the old pages when the marketing site isn't on
elif not settings.FEATURES.get('ENABLE_MKTG_SITE') and name in link_map:
elif not enable_mktg_site and name in link_map:
# don't try to reverse disabled marketing links
if link_map[name] is not None:
return reverse(link_map[name])
Expand Down Expand Up @@ -71,6 +78,10 @@ def marketing_link_context_processor(request):


def render_to_string(template_name, dictionary, context=None, namespace='main'):

# see if there is an override template defined in the microsite
template_name = MicrositeConfiguration.get_microsite_template_path(template_name)

context_instance = Context(dictionary)
# add dictionary to context_instance
context_instance.update(dictionary or {})
Expand Down Expand Up @@ -98,5 +109,9 @@ def render_to_response(template_name, dictionary=None, context_instance=None, na
Returns a HttpResponse whose content is filled with the result of calling
lookup.get_template(args[0]).render with the passed arguments.
"""

# see if there is an override template defined in the microsite
template_name = MicrositeConfiguration.get_microsite_template_path(template_name)

dictionary = dictionary or {}
return HttpResponse(render_to_string(template_name, dictionary, context_instance, namespace), **kwargs)
Empty file.
183 changes: 183 additions & 0 deletions common/djangoapps/microsite_configuration/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
"""
This file implements the initial Microsite support for the Open edX platform.
A microsite enables the following features:

1) Mapping of sub-domain name to a 'brand', e.g. foo-university.edx.org
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

from django.conf import settings

_microsite_configuration_threadlocal = threading.local()
_microsite_configuration_threadlocal.data = {}


def has_microsite_configuration_set():
"""
Returns whether the MICROSITE_CONFIGURATION has been set in the configuration files
"""
return getattr(settings, "MICROSITE_CONFIGURATION", False)


class MicrositeConfiguration(object):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has a lot of stuff besides the middleware. Can't we have a MicrositeMiddleware class with just the process_request and process_response methods, and then another class to do the rest? It's odd to have a middleware class that is also referenced throughout the rest of the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I defer this to a post-release cleanup activity? My concern is if we put the static methods elsewhere, I'll have to change a lot of call sites which is more code churn than is probably wise at this point in time.

"""
Middleware class which will bind configuration information regarding 'microsites' on a per request basis.
The actual configuration information is taken from Django settings information
"""

@classmethod
def is_request_in_microsite(cls):
"""
This will return if current request is a request within a microsite
"""
return cls.get_microsite_configuration()

@classmethod
def get_microsite_configuration(cls):
"""
Returns the current request's microsite configuration
"""
if not hasattr(_microsite_configuration_threadlocal, 'data'):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The threadlocal has the data attribute, you set it at module level. Why check here that it exists?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do remember there was a specific reason but I can't remember the details now. I think it might have had to do if the classmethod was called outside of a HTTP request.

return {}

return _microsite_configuration_threadlocal.data

@classmethod
def get_microsite_configuration_value(cls, val_name, default=None):
"""
Returns a value associated with the request's microsite, if present
"""
configuration = cls.get_microsite_configuration()
return configuration.get(val_name, default)

@classmethod
def get_microsite_template_path(cls, relative_path):
"""
Returns a path to a Mako template, which can either be in
a microsite directory (as an override) or will just return what is passed in
"""

if not cls.is_request_in_microsite():
return relative_path

microsite_template_path = cls.get_microsite_configuration_value('template_dir')

if microsite_template_path:
search_path = microsite_template_path / relative_path

if os.path.isfile(search_path):
path = '{0}/templates/{1}'.format(
cls.get_microsite_configuration_value('microsite_name'),
relative_path
)
return path

return relative_path

@classmethod
def get_microsite_configuration_value_for_org(cls, 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_microsite_configuration_set():
return default

for key in settings.MICROSITE_CONFIGURATION.keys():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for key in settings.MICROSITE_CONFIGURATION:

Iterating a dict gives you its keys.

org_filter = settings.MICROSITE_CONFIGURATION[key].get('course_org_filter', None)
if org_filter == org:
return settings.MICROSITE_CONFIGURATION[key].get(val_name, default)
return default

@classmethod
def get_all_microsite_orgs(cls):
"""
This returns a set of orgs that are considered within a Microsite. This can be used,
for example, to do filtering
"""
org_filter_set = []
if not has_microsite_configuration_set():
return org_filter_set

for key in settings.MICROSITE_CONFIGURATION:
org_filter = settings.MICROSITE_CONFIGURATION[key].get('course_org_filter')
if org_filter:
org_filter_set.append(org_filter)

return org_filter_set

def clear_microsite_configuration(self):
"""
Clears out any microsite configuration from the current request/thread
"""
_microsite_configuration_threadlocal.data = {}

def process_request(self, request):
"""
Middleware entry point on every request processing. This will associate a request's domain name
with a 'University' and any corresponding microsite configuration information
"""
self.clear_microsite_configuration()

domain = request.META.get('HTTP_HOST', None)

if domain:
subdomain = MicrositeConfiguration.pick_subdomain(domain, settings.SUBDOMAIN_BRANDING.keys())
university = MicrositeConfiguration.match_university(subdomain)
microsite_configuration = self.get_microsite_configuration_for_university(university)
if microsite_configuration:
microsite_configuration['university'] = university
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is modifying a dictionary from settings, which seems very unusual.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cdodge have we figured out another way to do this?

microsite_configuration['subdomain'] = subdomain
microsite_configuration['site_domain'] = domain
_microsite_configuration_threadlocal.data = microsite_configuration

# also put the configuration on the request itself to make it easier to dereference
request.microsite_configuration = _microsite_configuration_threadlocal.data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not add it to the session? Why can't one of these mechanisms for passing this around be sufficient?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like request.microsite_configuration is used?

return None

def process_response(self, request, response):
"""
Middleware entry point for request completion.
"""
self.clear_microsite_configuration()
return response

def get_microsite_configuration_for_university(self, university):
"""
For a given university, return the microsite configuration which
is in the Django settings
"""
if not university:
return None

if not has_microsite_configuration_set():
return None

configuration = settings.MICROSITE_CONFIGURATION.get(university, None)
return configuration

@classmethod
def match_university(cls, domain):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function name sounds like you give it a university.

"""
Return the university name specified for the domain, or None
if no university was specified
"""
if not settings.FEATURES['SUBDOMAIN_BRANDING'] or domain is None:
return None

subdomain = cls.pick_subdomain(domain, settings.SUBDOMAIN_BRANDING.keys())
return settings.SUBDOMAIN_BRANDING.get(subdomain)

@classmethod
def pick_subdomain(cls, domain, options, default='default'):
"""
Attempt to match the incoming request's HOST domain with a configuration map
to see what subdomains are supported in Microsites.
"""
for option in options:
if domain.startswith(option):
return option
return default
Loading