Commit 31b6f8cc by David Baumgold

Merge pull request #10090 from edx/db/edx.org-comp-theme

Re-enable the edx.org comprehensive theme
parents 6fd960c8 58daa365
......@@ -177,6 +177,7 @@ ASSET_IGNORE_REGEX = ENV_TOKENS.get('ASSET_IGNORE_REGEX', ASSET_IGNORE_REGEX)
# Theme overrides
THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
COMPREHENSIVE_THEME_DIR = path(ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', COMPREHENSIVE_THEME_DIR))
#Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
......
......@@ -31,7 +31,8 @@ import lms.envs.common
# Although this module itself may not use these imported variables, other dependent modules may.
from lms.envs.common import (
USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, DATA_DIR, ALL_LANGUAGES, WIKI_ENABLED,
update_module_store_settings, ASSET_IGNORE_REGEX, COPYRIGHT_YEAR, PARENTAL_CONSENT_AGE_LIMIT, COMP_THEME_DIR,
update_module_store_settings, ASSET_IGNORE_REGEX, COPYRIGHT_YEAR,
PARENTAL_CONSENT_AGE_LIMIT, COMPREHENSIVE_THEME_DIR,
# The following PROFILE_IMAGE_* settings are included as they are
# indirectly accessed through the email opt-in API, which is
# technically accessible through the CMS via legacy URLs.
......@@ -467,12 +468,12 @@ EMBARGO_SITE_REDIRECT_URL = None
PIPELINE_ENABLED = True
# Process static files using RequireJS Optimizer
STATICFILES_STORAGE = 'openedx.core.lib.django_require.staticstorage.OptimizedCachedRequireJsStorage'
STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
# List of finder classes that know how to find static files in various locations.
# Note: the pipeline finder is included to be able to discover optimized files
STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder',
......
......@@ -37,10 +37,11 @@ FEATURES['PREVIEW_LMS_BASE'] = "preview." + LMS_BASE
# Skip packaging and optimization in development
PIPELINE_ENABLED = False
STATICFILES_STORAGE = 'pipeline.storage.NonPackagingPipelineStorage'
STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
# Revert to the default set of finders as we don't want the production pipeline
STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
......
......@@ -12,11 +12,7 @@
<div class="wrapper wrapper-l">
<h1 class="branding"><a href="/">
% if settings.FEATURES.get('IS_EDX_DOMAIN', False):
<img src="${static.url("images/edx-theme/edx-studio-logo.png")}" alt="${settings.STUDIO_NAME}" />
% else:
<img src="${static.url("images/default-theme/logo.png")}" alt="${settings.STUDIO_NAME}" />
% endif
<img src="${static.url("images/studio-logo.png")}" alt="${settings.STUDIO_NAME}" />
</a></h1>
% if context_course:
......
......@@ -175,7 +175,7 @@ class LearnerProfilePage(FieldsMixin, PageObject):
"""
self.wait_for_field('image')
default_links = self.q(css='.image-frame').attrs('src')
return 'default-profile' in default_links[0] if default_links else False
return 'profiles/default' in default_links[0] if default_links else False
def mouse_hover(self, element):
"""
......
<%namespace name='static' file='../../static_content.html'/>
<%! from microsite_configuration import microsite %>
<% style_overrides_file = microsite.get_value('css_overrides_file') %>
% if style_overrides_file:
<link rel="stylesheet" type="text/css" href="${static.url(style_overrides_file)}" />
% endif
......@@ -81,4 +81,4 @@ def get_logo_url():
elif university:
return staticfiles_storage.url('images/{uni}-on-edx-logo.png'.format(uni=university))
else:
return staticfiles_storage.url('images/default-theme/logo.png')
return staticfiles_storage.url('images/logo.png')
......@@ -232,7 +232,7 @@ def footer(request):
"title": "Powered by Open edX",
"image": "http://example.com/openedx.png"
},
"logo_image": "http://example.com/static/images/default-theme/logo.png",
"logo_image": "http://example.com/static/images/logo.png",
"copyright": "EdX, Open edX, and the edX and Open edX logos are \
registered trademarks or trademarks of edX Inc."
}
......
"""Tests of comprehensive theming."""
import unittest
from django.conf import settings
from django.test import TestCase
from path import path # pylint: disable=no-name-in-module
from django.contrib import staticfiles
from openedx.core.djangoapps.theming.test_util import with_comp_theme
from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
from openedx.core.lib.tempdir import mkdtemp_clean
......@@ -20,8 +19,7 @@ class TestComprehensiveTheming(TestCase):
# Clear the internal staticfiles caches, to get test isolation.
staticfiles.finders.get_finder.cache_clear()
@with_comp_theme(settings.REPO_ROOT / 'themes/red-theme')
@unittest.skip("Disabled until we can release theming to production")
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
def test_red_footer(self):
resp = self.client.get('/')
self.assertEqual(resp.status_code, 200)
......@@ -42,7 +40,7 @@ class TestComprehensiveTheming(TestCase):
with open(template_dir / "footer.html", "w") as footer:
footer.write("<footer>TEMPORARY THEME</footer>")
@with_comp_theme(tmp_theme)
@with_comprehensive_theme(tmp_theme)
def do_the_test(self):
"""A function to do the work so we can use the decorator."""
resp = self.client.get('/')
......@@ -56,7 +54,7 @@ class TestComprehensiveTheming(TestCase):
before_finders = list(settings.STATICFILES_FINDERS)
before_dirs = list(settings.STATICFILES_DIRS)
@with_comp_theme(settings.REPO_ROOT / 'themes/red-theme')
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
def do_the_test(self):
"""A function to do the work so we can use the decorator."""
self.assertEqual(list(settings.STATICFILES_FINDERS), before_finders)
......@@ -65,12 +63,11 @@ class TestComprehensiveTheming(TestCase):
do_the_test(self)
@unittest.skip("Disabled until we can release theming to production")
def test_default_logo_image(self):
result = staticfiles.finders.find('images/logo.png')
self.assertEqual(result, settings.REPO_ROOT / 'lms/static/images/logo.png')
@with_comp_theme(settings.REPO_ROOT / 'themes/red-theme')
@with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
def test_overridden_logo_image(self):
result = staticfiles.finders.find('images/logo.png')
self.assertEqual(result, settings.REPO_ROOT / 'themes/red-theme/lms/static/images/logo.png')
......@@ -237,7 +237,7 @@ BULK_EMAIL_ROUTING_KEY_SMALL_JOBS = LOW_PRIORITY_QUEUE
# Theme overrides
THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
COMP_THEME_DIR = path(ENV_TOKENS.get('COMP_THEME_DIR', COMP_THEME_DIR))
COMPREHENSIVE_THEME_DIR = path(ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', COMPREHENSIVE_THEME_DIR))
# Marketing link overrides
MKTG_URL_LINK_MAP.update(ENV_TOKENS.get('MKTG_URL_LINK_MAP', {}))
......@@ -701,10 +701,7 @@ PROFILE_IMAGE_BACKEND = ENV_TOKENS.get('PROFILE_IMAGE_BACKEND', PROFILE_IMAGE_BA
PROFILE_IMAGE_SECRET_KEY = AUTH_TOKENS.get('PROFILE_IMAGE_SECRET_KEY', PROFILE_IMAGE_SECRET_KEY)
PROFILE_IMAGE_MAX_BYTES = ENV_TOKENS.get('PROFILE_IMAGE_MAX_BYTES', PROFILE_IMAGE_MAX_BYTES)
PROFILE_IMAGE_MIN_BYTES = ENV_TOKENS.get('PROFILE_IMAGE_MIN_BYTES', PROFILE_IMAGE_MIN_BYTES)
if FEATURES['IS_EDX_DOMAIN']:
PROFILE_IMAGE_DEFAULT_FILENAME = 'images/edx-theme/default'
else:
PROFILE_IMAGE_DEFAULT_FILENAME = ENV_TOKENS.get('PROFILE_IMAGE_DEFAULT_FILENAME', PROFILE_IMAGE_DEFAULT_FILENAME)
PROFILE_IMAGE_DEFAULT_FILENAME = 'images/profiles/default'
# EdxNotes config
......
......@@ -426,7 +426,7 @@ COURSES_ROOT = ENV_ROOT / "data"
DATA_DIR = COURSES_ROOT
# comprehensive theming system
COMP_THEME_DIR = ""
COMPREHENSIVE_THEME_DIR = ""
# TODO: Remove the rest of the sys.path modification here and in cms/envs/common.py
sys.path.append(REPO_ROOT)
......@@ -1087,7 +1087,7 @@ FOOTER_OPENEDX_LOGO_IMAGE = "https://files.edx.org/openedx-logos/edx-openedx-log
# This is just a placeholder image.
# Site operators can customize this with their organization's image.
FOOTER_ORGANIZATION_IMAGE = "images/default-theme/logo.png"
FOOTER_ORGANIZATION_IMAGE = "images/logo.png"
# These are referred to both by the Django asset pipeline
# AND by the branding footer API, which needs to decide which
......@@ -1197,12 +1197,12 @@ X_FRAME_OPTIONS = 'ALLOW'
PIPELINE_ENABLED = True
# Process static files using RequireJS Optimizer
STATICFILES_STORAGE = 'openedx.core.lib.django_require.staticstorage.OptimizedCachedRequireJsStorage'
STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
# List of finder classes that know how to find static files in various locations.
# Note: the pipeline finder is included to be able to discover optimized files
STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder',
......@@ -2550,7 +2550,7 @@ PDF_RECEIPT_TAX_ID_LABEL = 'Tax ID'
PDF_RECEIPT_LOGO_PATH = PROJECT_ROOT + '/static/images/openedx-logo-tag.png'
# Height of the Logo in mm
PDF_RECEIPT_LOGO_HEIGHT_MM = 12
PDF_RECEIPT_COBRAND_LOGO_PATH = PROJECT_ROOT + '/static/images/default-theme/logo.png'
PDF_RECEIPT_COBRAND_LOGO_PATH = PROJECT_ROOT + '/static/images/logo.png'
# Height of the Co-brand Logo in mm
PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM = 12
......@@ -2647,7 +2647,7 @@ PROFILE_IMAGE_BACKEND = {
'base_url': os.path.join(MEDIA_URL, 'profile-images/'),
},
}
PROFILE_IMAGE_DEFAULT_FILENAME = 'images/default-theme/default-profile'
PROFILE_IMAGE_DEFAULT_FILENAME = 'images/profiles/default'
PROFILE_IMAGE_DEFAULT_FILE_EXTENSION = 'png'
# This secret key is used in generating unguessable URLs to users'
# profile images. Once it has been set, changing it will make the
......
......@@ -86,12 +86,12 @@ def should_show_debug_toolbar(_):
########################### PIPELINE #################################
# Skip packaging and optimization in development
PIPELINE_ENABLED = False
STATICFILES_STORAGE = 'pipeline.storage.NonPackagingPipelineStorage'
STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
# Revert to the default set of finders as we don't want the production pipeline
STATICFILES_FINDERS = [
'openedx.core.djangoapps.theming.finders.ComprehensiveThemeFinder',
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
......
......@@ -10,18 +10,6 @@ from edxmako.shortcuts import marketing_link
<%inherit file="../main.html" />
<%block name="headextra">
<%
if self.theme_enabled():
google_analytics_file = u'../{ga}'.format(
ga=microsite.get_value('google_analytics_file', 'theme-google-analytics.html')
)
else:
google_analytics_file = '../google_analytics.html'
%>
<%include file="${google_analytics_file}" />
## OG (Open Graph) title and description added below to give social media info to display
## (https://developers.facebook.com/docs/opengraph/howtos/maximizing-distribution-media-content#tags)
<meta property="og:title" content="${get_course_about_section(request, course, 'title')}" />
......
% if settings.GOOGLE_ANALYTICS_ACCOUNT:
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '${settings.GOOGLE_ANALYTICS_ACCOUNT}']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
% endif
<%doc>
Yet, installing google tag manager for microsite(s).
So intentionally left it blank
</%doc>
## mako
<%namespace name='static' file='static_content.html'/>
<%!
from microsite_configuration import microsite
......@@ -5,13 +6,8 @@ from microsite_configuration import microsite
<%
theme_enabled = settings.FEATURES.get("USE_CUSTOM_THEME", False)
is_microsite = microsite.is_request_in_microsite()
style_overrides_file = microsite.get_value('css_overrides_file')
%>
% if style_overrides_file:
<link rel="stylesheet" type="text/css" href="${static.url(style_overrides_file)}" />
% endif
% if theme_enabled and not is_microsite:
<%include file="theme-header.html" />
% else:
......
......@@ -61,7 +61,17 @@ from branding import api as branding_api
<link rel="icon" type="image/x-icon" href="${static.url(microsite.get_value('favicon_path', settings.FAVICON_PATH))}" />
<%static:css group='style-vendor'/>
<%static:css group='style-main'/>
## We could do <%static:css group='style-main'/>, but that's only useful
## if the group contains multiple files, and the 'style-main' group doesn't.
## Instead, we'll construct this <link> element manually, to improve clarity.
## When nothing in the system is referencing the 'style-main' group, it can
## be removed from the environment file.
<%
application_css_path = "css/lms-main{rtl}.css".format(
rtl="-rtl" if get_language_bidi() else "",
)
%>
<link rel="stylesheet" href="${static.url(application_css_path)}" type="text/css" media="all" />
% if disable_courseware_js:
<%static:js group='base_vendor'/>
......@@ -84,31 +94,7 @@ from branding import api as branding_api
<%block name="headextra"/>
<%
if theme_enabled() and not is_microsite():
header_extra_file = 'theme-head-extra.html'
header_file = 'theme-header.html'
google_analytics_file = 'theme-google-analytics.html'
style_overrides_file = None
else:
header_extra_file = microsite.get_template_path('header_extra.html')
if settings.FEATURES['IS_EDX_DOMAIN'] and not is_microsite():
header_file = microsite.get_template_path('navigation-edx.html')
else:
header_file = microsite.get_template_path('navigation.html')
google_analytics_file = microsite.get_template_path('google_analytics.html')
style_overrides_file = microsite.get_value('css_overrides_file')
google_tag_manager_file = microsite.get_template_path('google_tag_manager.html')
%>
% if header_extra_file:
<%include file="${header_extra_file}" />
% endif
<%static:optional_include_mako file="head-extra.html" with_microsite="True" />
<%include file="widgets/optimizely.html" />
<%include file="widgets/segment-io.html" />
......@@ -116,16 +102,25 @@ from branding import api as branding_api
<meta name="path_prefix" content="${EDX_ROOT_URL}">
<meta name="google-site-verification" content="_mipQ4AtZQDNmbtOkwehQDOgCxUUV2fb_C0b6wbiRHY" />
<%include file="${google_analytics_file}" />
% if style_overrides_file:
<link rel="stylesheet" type="text/css" href="${static.url(style_overrides_file)}" />
<% ga_acct = microsite.get_value("GOOGLE_ANALYTICS_ACCOUNT", settings.GOOGLE_ANALYTICS_ACCOUNT) %>
% if ga_acct:
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '${ga_acct}']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
% endif
</head>
<body class="${static.dir_rtl()} <%block name='bodyclass'/> lang_${LANGUAGE_CODE}">
<%include file="${google_tag_manager_file}" />
<%static:optional_include_mako file="body-initial.html" with_microsite="True" />
<div id="page-prompt"></div>
% if not disable_window_wrap:
<div class="window-wrap" dir="${static.dir_rtl()}">
......@@ -133,7 +128,7 @@ from branding import api as branding_api
<a class="nav-skip" href="<%block name="nav_skip">#content</%block>">${_("Skip to main content")}</a>
% if not disable_header:
<%include file="${header_file}" />
<%include file="${microsite.get_template_path('header.html')}" />
% endif
<div class="content-wrapper" id="content">
......@@ -142,16 +137,7 @@ from branding import api as branding_api
</div>
% if not disable_footer:
<%block name="footer">
## Can be overridden by child templates wanting to hide the footer.
% if theme_enabled() and not is_microsite():
<%include file="theme-footer.html" />
% elif settings.FEATURES.get('IS_EDX_DOMAIN', False) and not is_microsite():
<%include file="footer-edx-v3.html" />
% else:
<%include file="${microsite.get_template_path('footer.html')}" />
% endif
</%block>
<%include file="themable-footer.html" />
% endif
% if not disable_window_wrap:
......@@ -162,6 +148,7 @@ from branding import api as branding_api
<%include file="widgets/segment-io-footer.html" />
<script type="text/javascript" src="${static.url('js/vendor/noreferrer.js')}" charset="utf-8"></script>
<script type="text/javascript" src="${static.url('js/utils/navigation.js')}" charset="utf-8"></script>
<%static:optional_include_mako file="body-extra.html" with_microsite="True" />
</body>
</html>
......
<!DOCTYPE html>
{% load pipeline %}
{% load sekizai_tags i18n microsite %}
{% load sekizai_tags i18n microsite pipeline optional_include %}
{% load url from future %}
{% load staticfiles %}
<html lang="{{LANGUAGE_CODE}}">
<head>
<meta charset="UTF-8">
......@@ -21,7 +19,7 @@
{% block headextra %}{% endblock %}
{% render_block "css" %}
{% microsite_css_overrides_file %}
{% optional_include "head-extra.html" %}
<meta name="path_prefix" content="{{EDX_ROOT_URL}}">
</head>
......@@ -30,23 +28,15 @@
<div class="window-wrap" dir="${static.dir_rtl()}">
<a class="nav-skip" href="{% block nav_skip %}#content{% endblock %}">{% trans "Skip to main content" %}</a>
{% with course=request.course %}
{% if IS_EDX_DOMAIN %}
{% include "navigation-edx.html" %}
{% else %}
{% include "navigation.html" %}
{% endif %}
{% include "header.html" %}
{% endwith %}
<div class="content-wrapper" id="content">
{% block body %}{% endblock %}
{% block bodyextra %}{% endblock %}
</div>
{% if IS_REQUEST_IN_MICROSITE %}
{# For now we don't support overriden Django templates in microsites. Leave footer blank for now which is better than saying Edx.#}
{% elif IS_EDX_DOMAIN %}
{% include "footer-edx-v3.html" %}
{% else %}
{% with course=request.course %}
{% include "footer.html" %}
{% endif %}
{% endwith %}
</div>
......
## mako
<%!
from microsite_configuration import microsite
%>
<%
theme_enabled = settings.FEATURES.get("USE_CUSTOM_THEME", False)
is_microsite = microsite.is_request_in_microsite()
%>
## This file only exists as an additional layer of indirection to preserve
## backwards compatibility with microsites and Stanford theming
## (as much as possible). If you are writing your own theme using the
## "comprehensive theming" system, do NOT override this file. You should
## override "footer.html" instead.
% if theme_enabled and not is_microsite:
<%include file="theme-footer.html" />
% else:
<%include file="${microsite.get_template_path('footer.html')}" />
% endif
"""
Core logic for Comprehensive Theming.
"""
from path import Path
from django.conf import settings
......@@ -28,21 +29,26 @@ def comprehensive_theme_changes(theme_dir):
'settings': {},
'mako_paths': [],
}
root = Path(settings.PROJECT_ROOT)
if root.name == "":
root = root.parent
templates_dir = theme_dir / "lms" / "templates"
component_dir = theme_dir / root.name
templates_dir = component_dir / "templates"
if templates_dir.isdir():
changes['settings']['TEMPLATE_DIRS'] = [templates_dir] + settings.DEFAULT_TEMPLATE_ENGINE['DIRS']
changes['mako_paths'].append(templates_dir)
staticfiles_dir = theme_dir / "lms" / "static"
staticfiles_dir = component_dir / "static"
if staticfiles_dir.isdir():
changes['settings']['STATICFILES_DIRS'] = [staticfiles_dir] + settings.STATICFILES_DIRS
locale_dir = theme_dir / "lms" / "conf" / "locale"
locale_dir = component_dir / "conf" / "locale"
if locale_dir.isdir():
changes['settings']['LOCALE_PATHS'] = [locale_dir] + settings.LOCALE_PATHS
favicon = theme_dir / "lms" / "static" / "images" / "favicon.ico"
favicon = component_dir / "static" / "images" / "favicon.ico"
if favicon.isfile():
changes['settings']['FAVICON_PATH'] = str(favicon)
......
"""
Static file finders for Django.
https://docs.djangoproject.com/en/1.8/ref/settings/#std:setting-STATICFILES_FINDERS
Yes, this interface is private and undocumented, but we need to access it anyway.
In order to deploy Open edX in production, it's important to be able to collect
and process static assets: images, CSS, JS, fonts, etc. Django's collectstatic
system is the accepted way to do that in Django-based projects, but that system
doesn't handle every kind of collection and processing that web developers need.
Other open source projects like `Django-Pipeline`_ and `Django-Require`_ hook
into Django's collectstatic system to provide features like minification,
compression, Sass pre-processing, and require.js optimization for assets before
they are pushed to production. To make sure that themed assets are collected
and served by the system (in addition to core assets), we need to extend this
interface, as well.
.. _Django-Pipeline: http://django-pipeline.readthedocs.org/
.. _Django-Require: https://github.com/etianen/django-require
"""
from path import Path
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.contrib.staticfiles import utils
from django.contrib.staticfiles.finders import BaseFinder
from openedx.core.djangoapps.theming.storage import CachedComprehensiveThemingStorage
class ComprehensiveThemeFinder(BaseFinder):
"""
A static files finder that searches the active comprehensive theme
for static files. If the ``COMPREHENSIVE_THEME_DIR`` setting is unset,
or the ``COMPREHENSIVE_THEME_DIR`` does not exist on the file system,
this finder will never find any files.
"""
def __init__(self, *args, **kwargs):
super(ComprehensiveThemeFinder, self).__init__(*args, **kwargs)
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
if not theme_dir:
self.storage = None
return
if not isinstance(theme_dir, basestring):
raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
root = Path(settings.PROJECT_ROOT)
if root.name == "":
root = root.parent
component_dir = Path(theme_dir) / root.name
static_dir = component_dir / "static"
self.storage = CachedComprehensiveThemingStorage(location=static_dir)
def find(self, path, all=False): # pylint: disable=redefined-builtin
"""
Looks for files in the default file storage, if it's local.
"""
if not self.storage:
return []
if path.startswith(self.storage.prefix):
# strip the prefix
path = path[len(self.storage.prefix):]
if self.storage.exists(path):
match = self.storage.path(path)
if all:
match = [match]
return match
return []
def list(self, ignore_patterns):
"""
List all files of the storage.
"""
if self.storage and self.storage.exists(''):
for path in utils.get_files(self.storage, ignore_patterns):
yield path, self.storage
......@@ -10,5 +10,5 @@ from .core import enable_comprehensive_theme
def run():
"""Enable comprehensive theming, if we should."""
if settings.COMP_THEME_DIR:
enable_comprehensive_theme(theme_dir=path(settings.COMP_THEME_DIR))
if settings.COMPREHENSIVE_THEME_DIR:
enable_comprehensive_theme(theme_dir=path(settings.COMPREHENSIVE_THEME_DIR))
"""
Comprehensive Theming support for Django's collectstatic functionality.
See https://docs.djangoproject.com/en/1.8/ref/contrib/staticfiles/
"""
from path import Path
import os.path
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin
from django.utils._os import safe_join
class ComprehensiveThemingAwareMixin(object):
"""
Mixin for Django storage system to make it aware of the currently-active
comprehensive theme, so that it can generate theme-scoped URLs for themed
static assets.
"""
def __init__(self, *args, **kwargs):
super(ComprehensiveThemingAwareMixin, self).__init__(*args, **kwargs)
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
if not theme_dir:
self.theme_location = None
return
if not isinstance(theme_dir, basestring):
raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
root = Path(settings.PROJECT_ROOT)
if root.name == "":
root = root.parent
component_dir = Path(theme_dir) / root.name
self.theme_location = component_dir / "static"
@property
def prefix(self):
"""
This is used by the ComprehensiveThemeFinder in the collection step.
"""
theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
if not theme_dir:
return None
theme_name = os.path.basename(os.path.normpath(theme_dir))
return "themes/{name}/".format(name=theme_name)
def themed(self, name):
"""
Given a name, return a boolean indicating whether that name exists
as a themed asset in the comprehensive theme.
"""
# Nothing can be themed if we don't have a theme location.
if not self.theme_location:
return False
path = safe_join(self.theme_location, name)
return os.path.exists(path)
def path(self, name):
"""
Get the path to the real asset on disk
"""
if self.themed(name):
base = self.theme_location
else:
base = self.location
path = safe_join(base, name)
return os.path.normpath(path)
def url(self, name, *args, **kwargs):
"""
Add the theme prefix to the asset URL
"""
if self.themed(name):
name = self.prefix + name
return super(ComprehensiveThemingAwareMixin, self).url(name, *args, **kwargs)
class CachedComprehensiveThemingStorage(
ComprehensiveThemingAwareMixin,
CachedFilesMixin,
StaticFilesStorage
):
"""
Used by the ComprehensiveThemeFinder class. Mixes in support for cached
files and comprehensive theming in static files.
"""
pass
"""
The functions in this module are based on the contents of
https://github.com/django/django/blob/1.8.5/django/template/loader_tags.py --
specifically, the do_include function. It has been modified as little as
possible, in order to match the behavior of the {% include %} template tag,
except for making it optional.
"""
# Because we want to match the original loader_tags.py file as closely as
# possible, we should disable pylint so it doesn't complain about the violations
# that are already in that file
# pylint: skip-file
from django.template.base import (
TemplateSyntaxError, Library, token_kwargs, TemplateDoesNotExist
)
from django.template.loader_tags import IncludeNode
register = Library()
class OptionalIncludeNode(IncludeNode):
def render(self, context):
try:
return super(OptionalIncludeNode, self).render(context)
except TemplateDoesNotExist:
return ''
@register.tag('optional_include')
def do_include(parser, token):
"""
Loads a template and renders it with the current context, if it exists.
You can pass additional context using keyword arguments.
Example::
{% optional_include "foo/some_include" %}
{% optional_include "foo/some_include" with bar="BAZZ!" baz="BING!" %}
Use the ``only`` argument to exclude the current context when rendering
the included template::
{% optional_include "foo/some_include" only %}
{% optional_include "foo/some_include" with bar="1" only %}
"""
bits = token.split_contents()
if len(bits) < 2:
msg = (
"%r tag takes at least one argument: the name of the template "
"to be optionally included."
) % bits[0]
raise TemplateSyntaxError(msg)
options = {}
remaining_bits = bits[2:]
while remaining_bits:
option = remaining_bits.pop(0)
if option in options:
raise TemplateSyntaxError('The %r option was specified more '
'than once.' % option)
if option == 'with':
value = token_kwargs(remaining_bits, parser, support_legacy=False)
if not value:
raise TemplateSyntaxError('"with" in %r tag needs at least '
'one keyword argument.' % bits[0])
elif option == 'only':
value = True
else:
raise TemplateSyntaxError('Unknown argument for %r tag: %r.' %
(bits[0], option))
options[option] = value
isolated_context = options.get('only', False)
namemap = options.get('with', {})
node = OptionalIncludeNode(
parser.compile_filter(bits[1]),
extra_context=namemap,
isolated_context=isolated_context,
)
return node
......@@ -16,7 +16,7 @@ import edxmako
from .core import comprehensive_theme_changes
def with_comp_theme(theme_dir):
def with_comprehensive_theme(theme_dir):
"""
A decorator to run a test with a particular comprehensive theme.
......@@ -34,7 +34,7 @@ def with_comp_theme(theme_dir):
def _decorator(func): # pylint: disable=missing-docstring
@wraps(func)
def _decorated(*args, **kwargs): # pylint: disable=missing-docstring
with override_settings(COMP_THEME_DIR=theme_dir, **changes['settings']):
with override_settings(COMPREHENSIVE_THEME_DIR=theme_dir, **changes['settings']):
with edxmako.save_lookups():
for template_dir in changes['mako_paths']:
edxmako.paths.add_lookup('main', template_dir, prepend=True)
......@@ -60,8 +60,8 @@ def with_is_edx_domain(is_edx_domain):
# decorators, which is confusing.
def _decorator(func): # pylint: disable=missing-docstring
if is_edx_domain:
# This applies @with_comp_theme to the func.
func = with_comp_theme(settings.REPO_ROOT / "themes" / "edx.org")(func)
# This applies @with_comprehensive_theme to the func.
func = with_comprehensive_theme(settings.REPO_ROOT / "themes" / "edx.org")(func)
# This applies @patch.dict() to the func to set IS_EDX_DOMAIN.
func = patch.dict('django.conf.settings.FEATURES', {"IS_EDX_DOMAIN": is_edx_domain})(func)
......
"""
Django storage backends for Open edX.
"""
from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin
from pipeline.storage import PipelineMixin, NonPackagingMixin
from require.storage import OptimizedFilesMixin
from openedx.core.djangoapps.theming.storage import ComprehensiveThemingAwareMixin
class ProductionStorage(
ComprehensiveThemingAwareMixin,
OptimizedFilesMixin,
PipelineMixin,
CachedFilesMixin,
StaticFilesStorage
):
"""
This class combines Django's StaticFilesStorage class with several mixins
that provide additional functionality. We use this version on production.
"""
pass
class DevelopmentStorage(
ComprehensiveThemingAwareMixin,
NonPackagingMixin,
PipelineMixin,
StaticFilesStorage
):
"""
This class combines Django's StaticFilesStorage class with several mixins
that provide additional functionality. We use this version for development,
so that we can skip packaging and optimization.
"""
pass
......@@ -45,18 +45,18 @@ def configure_paths():
css_dir.mkdir_p()
SASS_DIRS.append(sass_dir)
if edxapp_env.env_tokens.get("COMP_THEME_DIR", ""):
theme_dir = path(edxapp_env.env_tokens["COMP_THEME_DIR"])
if edxapp_env.env_tokens.get("COMPREHENSIVE_THEME_DIR", ""):
theme_dir = path(edxapp_env.env_tokens["COMPREHENSIVE_THEME_DIR"])
lms_sass = theme_dir / "lms" / "static" / "sass"
lms_css = theme_dir / "lms" / "static" / "css"
if lms_sass.isdir():
lms_css.mkdir_p()
SASS_DIRS.append(lms_sass)
studio_sass = theme_dir / "studio" / "static" / "sass"
studio_css = theme_dir / "studio" / "static" / "css"
if studio_sass.isdir():
studio_css.mkdir_p()
SASS_DIRS.append(studio_sass)
cms_sass = theme_dir / "cms" / "static" / "sass"
cms_css = theme_dir / "cms" / "static" / "css"
if cms_sass.isdir():
cms_css.mkdir_p()
SASS_DIRS.append(cms_sass)
configure_paths()
......
......@@ -90,6 +90,28 @@ in the appropriate place, and making the changes you need. Keep in mind that
in the future if you upgrade the Open edX code, you may have to update the
copied template in your theme also.
Template Names
==============
Here are the list of template names that you *should* use in your comprehensive
theme (so far):
* ``header.html``
* ``footer.html``
You should **not** use the following names in your comprehensive theme:
* ``themable-footer.html``
If you look at the ``main.html`` template file, you will notice that it includes
``header.html`` and ``themable-footer.html``, rather than ``footer.html``.
You might be inclined to override ``themable-footer.html`` as a result. DO NOT
DO THIS. ``themable-footer.html`` is an additional layer of indirection that
is necessary to avoid breaking microsites, which also refers to a file named
``footer.html``. The goal is to eventually make comprehensive theming do
everything that microsites does now, and then deprecate and remove microsites
from the codebase. At that point, the ``themable-footer.html`` file will go
away, since the additional layer of indirection will no longer be necessary.
Installing your theme
---------------------
......@@ -101,9 +123,9 @@ directory. There are two ways to do this.
#. As the vagrant user, edit (or create)
/edx/app/edx_ansible/server-vars.yml to add the
``edxapp_comp_theme_dir`` value::
``edxapp_comprehensive_theme_dir`` value::
edxapp_comp_theme_dir: '/full/path/to/my-theme'
edxapp_comprehensive_theme_dir: '/full/path/to/my-theme'
#. Run the update script::
......@@ -111,13 +133,18 @@ directory. There are two ways to do this.
$ sudo /edx/bin/update edx-platform HEAD
#. Otherwise, edit the /edx/app/edxapp/lms.env.json file to add the
``COMP_THEME_DIR`` value::
``COMPREHENSIVE_THEME_DIR`` value::
"COMP_THEME_DIR": "/full/path/to/my-theme",
"COMPREHENSIVE_THEME_DIR": "/full/path/to/my-theme",
Restart your site. Your changes should now be visible.
Comprehensive Theming
=====================
* The ``PROFILE_IMAGE_DEFAULT_FILENAME`` Django setting is now ignored.
"Stanford" theming
==================
......@@ -154,3 +181,54 @@ name in the ``@import`` line.
Run the ``update_assets`` command to recompile the theme::
$ paver update_assets lms --settings=aws
Microsites
==========
If you want to continue using the "Microsites" theming system, there are a few
changes you'll need to make. A few templates have been renamed, or folded into
other templates:
* ``header_extra.html`` has been renamed to ``head-extra.html``. This file
was always inserted into the ``<head>`` element of the page, rather than
the header of the ``<body>`` element, so this change makes the name more
accurate.
* ``google_analytics.html`` has been removed. The contents of this template
can and should be added to the ``head-extra.html`` template.
* ``google_tag_manager.html`` has been renamed to ``body-initial.html``.
In addition, there are some other changes you'll need to make:
* The ``google_analytics_file`` config value is now ignored. If your Open edX
installation has a Google Analytics account ID set, the Google Analytics
JavaScript will be included automatically on your site using that account ID.
You can set this account ID either using the "GOOGLE_ANALYTICS_ACCOUNT" value
in the Django settings, or by setting the newly-added "GOOGLE_ANALYTICS_ACCOUNT"
config value in your microsite configuration.
* If you don't want the Google Analytics JavaScript to be output at all in your
microsite, set the "GOOGLE_ANALYTICS_ACCOUNT" config value to the empty string.
If you want to customize the way that Google Analytics is loaded, set the
"GOOGLE_ANALYTICS_ACCOUNT" config value to the empty string, and then load
Google Analytics yourself (with whatever customizations you want) in your
``head-extra.html`` template.
* The ``css_overrides_file`` config value is now ignored. To add a CSS override
file to your microsite, create a ``head-extra.html`` template with the
following content:
.. code-block:: mako
<%namespace name='static' file='../../static_content.html'/>
<%! from microsite_configuration import microsite %>
<% style_overrides_file = microsite.get_value('css_overrides_file') %>
% if style_overrides_file:
<link rel="stylesheet" type="text/css" href="${static.url(style_overrides_file)}" />
% endif
If you already have a ``head-extra.html`` template, you can modify it to
output this ``<link rel="stylesheet">`` tag, in addition to whatever else you
already have in that template.
## mako
<%!
from django.utils.translation import ugettext as _
from branding.api import get_footer
%>
<% footer = get_footer(is_secure=is_secure) %>
<%namespace name='static' file='static_content.html'/>
## WARNING: These files are specific to edx.org and are not used in installations outside of that domain. Open edX users will want to use the file "footer.html" for any changes or overrides.
<footer id="footer-edx-v3" role="contentinfo" aria-label="${_("Page Footer")}"
## When rendering the footer through the branding API,
## the direction may not be set on the parent element,
## so we set it here.
% if bidi:
dir=${bidi}
% endif
>
<h2 class="sr footer-about-title">${_("About edX")}</h2>
<div class="footer-content-wrapper">
<div class="footer-logo">
<img alt="edX logo" src="${footer['logo_image']}">
</div>
<div class="site-details">
<nav class="site-nav" aria-label="${_("About edX")}">
% for link in footer["navigation_links"]:
<a href="${link['url']}">${link['title']}</a>
% endfor
</nav>
<nav class="legal-notices" aria-label="${_("Legal")}">
% for link in footer["legal_links"]:
<a href="${link['url']}">${link['title']}</a>
% endfor
</nav>
<p class="copyright">${footer['copyright']}</p>
## The OpenEdX link may be hidden when this view is served
## through an API to partner sites (such as marketing sites or blogs),
## which are not technically powered by OpenEdX.
% if not hide_openedx_link:
<div class="openedx-link">
<a href="${footer['openedx_link']['url']}" title="${footer['openedx_link']['title']}">
<img alt="${footer['openedx_link']['title']}" src="${footer['openedx_link']['image']}" width="140">
</a>
</div>
% endif
</div>
<div class="external-links">
<div class="social-media-links">
% for link in footer['social_links']:
<a href="${link['url']}" class="sm-link external" title="${link['title']}" rel="noreferrer">
<span class="icon fa ${link['icon-class']}" aria-hidden="true"></span>
<span class="sr">${link['action']}</span>
</a>
% endfor
</div>
<div class="mobile-app-links">
% for link in footer['mobile_links']:
<a href="${link['url']}" class="app-link external">
<img alt="${link['title']}" src="${link['image']}">
</a>
% endfor
</div>
</div>
</div>
</footer>
% if include_dependencies:
<%static:js group='base_vendor'/>
<%static:css group='style-vendor'/>
<%include file="widgets/segment-io.html" />
<%include file="widgets/segment-io-footer.html" />
% endif
% if footer_css_urls:
% for url in footer_css_urls:
<link rel="stylesheet" type="text/css" href="${url}"></link>
% endfor
% endif
% if footer_js_url:
<script type="text/javascript" src="${footer_js_url}"></script>
% endif
## mako
<%namespace name='static' file='static_content.html'/>
<%namespace file='main.html' import="login_query"/>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from microsite_configuration import microsite
from microsite_configuration.templatetags.microsite import platform_name
# App that handles subdomain specific branding
import branding
# app that handles site status messages
from status.status import get_site_status_msg
%>
## Provide a hook for themes to inject branding on top.
<%block name="navigation_top" />
<%block>
<%
try:
course_id = course.id
except:
# can't figure out a better way to get at a possibly-defined course var
course_id = None
site_status_msg = get_site_status_msg(course_id)
%>
% if site_status_msg:
<div class="site-status">
<div class="inner-wrapper">
<span class="white-error-icon"></span>
<p>${site_status_msg}</p>
</div>
</div>
% endif
</%block>
<header class="${"global slim" if course and not disable_courseware_header else "global-new"}" aria-label="Main" role="banner">
<div class="${'rwd ' if responsive else ''}wrapper-header nav-container">
<h1 class="logo" itemscope="" itemtype="http://schema.org/Organization">
<a href="${marketing_link('ROOT')}" itemprop="url">
<%block name="navigation_logo">
<img src="${static.url("images/logo.png")}" alt="${_("{platform_name} Home Page").format(platform_name=platform_name())}" itemprop="logo" />
</%block>
</a>
</h1>
% if course and not disable_courseware_header:
<h2 class="course-header">
<span class="provider">${course.display_org_with_default | h}:</span>
<span class="course-number">${course.display_number_with_default | h}</span>
<span class="course-name">${course.display_name_with_default}</span>
</h2>
% endif
% if user.is_authenticated():
% if not course or disable_courseware_header:
% if not nav_hidden:
<nav aria-label="Main" class="nav-main">
<ul class="left nav-global authenticated">
<%block name="navigation_global_links_authenticated">
<li class="nav-global-01">
<a href="${marketing_link('HOW_IT_WORKS')}">${_("How it Works")}</a>
</li>
<li class="nav-global-02">
<a href="${marketing_link('COURSES')}">${_("Find Courses")}</a>
</li>
<li class="nav-global-03">
<a href="${marketing_link('SCHOOLS')}">${_("Schools & Partners")}</a>
</li>
</%block>
</ul>
</nav>
% endif
% endif
<ul class="user">
<li class="primary">
<a href="${reverse('dashboard')}" class="user-link">
<span class="sr">${_("Dashboard for:")}</span>
<div>${user.username}</div>
</a>
</li>
<li class="primary">
<a href="#" class="dropdown" aria-haspopup="true" aria-expanded="false"><span class="sr">${_("More options dropdown")}</span> &#9662;</a>
<ul class="dropdown-menu" aria-label="More Options" role="menu">
<%block name="navigation_dropdown_menu_links" >
<li><a href="${reverse('dashboard')}">${_("Dashboard")}</a></li>
<li><a href="${reverse('learner_profile', kwargs={'username': user.username})}">${_("Profile")}</a></li>
<li><a href="${reverse('account_settings')}">${_("Account")}</a></li>
</%block>
<li><a href="${reverse('logout')}" role="menuitem">${_("Sign Out")}</a></li>
</ul>
</li>
</ul>
% if should_display_shopping_cart_func(): # see shoppingcart.context_processor.user_has_cart_context_processor
<ul class="user">
<li class="primary">
<a class="shopping-cart" href="${reverse('shoppingcart.views.show_cart')}">
<i class="icon fa fa-shopping-cart" aria-hidden="true"></i> ${_("Shopping Cart")}
</a>
</li>
</ul>
% endif
% else:
<nav aria-label="Account" class="nav-account-management">
<div class="right nav-courseware">
<div class="nav-courseware-01">
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
<a class="cta cta-login" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Sign in")}</a>
% else:
<a class="cta cta-login" href="/login${login_query()}">${_("Sign in")}</a>
% endif
% endif
</div>
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
<div class="nav-courseware-02">
<a class="cta cta-register nav-courseware-button" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register")}</a>
</div>
% else:
<div class="nav-courseware-02">
<a class="cta cta-register nav-courseware-button" href="/register">${_("Register")}</a>
</div>
% endif
% endif
</div>
</nav>
% endif
</div>
</header>
% if course:
<!--[if lte IE 9]>
<div class="ie-banner" aria-hidden="true">${_('<strong>Warning:</strong> Your browser is not fully supported. We strongly recommend using {chrome_link} or {ff_link}.').format(chrome_link='<a href="https://www.google.com/chrome" target="_blank">Chrome</a>', ff_link='<a href="http://www.mozilla.org/firefox" target="_blank">Firefox</a>')}</div>
<![endif]-->
% endif
<%include file="help_modal.html"/>
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