Commit 6af5fc14 by Will Daly

ECOM-1339 Branding API footer

Serve branded footer JSON/HTML/CSS/JS from an API endpoint
in the branding app.  Refactor OpenEdX and EdX.org footer templates
to use the Python version of the API, ensuring that the API
values are consistent with the footer included in main.html.

Detailed changes:

* Added footer API end-point to the branding app.
* Footer API allows the language to be set with querystring parameters.
* Footer API allows showing/hiding of the OpenEdX logo using querystring parameters.
* Deprecate ENABLE_FOOTER_V3 in favor of the branding API configuration flag.
* Move no referrer script into main.html from the edx footer template.
* Rename rwd_header_footer.js to rwd_header.js
* Cache API responses.

Authors:
Awais Qureshi, Aamir Khan, Will Daly
parent 947354fb
......@@ -78,14 +78,13 @@ lms/static/sass/lms-main.scss
lms/static/sass/lms-main-rtl.scss
lms/static/sass/lms-course.scss
lms/static/sass/lms-course-rtl.scss
lms/static/sass/lms-footer-edx.scss
lms/static/sass/lms-footer-edx-rtl.scss
lms/static/sass/lms-footer.scss
lms/static/sass/lms-footer-rtl.scss
cms/static/css/
cms/static/sass/*.css
cms/static/sass/*.css.map
### Logging artifacts
log/
logs
......
/**
* Adds rwd classes and click handlers.
*/
(function($) {
'use strict';
var rwd = (function() {
var _fn = {
header: 'header.global-new',
footer: '.edx-footer-new',
resultsUrl: 'course-search',
init: function() {
_fn.$header = $( _fn.header );
_fn.$footer = $( _fn.footer );
_fn.$nav = _fn.$header.find('nav');
_fn.$globalNav = _fn.$nav.find('.nav-global');
_fn.add.elements();
_fn.add.classes();
_fn.eventHandlers.init();
},
add: {
classes: function() {
// Add any RWD-specific classes
_fn.$header.addClass('rwd');
_fn.$footer.addClass('rwd');
},
elements: function() {
_fn.add.burger();
_fn.add.registerLink();
},
burger: function() {
_fn.$nav.prepend([
'<a href="#" class="mobile-menu-button" aria-label="menu">',
'<i class="icon fa fa-reorder" aria-hidden="true"></i>',
'</a>'
].join(''));
},
registerLink: function() {
var $register = _fn.$nav.find('.cta-register'),
$li = {},
$a = {},
count = 0;
// Add if register link is shown
if ( $register.length > 0 ) {
count = _fn.$globalNav.find('li').length + 1;
// Create new li
$li = $('<li/>');
$li.addClass('desktop-hide nav-global-0' + count);
// Clone register link and remove classes
$a = $register.clone();
$a.removeClass();
// append to DOM
$a.appendTo( $li );
_fn.$globalNav.append( $li );
}
}
},
eventHandlers: {
init: function() {
_fn.eventHandlers.click();
},
click: function() {
// Toggle menu
_fn.$nav.on( 'click', '.mobile-menu-button', _fn.toggleMenu );
}
},
toggleMenu: function( event ) {
event.preventDefault();
_fn.$globalNav.toggleClass('show');
}
};
return {
init: _fn.init
};
})();
setTimeout( function() {
rwd.init();
}, 100);
})(jQuery);
......@@ -10,6 +10,7 @@
var _fn = {
header: 'header.global-new',
// TODO (ECOM-1339): Remove this once the V3 footer is enabled permanently
footer: '.edx-footer-new',
resultsUrl: 'course-search',
......@@ -29,7 +30,7 @@
classes: function() {
// Add any RWD-specific classes
_fn.$header.addClass('rwd');
_fn.$footer.addClass('rwd');
_fn.$footer.addClass('rwd'); // TODO (ECOM-1339): remove once the V3 footer is enabled permanently
},
elements: function() {
......
'''
Django admin pages for Video Branding Configuration.
'''
"""Django admin pages for branding configuration. """
from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin
from .models import BrandingInfoConfig
from .models import BrandingInfoConfig, BrandingApiConfig
admin.site.register(BrandingInfoConfig, ConfigurationModelAdmin)
admin.site.register(BrandingApiConfig, ConfigurationModelAdmin)
"""EdX Branding API
Provides a way to retrieve "branded" parts of the site,
such as the site footer.
This information exposed to:
1) Templates in the LMS.
2) Consumers of the branding API.
This ensures that branded UI elements such as the footer
are consistent across the LMS and other sites (such as
the marketing site and blog).
"""
import logging
import urlparse
from django.conf import settings
from django.utils.translation import ugettext as _
from staticfiles.storage import staticfiles_storage
from microsite_configuration import microsite
from edxmako.shortcuts import marketing_link
from branding.models import BrandingApiConfig
log = logging.getLogger("edx.footer")
def is_enabled():
"""Check whether the branding API is enabled. """
# TODO (ECOM-1339): Remove this comment
# Currently, the branding API configuration controls two things:
# 1) whether we're using the new version of the footer
# 2) whether we're exposing footer information through the API.
#
# Once we've enabled the new footer, the feature flag will control
# only (2), but not (1).
return BrandingApiConfig.current().enabled
def get_footer(is_secure=True):
"""Retrieve information used to render the footer.
This will handle both the OpenEdX and EdX.org versions
of the footer. All user-facing text is internationalized.
Currently, this does NOT support theming.
Keyword Arguments:
is_secure (bool): If True, use https:// in URLs.
Returns: dict
Example:
>>> get_footer()
{
"copyright": "(c) 2015 EdX Inc",
"logo_image": "http://www.example.com/logo.png",
"social_links": [
{
"name": "facebook",
"title": "Facebook",
"url": "http://www.facebook.com/example",
"icon-class": "fa-facebook-square"
},
...
],
"navigation_links": [
{
"name": "about",
"title": "About",
"url": "http://www.example.com/about.html"
},
...
],
"mobile_links": [
{
"name": "apple",
"title": "Apple",
"url": "http://store.apple.com/example_app"
"image": "http://example.com/static/apple_logo.png"
},
...
],
"legal_links": [
{
"url": "http://example.com/terms-of-service.html",
"name": "terms_of_service",
"title': "Terms of Service"
},
# ...
],
"openedx_link": {
"url": "http://open.edx.org",
"title": "Powered by Open edX",
"image": "http://example.com/openedx.png"
}
}
"""
return {
"copyright": _footer_copyright(),
"logo_image": _footer_logo_img(is_secure),
"social_links": _footer_social_links(),
"navigation_links": _footer_navigation_links(),
"mobile_links": _footer_mobile_links(is_secure),
"legal_links": _footer_legal_links(),
"openedx_link": _footer_openedx_link(),
}
def _footer_copyright():
"""Return the copyright to display in the footer.
Returns: unicode
"""
org_name = (
"edX Inc" if settings.FEATURES.get('IS_EDX_DOMAIN', False)
else microsite.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)
)
# Translators: 'EdX', 'edX', and 'Open edX' are trademarks of 'edX Inc.'.
# Please do not translate any of these trademarks and company names.
return _(
u"\u00A9 {org_name}. All rights reserved except where noted. "
u"EdX, Open edX and the edX and Open EdX logos are registered trademarks "
u"or trademarks of edX Inc."
).format(org_name=org_name)
def _footer_openedx_link():
"""Return the image link for "powered by OpenEdX".
Args:
is_secure (bool): Whether the request is using TLS.
Returns: dict
"""
# Translators: 'Open edX' is a brand, please keep this untranslated.
# See http://openedx.org for more information.
title = _("Powered by Open edX")
return {
"url": settings.FOOTER_OPENEDX_URL,
"title": title,
"image": settings.FOOTER_OPENEDX_LOGO_IMAGE,
}
def _footer_social_links():
"""Return the social media links to display in the footer.
Returns: list
"""
links = []
for social_name in settings.SOCIAL_MEDIA_FOOTER_NAMES:
links.append(
{
"name": social_name,
"title": unicode(settings.SOCIAL_MEDIA_FOOTER_DISPLAY.get(social_name, {}).get("title", "")),
"url": settings.SOCIAL_MEDIA_FOOTER_URLS.get(social_name, "#"),
"icon-class": settings.SOCIAL_MEDIA_FOOTER_DISPLAY.get(social_name, {}).get("icon", ""),
}
)
return links
def _footer_navigation_links():
"""Return the navigation links to display in the footer. """
return [
{
"name": link_name,
"title": link_title,
"url": link_url,
}
for link_name, link_url, link_title in [
("about", marketing_link("ABOUT"), _("About")),
("blog", marketing_link("BLOG"), _("Blog")),
("news", marketing_link("NEWS"), _("News")),
("faq", marketing_link("FAQ"), _("FAQs")),
("contact", marketing_link("CONTACT"), _("Contact")),
("jobs", marketing_link("JOBS"), _("Jobs")),
("donate", marketing_link("DONATE"), _("Donate")),
("sitemap", marketing_link("SITE_MAP"), _("Sitemap")),
]
if link_url and link_url != "#"
]
def _footer_legal_links():
"""Return the legal footer links (e.g. terms of service). """
links = [
("terms_of_service_and_honor_code", marketing_link("TOS_AND_HONOR"), _("Terms of Service & Honor Code")),
("privacy_policy", marketing_link("PRIVACY"), _("Privacy Policy")),
("accessibility_policy", marketing_link("ACCESSIBILITY"), _("Accessibility Policy")),
]
# Backwards compatibility: If a combined "terms of service and honor code"
# link isn't provided, add separate TOS and honor code links.
tos_and_honor_link = marketing_link("TOS_AND_HONOR")
if not (tos_and_honor_link and tos_and_honor_link != "#"):
links.extend([
("terms_of_service", marketing_link("TOS"), _("Terms of Service")),
("honor_code", marketing_link("HONOR"), _("Honor Code")),
])
return [
{
"name": link_name,
"title": link_title,
"url": link_url,
}
for link_name, link_url, link_title in links
if link_url and link_url != "#"
]
def _footer_mobile_links(is_secure):
"""Return the mobile app store links.
Args:
is_secure (bool): Whether the request is using TLS.
Returns: list
"""
mobile_links = []
if settings.FEATURES.get('ENABLE_FOOTER_MOBILE_APP_LINKS'):
mobile_links = [
{
"name": "apple",
"title": "Apple",
"url": settings.MOBILE_STORE_URLS.get('apple', '#'),
"image": _absolute_url_staticfile(is_secure, 'images/app/app_store_badge_135x40.svg')
},
{
"name": "google",
"title": "Google",
"url": settings.MOBILE_STORE_URLS.get('google', '#'),
"image": _absolute_url_staticfile(is_secure, 'images/app/google_play_badge_45.png')
}
]
return mobile_links
def _footer_logo_img(is_secure):
"""Return the logo used for footer about link
Args:
is_secure (bool): Whether the request is using TLS.
Returns:
Absolute url to logo
"""
logo_name = microsite.get_value('FOOTER_ORGANIZATION_IMAGE', settings.FOOTER_ORGANIZATION_IMAGE)
return _absolute_url_staticfile(is_secure, logo_name)
def _absolute_url(is_secure, url_path):
"""Construct an absolute URL back to the site.
Arguments:
is_secure (bool): If true, use HTTPS as the protocol.
url_path (unicode): The path of the URL.
Returns:
unicode
"""
site_name = microsite.get_value('SITE_NAME', settings.SITE_NAME)
parts = ("https" if is_secure else "http", site_name, url_path, '', '', '')
return urlparse.urlunparse(parts)
def _absolute_url_staticfile(is_secure, name):
"""Construct an absolute URL back to a static resource on the site.
Arguments:
is_secure (bool): If true, use HTTPS as the protocol.
name (unicode): The name of the static resource to retrieve.
Returns:
unicode
"""
url_path = staticfiles_storage.url(name)
return _absolute_url(is_secure, url_path)
"""
Branding API endpoint urls.
"""
from django.conf.urls import patterns, url
urlpatterns = patterns(
"",
url(
r"^footer$",
"branding.views.footer",
name="branding_footer",
),
)
"""Context processors for Django templates. """
from branding import api as branding_api
# TODO (ECOM-1339): Remove this module once we permanently enable the V3 footer.
def branding_context_processor(request): # pylint: disable=unused-argument
"""Add the feature flag to Django template context. """
return {
"ENABLE_BRANDING_API": branding_api.is_enabled()
}
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'BrandingApiConfig'
db.create_table('branding_brandingapiconfig', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)),
('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
))
db.send_create_signal('branding', ['BrandingApiConfig'])
def backwards(self, orm):
# Deleting model 'BrandingApiConfig'
db.delete_table('branding_brandingapiconfig')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'branding.brandingapiconfig': {
'Meta': {'object_name': 'BrandingApiConfig'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'branding.brandinginfoconfig': {
'Meta': {'object_name': 'BrandingInfoConfig'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'configuration': ('django.db.models.fields.TextField', [], {}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['branding']
......@@ -44,3 +44,14 @@ class BrandingInfoConfig(ConfigurationModel):
"""
info = cls.current()
return json.loads(info.configuration) if info.enabled else {}
class BrandingApiConfig(ConfigurationModel):
"""Configure Branding api's
Enable or disable api's functionality.
When this flag is disabled, the api will return 404.
When the flag is enabled, the api will returns the valid reponse.
"""
pass
# encoding: utf-8
"""Tests of Branding API views. """
import contextlib
import json
import urllib
from django.test import TestCase
from django.core.urlresolvers import reverse
from django.conf import settings
import mock
import ddt
from config_models.models import cache
from branding.models import BrandingApiConfig
@ddt.ddt
class TestFooter(TestCase):
"""Test API end-point for retrieving the footer. """
def setUp(self):
"""Clear the configuration cache. """
super(TestFooter, self).setUp()
cache.clear()
@ddt.data("*/*", "text/html", "application/json")
def test_feature_flag(self, accepts):
self._set_feature_flag(False)
resp = self._get_footer(accepts=accepts)
self.assertEqual(resp.status_code, 404)
@ddt.data(
# Open source version
(False, "application/json", "application/json; charset=utf-8", "Open edX"),
(False, "text/html", "text/html; charset=utf-8", "lms-footer.css"),
(False, "text/html", "text/html; charset=utf-8", "Open edX"),
# EdX.org version
(True, "application/json", "application/json; charset=utf-8", "edX Inc"),
(True, "text/html", "text/html; charset=utf-8", "lms-footer-edx.css"),
(True, "text/html", "text/html; charset=utf-8", "edX Inc"),
)
@ddt.unpack
def test_footer_content_types(self, is_edx_domain, accepts, content_type, content):
self._set_feature_flag(True)
with self._set_is_edx_domain(is_edx_domain):
resp = self._get_footer(accepts=accepts)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp["Content-Type"], content_type)
self.assertIn(content, resp.content)
@mock.patch.dict(settings.FEATURES, {'ENABLE_FOOTER_MOBILE_APP_LINKS': True})
@ddt.data(True, False)
def test_footer_json(self, is_edx_domain):
self._set_feature_flag(True)
with self._set_is_edx_domain(is_edx_domain):
resp = self._get_footer()
self.assertEqual(resp.status_code, 200)
json_data = json.loads(resp.content)
self.assertTrue(isinstance(json_data, dict))
# Logo
self.assertIn("logo_image", json_data)
# Links
self.assertIn("navigation_links", json_data)
for link in json_data["navigation_links"]:
self.assertIn("name", link)
self.assertIn("title", link)
self.assertIn("url", link)
# Social links
self.assertIn("social_links", json_data)
for link in json_data["social_links"]:
self.assertIn("name", link)
self.assertIn("title", link)
self.assertIn("url", link)
self.assertIn("icon-class", link)
# Mobile links
self.assertIn("mobile_links", json_data)
for link in json_data["mobile_links"]:
self.assertIn("name", link)
self.assertIn("title", link)
self.assertIn("url", link)
self.assertIn("image", link)
# Legal links
self.assertIn("legal_links", json_data)
for link in json_data["legal_links"]:
self.assertIn("name", link)
self.assertIn("title", link)
self.assertIn("url", link)
# OpenEdX
self.assertIn("openedx_link", json_data)
self.assertIn("url", json_data["openedx_link"])
self.assertIn("title", json_data["openedx_link"])
self.assertIn("image", json_data["openedx_link"])
# Copyright
self.assertIn("copyright", json_data)
@ddt.data(
("en", "registered trademarks"),
("eo", u"régïstéréd trädémärks"), # Dummy language string
("unknown", "registered trademarks"), # default to English
)
@ddt.unpack
def test_language_override_translation(self, language, expected_copyright):
self._set_feature_flag(True)
# Load the footer with the specified language
resp = self._get_footer(params={'language': language})
self.assertEqual(resp.status_code, 200)
json_data = json.loads(resp.content)
# Verify that the translation occurred
self.assertIn(expected_copyright, json_data['copyright'])
@ddt.data(
# OpenEdX
(False, "en", "lms-footer.css"),
(False, "ar", "lms-footer-rtl.css"),
# EdX.org
(True, "en", "lms-footer-edx.css"),
(True, "ar", "lms-footer-edx-rtl.css"),
)
@ddt.unpack
def test_language_rtl(self, is_edx_domain, language, static_path):
self._set_feature_flag(True)
with self._set_is_edx_domain(is_edx_domain):
resp = self._get_footer(accepts="text/html", params={'language': language})
self.assertEqual(resp.status_code, 200)
self.assertIn(static_path, resp.content)
@ddt.data(
# OpenEdX
(False, True),
(False, False),
# EdX.org
(True, True),
(True, False),
)
@ddt.unpack
def test_show_openedx_logo(self, is_edx_domain, show_logo):
self._set_feature_flag(True)
with self._set_is_edx_domain(is_edx_domain):
params = {'show-openedx-logo': 1} if show_logo else {}
resp = self._get_footer(accepts="text/html", params=params)
self.assertEqual(resp.status_code, 200)
if show_logo:
self.assertIn(settings.FOOTER_OPENEDX_URL, resp.content)
else:
self.assertNotIn(settings.FOOTER_OPENEDX_URL, resp.content)
@ddt.data(
# OpenEdX
(False, False),
(False, True),
# EdX.org
(True, False),
(True, True),
)
@ddt.unpack
def test_include_dependencies(self, is_edx_domain, include_dependencies):
self._set_feature_flag(True)
with self._set_is_edx_domain(is_edx_domain):
params = {'include-dependencies': 1} if include_dependencies else {}
resp = self._get_footer(accepts="text/html", params=params)
self.assertEqual(resp.status_code, 200)
if include_dependencies:
self.assertIn("vendor", resp.content)
else:
self.assertNotIn("vendor", resp.content)
def test_no_supported_accept_type(self):
self._set_feature_flag(True)
resp = self._get_footer(accepts="application/x-shockwave-flash")
self.assertEqual(resp.status_code, 406)
def _set_feature_flag(self, enabled):
"""Enable or disable the feature flag for the branding API end-points. """
config = BrandingApiConfig(enabled=enabled)
config.save()
def _get_footer(self, accepts="application/json", params=None):
"""Retrieve the footer. """
url = reverse("branding_footer")
if params is not None:
url = u"{url}?{params}".format(
url=url,
params=urllib.urlencode(params)
)
return self.client.get(url, HTTP_ACCEPT=accepts)
@contextlib.contextmanager
def _set_is_edx_domain(self, is_edx_domain):
"""Configure whether this an EdX-controlled domain. """
with mock.patch.dict(settings.FEATURES, {'IS_EDX_DOMAIN': is_edx_domain}):
yield
"""Views for the branding app. """
import logging
import urllib
from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import Http404
from django.core.cache import cache
from django.views.decorators.cache import cache_control
from django.http import HttpResponse, Http404
from django.utils import translation
from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie
from staticfiles.storage import staticfiles_storage
from edxmako.shortcuts import render_to_response
import student.views
from student.models import CourseEnrollment
import courseware.views
from microsite_configuration import microsite
from edxmako.shortcuts import marketing_link
from util.cache import cache_if_anonymous
from util.json_request import JsonResponse
import branding.api as branding_api
log = logging.getLogger(__name__)
def get_course_enrollments(user):
......@@ -102,3 +114,198 @@ def courses(request):
# we do not expect this case to be reached in cases where
# marketing is enabled or the courses are not browsable
return courseware.views.courses(request)
def _footer_static_url(request, name):
"""Construct an absolute URL to a static asset. """
return request.build_absolute_uri(staticfiles_storage.url(name))
def _footer_css_urls(request, package_name):
"""Construct absolute URLs to CSS assets in a package. """
# We need this to work both in local development and in production.
# Unfortunately, in local development we don't run the full asset pipeline,
# so fully processed output files may not exist.
# For this reason, we use the *css package* name(s), rather than the static file name
# to identify the CSS file name(s) to include in the footer.
# We then construct an absolute URI so that external sites (such as the marketing site)
# can locate the assets.
package = settings.PIPELINE_CSS.get(package_name, {})
paths = [package['output_filename']] if not settings.DEBUG else package['source_filenames']
return [
_footer_static_url(request, path)
for path in paths
]
def _render_footer_html(request, show_openedx_logo, include_dependencies):
"""Render the footer as HTML.
Arguments:
show_openedx_logo (bool): If True, include the OpenEdX logo in the rendered HTML.
include_dependencies (bool): If True, include JavaScript and CSS dependencies.
Returns: unicode
"""
bidi = 'rtl' if translation.get_language_bidi() else 'ltr'
version = 'edx' if settings.FEATURES.get('IS_EDX_DOMAIN') else 'openedx'
css_name = settings.FOOTER_CSS[version][bidi]
context = {
'hide_openedx_link': not show_openedx_logo,
'footer_js_url': _footer_static_url(request, 'js/footer-edx.js'),
'footer_css_urls': _footer_css_urls(request, css_name),
'bidi': bidi,
'include_dependencies': include_dependencies,
}
return (
render_to_response("footer-edx-v3.html", context)
if settings.FEATURES.get("IS_EDX_DOMAIN", False)
else render_to_response("footer.html", context)
)
@cache_control(must_revalidate=True, max_age=settings.FOOTER_BROWSER_CACHE_MAX_AGE)
def footer(request):
"""Retrieve the branded footer.
This end-point provides information about the site footer,
allowing for consistent display of the footer across other sites
(for example, on the marketing site and blog).
It can be used in one of two ways:
1) A client renders the footer from a JSON description.
2) A browser loads an HTML representation of the footer
and injects it into the DOM. The HTML includes
CSS and JavaScript links.
In case (2), we assume that the following dependencies
are included on the page:
a) JQuery (same version as used in edx-platform)
b) font-awesome (same version as used in edx-platform)
c) Open Sans web fonts
Example: Retrieving the footer as JSON
GET /api/branding/v1/footer
Accepts: application/json
{
"navigation_links": [
{
"url": "http://example.com/about",
"name": "about",
"title": "About"
},
# ...
],
"social_links": [
{
"url": "http://example.com/social",
"name": "facebook",
"icon-class": "fa-facebook-square",
"title": "Facebook"
},
# ...
],
"mobile_links": [
{
"url": "http://example.com/android",
"name": "google",
"image": "http://example.com/google.png",
"title": "Google"
},
# ...
],
"legal_links": [
{
"url": "http://example.com/terms-of-service.html",
"name": "terms_of_service",
"title': "Terms of Service"
},
# ...
],
"openedx_link": {
"url": "http://open.edx.org",
"title": "Powered by Open edX",
"image": "http://example.com/openedx.png"
},
"logo_image": "http://example.com/static/images/default-theme/logo.png",
"copyright": "EdX, Open edX, and the edX and Open edX logos are \
registered trademarks or trademarks of edX Inc."
}
Example: Retrieving the footer as HTML
GET /api/branding/v1/footer
Accepts: text/html
Example: Including the footer with the "Powered by OpenEdX" logo
GET /api/branding/v1/footer?show-openedx-logo=1
Accepts: text/html
Example: Retrieving the footer in a particular language
GET /api/branding/v1/footer?language=en
Accepts: text/html
Example: Retrieving the footer with all JS and CSS dependencies (for testing)
GET /api/branding/v1/footer?include-dependencies=1
Accepts: text/html
"""
if not branding_api.is_enabled():
raise Http404
# Use the content type to decide what representation to serve
accepts = request.META.get('HTTP_ACCEPT', '*/*')
# Show the OpenEdX logo in the footer
show_openedx_logo = bool(request.GET.get('show-openedx-logo', False))
# Include JS and CSS dependencies
# This is useful for testing the end-point directly.
include_dependencies = bool(request.GET.get('include-dependencies', False))
# Override the language if necessary
language = request.GET.get('language', translation.get_language())
# Render the footer information based on the extension
if 'text/html' in accepts or '*/*' in accepts:
cache_key = u"branding.footer.{params}.html".format(
params=urllib.urlencode({
'language': language,
'show_openedx_logo': show_openedx_logo,
'include_dependencies': include_dependencies,
})
)
content = cache.get(cache_key)
if content is None:
with translation.override(language):
content = _render_footer_html(request, show_openedx_logo, include_dependencies)
cache.set(cache_key, content, settings.FOOTER_CACHE_TIMEOUT)
return HttpResponse(content, status=200, content_type="text/html; charset=utf-8")
elif 'application/json' in accepts:
cache_key = u"branding.footer.{params}.json".format(
params=urllib.urlencode({
'language': language,
'is_secure': request.is_secure(),
})
)
footer_dict = cache.get(cache_key)
if footer_dict is None:
with translation.override(language):
footer_dict = branding_api.get_footer(is_secure=request.is_secure())
cache.set(cache_key, footer_dict, settings.FOOTER_CACHE_TIMEOUT)
return JsonResponse(footer_dict, 200, content_type="application/json; charset=utf-8")
else:
return HttpResponse(status=406)
......@@ -18,6 +18,6 @@ Feature: LMS.Homepage for web users
| id | Link |
| about | About |
| jobs | Jobs |
| faq | FAQ |
| faq | FAQs |
| contact | Contact|
| news | News |
......@@ -306,7 +306,7 @@ class AboutWithInvitationOnly(ModuleStoreTestCase):
url = reverse('about_course', args=[self.course.id.to_deprecated_string()])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn(u"Register for {}".format(self.course.id.course), resp.content)
self.assertIn(u"Register for {}".format(self.course.id.course), resp.content.decode('utf-8'))
# Check that registration button is present
self.assertIn(REG_STR, resp.content)
......@@ -336,7 +336,7 @@ class AboutTestCaseShibCourse(LoginEnrollmentTestCase, ModuleStoreTestCase):
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("OOGIE BLOOGIE", resp.content)
self.assertIn(u"Register for {}".format(self.course.id.course), resp.content)
self.assertIn(u"Register for {}".format(self.course.id.course), resp.content.decode('utf-8'))
self.assertIn(SHIB_ERROR_STR, resp.content)
self.assertIn(REG_STR, resp.content)
......@@ -348,7 +348,7 @@ class AboutTestCaseShibCourse(LoginEnrollmentTestCase, ModuleStoreTestCase):
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn("OOGIE BLOOGIE", resp.content)
self.assertIn(u"Register for {}".format(self.course.id.course), resp.content)
self.assertIn(u"Register for {}".format(self.course.id.course), resp.content.decode('utf-8'))
self.assertIn(SHIB_ERROR_STR, resp.content)
self.assertIn(REG_STR, resp.content)
......
......@@ -14,6 +14,17 @@ from django.test.utils import override_settings
@attr('shard_1')
class TestFooter(TestCase):
SOCIAL_MEDIA_NAMES = [
"facebook",
"google_plus",
"twitter",
"linkedin",
"tumblr",
"meetup",
"reddit",
"youtube",
]
SOCIAL_MEDIA_URLS = {
"facebook": "http://www.facebook.com/",
"google_plus": "https://plus.google.com/",
......@@ -51,7 +62,10 @@ class TestFooter(TestCase):
self.assertContains(resp, 'wrapper-footer')
@patch.dict(settings.FEATURES, {'IS_EDX_DOMAIN': True})
@override_settings(SOCIAL_MEDIA_FOOTER_URLS=SOCIAL_MEDIA_URLS)
@override_settings(
SOCIAL_MEDIA_FOOTER_NAMES=SOCIAL_MEDIA_NAMES,
SOCIAL_MEDIA_FOOTER_URLS=SOCIAL_MEDIA_URLS
)
def test_edx_footer_social_links(self):
resp = self.client.get('/')
for name, url in self.SOCIAL_MEDIA_URLS.iteritems():
......
......@@ -309,6 +309,13 @@ if FEATURES.get('AUTH_USE_CAS'):
# Example: {'CN': 'http://api.xuetangx.com/edx/video?s3_url='}
VIDEO_CDN_URL = ENV_TOKENS.get('VIDEO_CDN_URL', {})
# Branded footer
FOOTER_OPENEDX_URL = ENV_TOKENS.get('FOOTER_OPENEDX_URL', FOOTER_OPENEDX_URL)
FOOTER_OPENEDX_LOGO_IMAGE = ENV_TOKENS.get('FOOTER_OPENEDX_LOGO_IMAGE', FOOTER_OPENEDX_LOGO_IMAGE)
FOOTER_ORGANIZATION_IMAGE = ENV_TOKENS.get('FOOTER_ORGANIZATION_IMAGE', FOOTER_ORGANIZATION_IMAGE)
FOOTER_CACHE_TIMEOUT = ENV_TOKENS.get('FOOTER_CACHE_TIMEOUT', FOOTER_CACHE_TIMEOUT)
FOOTER_BROWSER_CACHE_MAX_AGE = ENV_TOKENS.get('FOOTER_BROWSER_CACHE_MAX_AGE', FOOTER_BROWSER_CACHE_MAX_AGE)
############# CORS headers for cross-domain requests #################
if FEATURES.get('ENABLE_CORS_HEADERS') or FEATURES.get('ENABLE_CROSS_DOMAIN_CSRF_COOKIE'):
......
......@@ -342,9 +342,6 @@ FEATURES = {
# Show the mobile app links in the footer
'ENABLE_FOOTER_MOBILE_APP_LINKS': False,
# Use version 3 of the footer (added May 2015)
'ENABLE_FOOTER_V3': False,
# Let students save and manage their annotations
'ENABLE_EDXNOTES': False,
......@@ -507,6 +504,11 @@ TEMPLATE_CONTEXT_PROCESSORS = (
# Allows the open edX footer to be leveraged in Django Templates.
'edxmako.shortcuts.open_source_footer_context_processor',
# TODO (ECOM-1339): Remove once the V3 version of the footer is enabled permanently
# This allows us to pass the appropriate feature flag to the main Django template
# that contains the footer.
'branding.context_processors.branding_context_processor',
# Shoppingcart processor (detects if request.user has a cart)
'shoppingcart.context_processor.user_has_cart_context_processor',
......@@ -1039,6 +1041,48 @@ PARENTAL_CONSENT_AGE_LIMIT = 13
################################# Jasmine ##################################
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
######################### Branded Footer ###################################
# Constants for the footer used on the site and shared with other sites
# (such as marketing and the blog) via the branding API.
# URL for OpenEdX displayed in the footer
FOOTER_OPENEDX_URL = "http://open.edx.org"
# URL for the OpenEdX logo image
# We use logo images served from files.edx.org so we can (roughly) track
# how many OpenEdX installations are running.
# Site operators can choose from these logo options:
# * https://files.edx.org/openedx-logos/edx-openedx-logo-tag.png
# * https://files.edx.org/openedx-logos/edx-openedx-logo-tag-light.png"
# * https://files.edx.org/openedx-logos/edx-openedx-logo-tag-dark.png
FOOTER_OPENEDX_LOGO_IMAGE = "https://files.edx.org/openedx-logos/edx-openedx-logo-tag.png"
# This is just a placeholder image.
# Site operators can customize this with their organization's image.
FOOTER_ORGANIZATION_IMAGE = "images/default-theme/logo.png"
# These are referred to both by the Django asset pipeline
# AND by the branding footer API, which needs to decide which
# version of the CSS to serve.
FOOTER_CSS = {
"openedx": {
"ltr": "style-lms-footer",
"rtl": "style-lms-footer-rtl",
},
"edx": {
"ltr": "style-lms-footer-edx",
"rtl": "style-lms-footer-edx-rtl",
},
}
# Cache expiration for the version of the footer served
# by the branding API.
FOOTER_CACHE_TIMEOUT = 30 * 60
# Max age cache control header for the footer (controls browser caching).
FOOTER_BROWSER_CACHE_MAX_AGE = 5 * 60
################################# Deprecation warnings #####################
# Ignore deprecation warnings (so we don't clutter Jenkins builds/production)
......@@ -1182,8 +1226,7 @@ dashboard_js = (
['js/search/dashboard/main.js']
)
discussion_js = sorted(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/discussion/**/*.js'))
rwd_header_footer_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/common_helpers/rwd_header_footer.js'))
footer_edx_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/footer-edx.js'))
rwd_header_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/utils/rwd_header.js'))
staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.js'))
open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/open_ended/**/*.js'))
notes_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/notes/**/*.js'))
......@@ -1196,7 +1239,7 @@ instructor_dash_js = (
# These are not courseware, so they do not need many of the courseware-specific
# JavaScript modules.
student_account_js = [
'js/utils/rwd_header_footer.js',
'js/utils/rwd_header.js',
'js/utils/edx.utils.validate.js',
'js/form.ext.js',
'js/my_courses_dropdown.js',
......@@ -1342,17 +1385,29 @@ PIPELINE_CSS = {
],
'output_filename': 'css/lms-style-xmodule-annotations.css',
},
'style-edx-footer': {
FOOTER_CSS['openedx']['ltr']: {
'source_filenames': [
'sass/footer-v3.css',
'sass/lms-footer.css',
],
'output_filename': 'css/lms-footer-edx.css',
'output_filename': 'css/lms-footer.css',
},
'style-edx-footer-rtl': {
FOOTER_CSS['openedx']['rtl']: {
'source_filenames': [
'sass/footer-v3-rtl.css',
'sass/lms-footer-rtl.css',
],
'output_filename': 'css/lms-footer-edx-rtl.css',
'output_filename': 'css/lms-footer-rtl.css'
},
FOOTER_CSS['edx']['ltr']: {
'source_filenames': [
'sass/lms-footer-edx.css',
],
'output_filename': 'css/lms-footer-edx.css'
},
FOOTER_CSS['edx']['rtl']: {
'source_filenames': [
'sass/lms-footer-edx-rtl.css',
],
'output_filename': 'css/lms-footer-edx-rtl.css'
},
}
......@@ -1423,9 +1478,9 @@ PIPELINE_JS = {
'source_filenames': dashboard_js,
'output_filename': 'js/dashboard.js'
},
'rwd_header_footer': {
'source_filenames': rwd_header_footer_js,
'output_filename': 'js/rwd_header_footer.js'
'rwd_header': {
'source_filenames': rwd_header_js,
'output_filename': 'js/rwd_header.js'
},
'student_account': {
'source_filenames': student_account_js,
......@@ -1448,8 +1503,8 @@ PIPELINE_JS = {
'output_filename': 'js/ccx.js'
},
'footer_edx': {
'source_filenames': footer_edx_js,
'output_filename': 'js/footer_edx.js'
'source_filenames': ['js/footer-edx.js'],
'output_filename': 'js/footer-edx.js',
}
}
......@@ -1492,6 +1547,7 @@ PIPELINE_UGLIFYJS_BINARY = 'node_modules/.bin/uglifyjs'
# Setting that will only affect the edX version of django-pipeline until our changes are merged upstream
PIPELINE_COMPILE_INPLACE = True
################################# CELERY ######################################
# Message configuration
......@@ -1797,25 +1853,14 @@ MKTG_URL_LINK_MAP = {
################# Social Media Footer Links #######################
# The names list controls the order of social media
# links in the footer.
if FEATURES.get('ENABLE_FOOTER_V3'):
SOCIAL_MEDIA_FOOTER_NAMES = [
"facebook",
"twitter",
"linkedin",
"weibo",
"vk",
]
else:
SOCIAL_MEDIA_FOOTER_NAMES = [
"facebook",
"twitter",
"linkedin",
"google_plus",
"tumblr",
"meetup",
"reddit",
"youtube",
]
SOCIAL_MEDIA_FOOTER_NAMES = [
"facebook",
"twitter",
"youtube",
"linkedin",
"google_plus",
"reddit",
]
# The footer URLs dictionary maps social footer names
# to URLs defined in configuration.
......@@ -1852,7 +1897,7 @@ SOCIAL_MEDIA_FOOTER_DISPLAY = {
# Translators: This is the website name of www.tumblr.com. Please
# translate this the way that Tumblr advertises in your language.
"title": _("Tumblr"),
"icon": "fa-tumblr-square"
"icon": "fa-tumblr"
},
"meetup": {
# Translators: This is the website name of www.meetup.com. Please
......@@ -1864,7 +1909,7 @@ SOCIAL_MEDIA_FOOTER_DISPLAY = {
# Translators: This is the website name of www.reddit.com. Please
# translate this the way that Reddit advertises in your language.
"title": _("Reddit"),
"icon": "fa-reddit-square"
"icon": "fa-reddit"
},
"vk": {
# Translators: This is the website name of https://vk.com. Please
......@@ -1882,7 +1927,7 @@ SOCIAL_MEDIA_FOOTER_DISPLAY = {
# Translators: This is the website name of www.youtube.com. Please
# translate this the way that YouTube advertises in your language.
"title": _("Youtube"),
"icon": "fa-youtube-square"
"icon": "fa-youtube"
}
}
......
......@@ -7,18 +7,6 @@ var edx = edx || {};
var _fn = {
el: '#footer-edx-v3',
init: function() {
_fn.$el = _fn.$el || $( _fn.el );
/**
* Only continue if the expected element
* to add footer to is in the DOM
*/
if ( _fn.$el.length > -1 ) {
_fn.footer.get();
}
},
analytics: {
init: function() {
_fn.$el = _fn.$el || $( _fn.el );
......@@ -27,7 +15,7 @@ var edx = edx || {};
* Only continue if the expected element
* to add footer to is in the DOM
*/
if ( _fn.$el.length > -1 ) {
if ( _fn.$el.length ) {
_fn.analytics.eventListener();
}
},
......@@ -50,31 +38,12 @@ var edx = edx || {};
}
}
},
footer: {
get: function() {
$.ajax({
url: 'https://courses.edx.org/api/v1/branding/footer',
type: 'GET',
dataType: 'html',
success: function( data ) {
_fn.footer.render( data );
}
});
},
render: function( html ) {
_fn.$el.html( html );
}
}
};
return {
load: _fn.init,
analytics: _fn.analytics.init
};
})();
// Initialize the analytics events
edx.footer.analytics();
})(jQuery);
// ----------------------------------------
// LMS edx.org Footer: Shared Build Compile
// base - utilities
@import 'base/variables';
@import 'base/mixins';
footer#footer-edx-v3 {
@import 'base/extends';
// base - starter
@import 'base/base';
}
// base - elements
@import 'elements/typography';
// shared - platform
@import 'shared/footer-edx';
......@@ -21,7 +21,7 @@
@import 'shared/fields';
@import 'shared/forms';
@import 'shared/footer';
@import 'shared/footer-edx'; // Replaces most of the footer partial. Will update footer to remove edx specific styles once feature flag removed.
@import 'shared/footer-edx';
@import 'shared/header';
@import 'shared/course_object';
@import 'shared/course_filter';
......
// Variables for LMS (left-to-right)
// ==================================
// Neat
// ==================================
// Sets the default layout direction of the grid.
$default-layout-direction: LTR !global;
// Variables for LMS (right-to-left)
// ==================================
// Neat
// ==================================
// Sets the default layout direction of the grid.
$default-layout-direction: RTL !global;
// Footer for edx.org (right-to-left)
// ==================================
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-rtl'; // set the layout for right to left languages
@import 'base/variables-rtl';
// Import shared build for the edx.org footer
@import 'build-footer-edx'
// Footer for edx.org (left-to-right)
// ==================================
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages
@import 'base/variables-ltr';
// Import shared build for the edx.org footer
@import 'build-footer-edx'
## Note: This Sass infrastructure is repeated in application-extend1 and application-extend2, but needed in order to address an IE9 rule limit within CSS - http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/10164546.aspx
// lms - css application architecture
// ====================
// Footer for OpenEdX (right-to-left)
// ==================================
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-rtl'; // set the layout for left to right languages
// BASE *default edX offerings*
// ====================
@import 'vendor/bi-app/bi-app-rtl'; // set the layout for right to left languages
// base - utilities
@import 'base/variables';
......@@ -27,19 +22,14 @@
@import '${env.get('THEME_NAME')}';
% endif
// base - assets
@import 'base/font_face';
footer#footer-edx-v3 {
footer#footer-openedx {
@import 'base/reset';
@import 'base/extends';
// base - starter
@import 'base/base';
}
// base - elements
@import 'elements/typography';
// shared - platform
@import 'shared/footer-edx';
@import 'shared/footer';
## Note: This Sass infrastructure is repeated in application-extend1 and application-extend2, but needed in order to address an IE9 rule limit within CSS - http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/10164546.aspx
// lms - css application architecture
// ====================
// Footer for OpenEdX (left-to-right)
// ==================================
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages
// BASE *default edX offerings*
// ====================
// base - utilities
@import 'base/variables';
@import 'base/mixins';
......@@ -27,19 +22,14 @@
@import '${env.get('THEME_NAME')}';
% endif
// base - assets
@import 'base/font_face';
footer#footer-edx-v3 {
footer#footer-openedx {
@import 'base/reset';
@import 'base/extends';
// base - starter
@import 'base/base';
}
// base - elements
@import 'elements/typography';
// shared - platform
@import 'shared/footer-edx';
@import 'shared/footer';
......@@ -4,6 +4,7 @@
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-rtl'; // set the layout for right to left languages
@import 'base/variables-rtl';
// BASE *default edX offerings*
// ====================
......
......@@ -4,6 +4,7 @@
// libs and resets *do not edit*
@import 'bourbon/bourbon'; // lib - bourbon
@import 'vendor/bi-app/bi-app-ltr'; // set the layout for left to right languages
@import 'base/variables-ltr';
// BASE *default edX offerings*
// ====================
......
......@@ -80,7 +80,7 @@ footer#footer-edx-v3 {
margin-bottom: 30px;
}
.sm-link {
a.sm-link {
@include float(left);
@include margin(0, 0, 10px, 12px);
@include font-size(28);
......
......@@ -10,7 +10,7 @@
background: $footer-bg;
clear: both;
footer {
footer#footer-openedx {
@include clearfix();
@include box-sizing(border-box);
max-width: grid-width(12);
......@@ -286,6 +286,8 @@ $edx-footer-bg-color: rgb(252,252,252);
}
}
// TODO (ECOM-1339): Remove the "new" (v2) footer once the v3 footer
// is permanently enabled.
.edx-footer-new {
background: $edx-footer-bg-color;
......
......@@ -17,7 +17,7 @@ ${_("Receipt")}
</%block>
<%block name="js_extra">
<%static:js group='rwd_header_footer'/>
<%static:js group='rwd_header'/>
<script src="${static.url('js/vendor/jquery.ajax-retry.js')}"></script>
<script src="${static.url('js/vendor/underscore-min.js')}"></script>
<script src="${static.url('js/vendor/underscore.string.min.js')}"></script>
......
## mako
## TODO (ECOM-1339): Delete this template once the V3 footer is enabled
<%! from django.core.urlresolvers import reverse %>
<%! from django.utils.translation import ugettext as _ %>
<%namespace name='static' file='static_content.html'/>
......@@ -50,7 +51,7 @@
</div>
<div class="footer-about-links">
<a href="${marketing_link('TOS')}"><span class="copy">${_("Terms of Service and Honor Code")}</span></a>
<a href="${marketing_link('TOS_AND_HONOR')}"><span class="copy">${_("Terms of Service and Honor Code")}</span></a>
<a href="${marketing_link('PRIVACY')}"><span class="copy">${_("Privacy Policy")}</span>
<span class="note">
## Translators: {date} will be an abbreviated date, indicating when the privacy policy was most recently revised.
......
## mako
<%! from django.core.urlresolvers import reverse %>
<%! from django.utils.translation import ugettext as _ %>
<%!
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")}">
<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-logo">
<img alt="edX logo" src="${static.url('images/edx-theme/edx-header-logo.png')}">
<img alt="edX logo" src="${footer['logo_image']}">
</div>
<div class="site-details">
<nav class="site-nav" aria-label="${_("About edX")}">
<a href="${marketing_link('ABOUT')}">${_("About")}</a>
<a href="${marketing_link('BLOG')}">${_("Blog")}</a>
<a href="${marketing_link('NEWS')}">${_("News")}</a>
<a href="${marketing_link('FAQ')}">${_("FAQs")}</a>
<a href="${marketing_link('CONTACT')}">${_("Contact")}</a>
<a href="${marketing_link('JOBS')}">${_("Jobs")}</a>
<a href="${marketing_link('DONATE')}">${_("Donate")}</a>
<a href="${marketing_link('DONATE')}">${_("Sitemap")}</a>
% for link in footer["navigation_links"]:
<a href="${link['url']}">${link['title']}</a>
% endfor
</nav>
<nav class="legal-notices" aria-label="${_("Legal")}">
<a href="${marketing_link('TOS')}">${_("Terms of Service &amp; Honor Code")}</a>
<a href="${marketing_link('PRIVACY')}">${_("Privacy Policy")}</a>
<a href="${marketing_link('PRIVACY')}">${_("Website Accessibility Policy")}</a>
% for link in footer["legal_links"]:
<a href="${link['url']}">${link['title']}</a>
% endfor
</nav>
<p class="copyright">
## Using "edX Inc." explicitly here for copyright purposes (settings.PLATFORM_NAME is just "edX", and this footer is only used on edx.org)
## Site operators: Please do not remove this paragraph! This attributes back to edX and makes your acknowledgement of edX's trademarks clear.
<p>&copy; ${settings.COPYRIGHT_YEAR} edX Inc. All rights reserved except where noted. EdX, Open edX and the edX and Open edX logos are registered trademarks of trademarks of edX Inc.</p>
</p>
<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="http://open.edx.org" title="${_("Powered by Open edX")}">
<img alt="${_("Powered by Open edX")}" src="https://files.edx.org/openedx-logos/edx-openedx-logo-tag.png">
<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">
## Translators: This is the website name of www.facebook.com. Please
## translate this the way that Facebook advertises in your language.
<a href="${settings.SOCIAL_MEDIA_FOOTER_URLS.get('facebook', '#')}" class="sm-link external" title="${_("Facebook")}" rel="noreferrer">
<span class="icon fa fa-facebook-square element-invisible"></span>
</a>
## Translators: This is the website name of www.twitter.com. Please
## translate this the way that Twitter advertises in your language.
<a href="${settings.SOCIAL_MEDIA_FOOTER_URLS.get('twitter', '#')}" class="sm-link external" title="${_("Twitter")}" rel="noreferrer">
<span class="icon fa fa-twitter element-invisible"></span>
</a>
## Translators: This is the website name of www.linked.com. Please
## translate this the way that LinkedIn advertises in your language.
<a href="${settings.SOCIAL_MEDIA_FOOTER_URLS.get('linkedin', '#')}" class="sm-link external" title="${_("LinkedIn")}" rel="noreferrer">
<span class="icon fa fa-linkedin-square element-invisible"></span>
</a>
<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>
</a>
% endfor
</div>
## Translators: This is the website name of plus.google.com. Please
## translate this the way that Google+ advertises in your language.
<a href="${settings.SOCIAL_MEDIA_FOOTER_URLS.get('facebook', '#')}" class="sm-link external" title="${_("Google+")}" rel="noreferrer">
<span class="icon fa fa-weibo element-invisible"></span>
</a>
## Translators: This is the website name of www.meetup.com. Please
## translate this the way that Meetup advertises in your language.
<a href="${settings.SOCIAL_MEDIA_FOOTER_URLS.get('facebook', '#')}" class="sm-link external" title="${_("Meetup")}" rel="noreferrer">
<span class="icon fa fa-vk element-invisible"></span>
</a>
</div>
## % if settings.FEATURES.get('ENABLE_FOOTER_MOBILE_APP_LINKS'):
<div class="mobile-app-links">
<a href="${settings.MOBILE_STORE_URLS.get('apple', '#')}" class="app-link external">
<img class="app-store" alt="${_("Apple app on Apple Store")}" src="${static.url('images/app/app_store_badge_135x40.svg')}">
</a>
<a href="${settings.MOBILE_STORE_URLS.get('google', '#')}" class="app-link external">
<img class="google-play" alt="${_("Android app on Google Play")}" src="${static.url('images/app/google_play_badge_45.png')}">
</a>
</div>
## % endif
<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>
</footer>
% if include_dependencies:
<%static:js group='base_vendor'/>
<%static:css group='style-vendor'/>
<%include file="widgets/segment-io.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
<script type="text/javascript" src="/static/js/vendor/noreferrer.js" charset="utf-8"></script>
<%block name="js_extra">
<%static:js group='footer_edx'/>
</%block>
## mako
<%! from django.core.urlresolvers import reverse %>
<%! from django.utils.translation import ugettext as _ %>
<%!
from django.core.urlresolvers import reverse
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'/>
<%! from microsite_configuration.templatetags.microsite import platform_name %>
<div class="wrapper wrapper-footer">
<footer>
<footer id="footer-openedx"
## 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
>
<div class="colophon">
<nav class="nav-colophon" aria-label="${_('About')}">
<ol>
<li class="nav-colophon-01">
<a id="about" href="${marketing_link('ABOUT')}">
${_("About")}
</a>
</li>
%if marketing_link('JOBS') and marketing_link('JOBS') != '#':
<li class="nav-colophon-02">
<a id="jobs" href="${marketing_link('JOBS')}">
${_("Jobs")}
</a>
</li>
%endif
%if marketing_link('NEWS') and marketing_link('NEWS') != '#':
<li class="nav-colophon-03">
<a id="news" href="${marketing_link('NEWS')}">
${_("News")}
</a>
<ol>
% for item_num, link in enumerate(footer['navigation_links'], start=1):
<li class="nav-colophon-0${item_num}">
<a id="${link['name']}" href="${link['url']}">${link['title']}</a>
</li>
%endif
<li class="nav-colophon-04">
<a id="faq" href="${marketing_link('FAQ')}">
${_("FAQ")}
</a>
</li>
<li class="nav-colophon-05">
<a id="contact" href="${marketing_link('CONTACT')}">
${_("Contact")}
</a>
</li>
</ol>
% endfor
</ol>
</nav>
<div class="wrapper-logo">
<p>
<a href="/">
## this is just a placeholder logo
## feel free to change this logo to your own by replacing "logo.png" with your own logo
<img alt="organization logo placeholder" src="/static/images/default-theme/logo.png">
## The default logo is a placeholder.
## You can either replace this link entirely or update
## the FOOTER_ORGANIZATION_IMAGE in Django settings.
## If you customize FOOTER_ORGANIZATION_IMAGE, then the image
## can be included in the footer on other sites
## (e.g. a blog or marketing front-end) to provide a consistent
## user experience. See the branding app for details.
<img alt="organization logo" src="${footer['logo_image']}">
</a>
</p>
</div>
<p class="copyright">&copy; ${settings.COPYRIGHT_YEAR} ${settings.PLATFORM_NAME}.</p>
## Site operators: Please do not remove this paragraph! This attributes back to edX and makes your acknowledgement of edX's trademarks clear.
<p class="copyright">
## Translators: 'EdX', 'edX', and 'Open edX' are trademarks of 'edX Inc.'. Please do not translate any of these trademarks and company names.
${_("EdX, Open edX, and the edX and Open edX logos are registered trademarks or trademarks of {link_start}edX Inc.{link_end}").format(
link_start=u"<a href='https://www.edx.org/'>",
link_end=u"</a>"
)}
</p>
<p class="copyright">${footer['copyright']}</p>
<nav class="nav-legal" aria-label="${_('Legal')}">
<ul>
% if marketing_link('HONOR') and marketing_link('HONOR') != '#':
<li class="nav-legal-01">
<%
tos_link = u"<a href='{}'>".format(marketing_link('TOS'))
honor_link = u"<a href='{}'>".format(marketing_link('HONOR'))
%>
${
_("{tos_link_start}Terms of Service{tos_link_end} and {honor_link_start}Honor Code{honor_link_end}").format(
tos_link_start=tos_link,
tos_link_end="</a>",
honor_link_start=honor_link,
honor_link_end="</a>",
)
}
</li>
% else:
<li class="nav-legal-01">
<a href="${marketing_link('TOS')}">${_("Terms of Service")}</a>
</li>
% endif
<li class="nav-legal-02">
<a href="${marketing_link('PRIVACY')}">${_("Privacy Policy")}</a>
</li>
% for item_num, link in enumerate(footer['legal_links'], start=1):
<li class="nav-legal-0${item_num}">
<a href="${link['url']}">${link['title']}</a>
</li>
% endfor
</ul>
</nav>
</div>
## please leave this link and use one of the logos provided
## Please leave this link and use one of the logos provided
## 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="footer-about-openedx">
<p>
<a href="http://openedx.org/">
## standard powered-by logo
## Translators: 'Open edX' is a brand, please keep this untranslated. See http://openedx.org for more information.
<img src="https://files.edx.org/openedx-logos/edx-openedx-logo-tag.png" alt="${_('Powered by Open edX')}" width="140" />
## greyscale logo for dark background
## <img src="https://files.edx.org/openedx-logos/edx-openedx-logo-tag-light.png" alt="${_('Powered by Open edX')}" width="140" />
## greyscale logo for light background
## <img src="https://files.edx.org/openedx-logos/edx-openedx-logo-tag-dark.png" alt="${_('Powered by Open edX')}" width="140" />
<a href="${footer['openedx_link']['url']}">
<img src="${footer['openedx_link']['image']}" alt="${footer['openedx_link']['title']}" width="140" />
</a>
</p>
</div>
% endif
</footer>
</div>
% if include_dependencies:
<%static:js group='base_vendor'/>
<%static:css group='style-vendor'/>
<%include file="widgets/segment-io.html" />
% endif
% if footer_css_urls:
% for url in footer_css_urls:
<link rel="stylesheet" type="text/css" href="${url}"></link>
% endfor
% endif
## coding=utf-8
<%namespace name='static' file='static_content.html'/>
<%!
from django.core.urlresolvers import reverse
from django.utils.http import urlquote_plus
from django.utils.translation import ugettext as _
from django.utils.translation import get_language_bidi
from microsite_configuration import microsite
from microsite_configuration import page_title_breadcrumbs
from branding import api as branding_api
dir_rtl = 'rtl' if get_language_bidi() else 'ltr'
%>
......@@ -145,17 +147,18 @@ dir_rtl = 'rtl' if get_language_bidi() else 'ltr'
% 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():
footer_file = 'theme-footer.html'
elif settings.FEATURES.get('IS_EDX_DOMAIN', False) and settings.FEATURES.get('ENABLE_FOOTER_V3', True) and not is_microsite():
footer_file = microsite.get_template_path('footer-edx-v3.html')
elif settings.FEATURES.get('IS_EDX_DOMAIN', False) and not is_microsite():
footer_file = microsite.get_template_path('footer-edx-v2.html')
else:
footer_file = microsite.get_template_path('footer.html')
%>
<%include file="${footer_file}" />
% if theme_enabled() and not is_microsite():
<%include file="theme-footer.html" />
% elif settings.FEATURES.get('IS_EDX_DOMAIN', False) and not is_microsite():
## TODO (ECOM-1339): Remove this check to permanently enable the V3 footer.
% if branding_api.is_enabled():
<%include file="footer-edx-v3.html" />
% else:
<%include file="footer-edx-v2.html" />
% endif
% else:
<%include file="${microsite.get_template_path('footer.html')}" />
% endif
</%block>
% endif
......@@ -167,6 +170,7 @@ dir_rtl = 'rtl' if get_language_bidi() else 'ltr'
% endif
<%block name="js_extra"/>
<script type="text/javascript" src="${static.url('js/vendor/noreferrer.js')}" charset="utf-8"></script>
</body>
</html>
......
<!DOCTYPE html>
{% load compressed %}{% load sekizai_tags i18n microsite %}{% load url from future %}{% load staticfiles %}
{% load compressed %}
{% load sekizai_tags i18n microsite %}
{% load url from future %}
{% load staticfiles %}
<html lang="{{LANGUAGE_CODE}}">
<head>
<meta charset="UTF-8">
......@@ -38,10 +41,13 @@
</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 and not ENABLE_FOOTER_V3 %}
{% include "footer-edx-v2.html" %}
{% elif IS_EDX_DOMAIN and ENABLE_FOOTER_V3 %}
{% include "footer-edx-v3.html" %}
{% elif IS_EDX_DOMAIN %}
{# TODO (ECOM-1339): Remove this check once we switch to the v3 footer permanently. #}
{% if ENABLE_BRANDING_API %}
{% include "footer-edx-v3.html" %}
{% else %}
{% include "footer-edx-v2.html" %}
{% endif %}
{% else %}
{% include "footer.html" %}
{% endif %}
......
......@@ -27,5 +27,5 @@
</header>
<%block name="js_extra">
<%static:js group='rwd_header_footer'/>
<%static:js group='rwd_header'/>
</%block>
......@@ -20,7 +20,7 @@ from verify_student.views import PayAndVerifyView
% endfor
</%block>
<%block name="js_extra">
<%static:js group='rwd_header_footer'/>
<%static:js group='rwd_header'/>
<script src="${static.url('js/vendor/underscore-min.js')}"></script>
<script src="${static.url('js/vendor/underscore.string.min.js')}"></script>
<script src="${static.url('js/vendor/backbone-min.js')}"></script>
......
......@@ -34,7 +34,7 @@ from verify_student.views import PayAndVerifyView
% endfor
</%block>
<%block name="js_extra">
<%static:js group='rwd_header_footer'/>
<%static:js group='rwd_header'/>
<script src="${static.url('js/vendor/underscore-min.js')}"></script>
<script src="${static.url('js/vendor/underscore.string.min.js')}"></script>
<script src="${static.url('js/vendor/backbone-min.js')}"></script>
......
......@@ -427,6 +427,8 @@ if settings.COURSEWARE_ENABLED:
# Student Notes
url(r'^courses/{}/edxnotes'.format(settings.COURSE_ID_PATTERN),
include('edxnotes.urls'), name="edxnotes_endpoints"),
url(r'^api/branding/v1/', include('branding.api_urls')),
)
# allow course staff to change to student view of courseware
......
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