diff --git a/.gitignore b/.gitignore
index e1e15e6102..6769176b86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/common/static/js/spec_helpers/rwd_header_footer.js b/common/static/js/spec_helpers/rwd_header_footer.js
deleted file mode 100644
index 8f737a6bbf..0000000000
--- a/common/static/js/spec_helpers/rwd_header_footer.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * 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([
- ''
- ].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.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);
diff --git a/common/static/js/utils/rwd_header_footer.js b/common/static/js/utils/rwd_header.js
similarity index 93%
rename from common/static/js/utils/rwd_header_footer.js
rename to common/static/js/utils/rwd_header.js
index 83ba652c8b..616f176268 100644
--- a/common/static/js/utils/rwd_header_footer.js
+++ b/common/static/js/utils/rwd_header.js
@@ -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() {
diff --git a/conf/locale/eo/LC_MESSAGES/django.mo b/conf/locale/eo/LC_MESSAGES/django.mo
index 338bdec0f2..0b34bcb3f0 100644
Binary files a/conf/locale/eo/LC_MESSAGES/django.mo and b/conf/locale/eo/LC_MESSAGES/django.mo differ
diff --git a/conf/locale/eo/LC_MESSAGES/django.po b/conf/locale/eo/LC_MESSAGES/django.po
index 6963d0a28c..9e5407259f 100644
--- a/conf/locale/eo/LC_MESSAGES/django.po
+++ b/conf/locale/eo/LC_MESSAGES/django.po
@@ -37,8 +37,8 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.1a\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
-"POT-Creation-Date: 2015-05-26 15:42+0000\n"
-"PO-Revision-Date: 2015-05-26 15:42:32.205037\n"
+"POT-Creation-Date: 2015-05-26 18:33+0000\n"
+"PO-Revision-Date: 2015-05-26 18:33:10.093000\n"
"Last-Translator: \n"
"Language-Team: openedx-translation \n"
"MIME-Version: 1.0\n"
@@ -179,7 +179,7 @@ msgstr ""
"Ýöü'ré énrölléd äs än hönör çödé stüdént Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, "
"¢σηѕє¢тєтυя#"
-#: common/djangoapps/course_modes/models.py
+#: common/djangoapps/course_modes/models.py lms/djangoapps/branding/api.py
#: openedx/core/djangoapps/user_api/views.py
#: lms/templates/static_templates/honor.html
msgid "Honor Code"
@@ -3184,9 +3184,9 @@ msgstr ""
msgid "Both"
msgstr "Böth Ⱡ'σяєм ι#"
-#: common/lib/xmodule/xmodule/course_module.py
-#: lms/templates/footer-edx-v2.html lms/templates/footer-edx-v3.html
-#: lms/templates/footer.html lms/templates/static_templates/about.html
+#: common/lib/xmodule/xmodule/course_module.py lms/djangoapps/branding/api.py
+#: lms/templates/footer-edx-v2.html lms/templates/footer.html
+#: lms/templates/static_templates/about.html
msgid "About"
msgstr "Àßöüt Ⱡ'σяєм ιρѕ#"
@@ -5141,6 +5141,80 @@ msgstr "Séärçh Ⱡ'σяєм ιρѕυ#"
msgid "Copyright"
msgstr "Çöpýrïght Ⱡ'σяєм ιρѕυм ∂σł#"
+#: lms/djangoapps/branding/api.py
+msgid ""
+"© {org_name}. All rights reserved except where noted. EdX, Open edX and "
+"the edX and Open EdX logos are registered trademarks or trademarks of edX "
+"Inc."
+msgstr ""
+"© {org_name}. Àll rïghts résérvéd éxçépt whéré nötéd. ÉdX, Öpén édX änd "
+"thé édX änd Öpén ÉdX lögös äré régïstéréd trädémärks ör trädémärks öf édX "
+"Ìnç. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ "
+"єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм"
+" νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα "
+"¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт"
+" єѕѕє ¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт "
+"¢υρι∂αтαт ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αηιм ι∂#"
+
+#. #-#-#-#-# django-partial.po (0.1a) #-#-#-#-#
+#. Translators: 'Open edX' is a brand, please keep this untranslated.
+#. See http://openedx.org for more information.
+#: lms/djangoapps/branding/api.py cms/templates/widgets/footer.html
+#: lms/templates/footer-edx-v2.html
+msgid "Powered by Open edX"
+msgstr "Pöwéréd ßý Öpén édX Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
+
+#: lms/djangoapps/branding/api.py lms/templates/static_templates/blog.html
+msgid "Blog"
+msgstr "Blög Ⱡ'σяєм ι#"
+
+#: lms/djangoapps/branding/api.py lms/templates/footer-edx-v2.html
+msgid "News"
+msgstr "Néws Ⱡ'σяєм ι#"
+
+#: lms/djangoapps/branding/api.py
+msgid "FAQs"
+msgstr "FÀQs Ⱡ'σяєм ι#"
+
+#: lms/djangoapps/branding/api.py lms/templates/footer-edx-v2.html
+#: lms/templates/static_templates/contact.html
+msgid "Contact"
+msgstr "Çöntäçt Ⱡ'σяєм ιρѕυм #"
+
+#: lms/djangoapps/branding/api.py lms/templates/static_templates/jobs.html
+msgid "Jobs"
+msgstr "Jößs Ⱡ'σяєм ι#"
+
+#: lms/djangoapps/branding/api.py lms/templates/static_templates/donate.html
+msgid "Donate"
+msgstr "Dönäté Ⱡ'σяєм ιρѕυ#"
+
+#: lms/djangoapps/branding/api.py
+msgid "Sitemap"
+msgstr "Sïtémäp Ⱡ'σяєм ιρѕυм #"
+
+#. #-#-#-#-# django-partial.po (0.1a) #-#-#-#-#
+#. Translators: This is a legal document users must agree to
+#. in order to register a new account.
+#: lms/djangoapps/branding/api.py openedx/core/djangoapps/user_api/views.py
+#: cms/templates/widgets/footer.html lms/templates/static_templates/tos.html
+msgid "Terms of Service"
+msgstr "Térms öf Sérvïçé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#"
+
+#: lms/djangoapps/branding/api.py
+msgid "Terms of Service & Honor Code"
+msgstr "Térms öf Sérvïçé & Hönör Çödé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
+
+#: lms/djangoapps/branding/api.py lms/djangoapps/certificates/views.py
+#: cms/templates/widgets/footer.html lms/templates/footer-edx-v2.html
+#: lms/templates/static_templates/privacy.html
+msgid "Privacy Policy"
+msgstr "Prïväçý Pölïçý Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#"
+
+#: lms/djangoapps/branding/api.py
+msgid "Accessibility Policy"
+msgstr "Àççéssïßïlïtý Pölïçý Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
+
#: lms/djangoapps/ccx/views.py
msgid "You must be a CCX Coach to access this view."
msgstr ""
@@ -5363,13 +5437,7 @@ msgstr "Wörk ät {platform_name} Ⱡ'σяєм ιρѕυм ∂σłσя #"
msgid "Contact {platform_name}"
msgstr "Çöntäçt {platform_name} Ⱡ'σяєм ιρѕυм ∂σłσя #"
-#: lms/djangoapps/certificates/views.py cms/templates/widgets/footer.html
-#: lms/templates/footer-edx-v2.html lms/templates/footer-edx-v3.html
-#: lms/templates/footer.html lms/templates/static_templates/privacy.html
-msgid "Privacy Policy"
-msgstr "Prïväçý Pölïçý Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#"
-
-#: lms/djangoapps/certificates/views.py lms/templates/footer-edx-v3.html
+#: lms/djangoapps/certificates/views.py
msgid "Terms of Service & Honor Code"
msgstr ""
"Térms öf Sérvïçé & Hönör Çödé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
@@ -8427,42 +8495,26 @@ msgstr "Çöüld nöt süßmït phötös Ⱡ'σяєм ιρѕυм ∂σłσя ѕ
#. #-#-#-#-# django-partial.po (0.1a) #-#-#-#-#
#. Translators: This is the website name of www.facebook.com. Please
#. translate this the way that Facebook advertises in your language.
-#. #-#-#-#-# mako.po (0.1a) #-#-#-#-#
-#. Translators: This is the website name of www.facebook.com. Please
-#. translate this the way that Facebook advertises in your language.
-#: lms/envs/common.py lms/templates/footer-edx-v3.html
-#: lms/templates/dashboard/_dashboard_course_listing.html
+#: lms/envs/common.py lms/templates/dashboard/_dashboard_course_listing.html
msgid "Facebook"
msgstr "Fäçéßöök Ⱡ'σяєм ιρѕυм ∂#"
#. #-#-#-#-# django-partial.po (0.1a) #-#-#-#-#
#. Translators: This is the website name of www.twitter.com. Please
#. translate this the way that Twitter advertises in your language.
-#. #-#-#-#-# mako.po (0.1a) #-#-#-#-#
-#. Translators: This is the website name of www.twitter.com. Please
-#. translate this the way that Twitter advertises in your language.
-#: lms/envs/common.py lms/templates/footer-edx-v3.html
-#: lms/templates/dashboard/_dashboard_course_listing.html
+#: lms/envs/common.py lms/templates/dashboard/_dashboard_course_listing.html
msgid "Twitter"
msgstr "Twïttér Ⱡ'σяєм ιρѕυм #"
-#. #-#-#-#-# django-partial.po (0.1a) #-#-#-#-#
#. Translators: This is the website name of www.linkedin.com. Please
#. translate this the way that LinkedIn advertises in your language.
-#. #-#-#-#-# mako.po (0.1a) #-#-#-#-#
-#. Translators: This is the website name of www.linked.com. Please
-#. translate this the way that LinkedIn advertises in your language.
-#: lms/envs/common.py lms/templates/footer-edx-v3.html
+#: lms/envs/common.py
msgid "LinkedIn"
msgstr "LïnkédÌn Ⱡ'σяєм ιρѕυм ∂#"
-#. #-#-#-#-# django-partial.po (0.1a) #-#-#-#-#
#. Translators: This is the website name of plus.google.com. Please
#. translate this the way that Google+ advertises in your language.
-#. #-#-#-#-# mako.po (0.1a) #-#-#-#-#
-#. Translators: This is the website name of plus.google.com. Please
-#. translate this the way that Google+ advertises in your language.
-#: lms/envs/common.py lms/templates/footer-edx-v3.html
+#: lms/envs/common.py
msgid "Google+"
msgstr "Gööglé+ Ⱡ'σяєм ιρѕυм #"
@@ -8472,13 +8524,9 @@ msgstr "Gööglé+ Ⱡ'σяєм ιρѕυм #"
msgid "Tumblr"
msgstr "Tümßlr Ⱡ'σяєм ιρѕυ#"
-#. #-#-#-#-# django-partial.po (0.1a) #-#-#-#-#
#. Translators: This is the website name of www.meetup.com. Please
#. translate this the way that MeetUp advertises in your language.
-#. #-#-#-#-# mako.po (0.1a) #-#-#-#-#
-#. Translators: This is the website name of www.meetup.com. Please
-#. translate this the way that Meetup advertises in your language.
-#: lms/envs/common.py lms/templates/footer-edx-v3.html
+#: lms/envs/common.py
msgid "Meetup"
msgstr "Méétüp Ⱡ'σяєм ιρѕυ#"
@@ -9397,14 +9445,6 @@ msgstr ""
"Ýöü müst ägréé tö thé {platform_name} {terms_of_service}. Ⱡ'σяєм ιρѕυм ∂σłσя"
" ѕιт αмєт, ¢σηѕє¢т#"
-#. #-#-#-#-# django-partial.po (0.1a) #-#-#-#-#
-#. Translators: This is a legal document users must agree to
-#. in order to register a new account.
-#: openedx/core/djangoapps/user_api/views.py cms/templates/widgets/footer.html
-#: lms/templates/footer.html lms/templates/static_templates/tos.html
-msgid "Terms of Service"
-msgstr "Térms öf Sérvïçé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#"
-
#: openedx/core/djangoapps/user_api/accounts/api.py
msgid "The '{field_name}' field cannot be edited."
msgstr ""
@@ -10081,13 +10121,6 @@ msgstr ""
"Vïsït ýöür {link_start}däshßöärd{link_end} tö séé ýöür çöürsés. Ⱡ'σяєм ιρѕυм"
" ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
-#. Translators: 'Open edX' is a brand, please keep this untranslated. See
-#. http://openedx.org for more information.
-#: cms/templates/widgets/footer.html lms/templates/footer-edx-v2.html
-#: lms/templates/footer-edx-v3.html lms/templates/footer.html
-msgid "Powered by Open edX"
-msgstr "Pöwéréd ßý Öpén édX Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
-
#: cms/templates/widgets/header.html lms/templates/courseware/courseware.html
msgid "Course Navigation"
msgstr "Çöürsé Nävïgätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмє#"
@@ -10499,7 +10532,7 @@ msgstr ""
#. Translators: 'EdX', 'edX', and 'Open edX' are trademarks of 'edX Inc.'.
#. Please do not translate any of these trademarks and company names.
-#: lms/templates/footer-edx-v2.html lms/templates/footer.html
+#: lms/templates/footer-edx-v2.html
msgid ""
"EdX, Open edX, and the edX and Open edX logos are registered trademarks or "
"trademarks of {link_start}edX Inc.{link_end}"
@@ -10521,18 +10554,7 @@ msgstr "(Révïséd {date}) Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
msgid "10/22/2014"
msgstr "10/22/2014 Ⱡ'σяєм ιρѕυм ∂σłσ#"
-#: lms/templates/footer-edx-v2.html lms/templates/footer-edx-v3.html
-#: lms/templates/footer.html
-msgid "News"
-msgstr "Néws Ⱡ'σяєм ι#"
-
-#: lms/templates/footer-edx-v2.html lms/templates/footer-edx-v3.html
-#: lms/templates/footer.html lms/templates/static_templates/contact.html
-msgid "Contact"
-msgstr "Çöntäçt Ⱡ'σяєм ιρѕυм #"
-
-#: lms/templates/footer-edx-v2.html lms/templates/footer.html
-#: lms/templates/static_templates/faq.html
+#: lms/templates/footer-edx-v2.html lms/templates/static_templates/faq.html
msgid "FAQ"
msgstr "FÀQ Ⱡ'σяєм#"
@@ -10556,11 +10578,11 @@ msgstr "Föllöw Ûs Ⱡ'σяєм ιρѕυм ∂σł#"
msgid "Mobile Apps"
msgstr "Mößïlé Àpps Ⱡ'σяєм ιρѕυм ∂σłσя #"
-#: lms/templates/footer-edx-v2.html lms/templates/footer-edx-v3.html
+#: lms/templates/footer-edx-v2.html
msgid "Apple app on Apple Store"
msgstr "Àpplé äpp ön Àpplé Störé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#"
-#: lms/templates/footer-edx-v2.html lms/templates/footer-edx-v3.html
+#: lms/templates/footer-edx-v2.html
msgid "Android app on Google Play"
msgstr "Àndröïd äpp ön Gööglé Pläý Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
@@ -10568,43 +10590,10 @@ msgstr "Àndröïd äpp ön Gööglé Pläý Ⱡ'σяєм ιρѕυм ∂σłσя
msgid "Page Footer"
msgstr "Pägé Föötér Ⱡ'σяєм ιρѕυм ∂σłσя #"
-#: lms/templates/footer-edx-v3.html lms/templates/static_templates/blog.html
-msgid "Blog"
-msgstr "Blög Ⱡ'σяєм ι#"
-
-#: lms/templates/footer-edx-v3.html
-msgid "FAQs"
-msgstr "FÀQs Ⱡ'σяєм ι#"
-
-#: lms/templates/footer-edx-v3.html lms/templates/footer.html
-#: lms/templates/static_templates/jobs.html
-msgid "Jobs"
-msgstr "Jößs Ⱡ'σяєм ι#"
-
-#: lms/templates/footer-edx-v3.html lms/templates/static_templates/donate.html
-msgid "Donate"
-msgstr "Dönäté Ⱡ'σяєм ιρѕυ#"
-
-#: lms/templates/footer-edx-v3.html
-msgid "Sitemap"
-msgstr "Sïtémäp Ⱡ'σяєм ιρѕυм #"
-
#: lms/templates/footer-edx-v3.html lms/templates/footer.html
msgid "Legal"
msgstr "Légäl Ⱡ'σяєм ιρѕ#"
-#: lms/templates/footer-edx-v3.html
-msgid "Website Accessibility Policy"
-msgstr "Wéßsïté Àççéssïßïlïtý Pölïçý Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
-
-#: lms/templates/footer.html
-msgid ""
-"{tos_link_start}Terms of Service{tos_link_end} and {honor_link_start}Honor "
-"Code{honor_link_end}"
-msgstr ""
-"{tos_link_start}Térms öf Sérvïçé{tos_link_end} änd {honor_link_start}Hönör "
-"Çödé{honor_link_end} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
-
#: lms/templates/forgot_password_modal.html
msgid "Password Reset"
msgstr "Pässwörd Rését Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#"
diff --git a/lms/djangoapps/branding/admin.py b/lms/djangoapps/branding/admin.py
index d10b2a9c61..4450174e16 100644
--- a/lms/djangoapps/branding/admin.py
+++ b/lms/djangoapps/branding/admin.py
@@ -1,9 +1,8 @@
-'''
-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)
diff --git a/lms/djangoapps/branding/api.py b/lms/djangoapps/branding/api.py
new file mode 100644
index 0000000000..119f3aa06b
--- /dev/null
+++ b/lms/djangoapps/branding/api.py
@@ -0,0 +1,291 @@
+"""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)
diff --git a/lms/djangoapps/branding/api_urls.py b/lms/djangoapps/branding/api_urls.py
new file mode 100644
index 0000000000..d7ba50c012
--- /dev/null
+++ b/lms/djangoapps/branding/api_urls.py
@@ -0,0 +1,15 @@
+"""
+Branding API endpoint urls.
+"""
+
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns(
+ "",
+
+ url(
+ r"^footer$",
+ "branding.views.footer",
+ name="branding_footer",
+ ),
+)
diff --git a/lms/djangoapps/branding/context_processors.py b/lms/djangoapps/branding/context_processors.py
new file mode 100644
index 0000000000..3aa688381d
--- /dev/null
+++ b/lms/djangoapps/branding/context_processors.py
@@ -0,0 +1,10 @@
+"""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()
+ }
diff --git a/lms/djangoapps/branding/migrations/0002_auto__add_brandingapiconfig.py b/lms/djangoapps/branding/migrations/0002_auto__add_brandingapiconfig.py
new file mode 100644
index 0000000000..ffa45652a6
--- /dev/null
+++ b/lms/djangoapps/branding/migrations/0002_auto__add_brandingapiconfig.py
@@ -0,0 +1,80 @@
+# -*- 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']
diff --git a/lms/djangoapps/branding/models.py b/lms/djangoapps/branding/models.py
index 7ee3d21e99..67a51d4f26 100644
--- a/lms/djangoapps/branding/models.py
+++ b/lms/djangoapps/branding/models.py
@@ -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
diff --git a/lms/djangoapps/branding/tests/test_views.py b/lms/djangoapps/branding/tests/test_views.py
new file mode 100644
index 0000000000..b11ab62518
--- /dev/null
+++ b/lms/djangoapps/branding/tests/test_views.py
@@ -0,0 +1,214 @@
+# 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
diff --git a/lms/djangoapps/branding/views.py b/lms/djangoapps/branding/views.py
index b79df70f55..8e950dfb91 100644
--- a/lms/djangoapps/branding/views.py
+++ b/lms/djangoapps/branding/views.py
@@ -1,17 +1,29 @@
+"""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)
diff --git a/lms/djangoapps/courseware/features/homepage.feature b/lms/djangoapps/courseware/features/homepage.feature
index 49b3cf3e65..2cf3173713 100644
--- a/lms/djangoapps/courseware/features/homepage.feature
+++ b/lms/djangoapps/courseware/features/homepage.feature
@@ -18,6 +18,6 @@ Feature: LMS.Homepage for web users
| id | Link |
| about | About |
| jobs | Jobs |
- | faq | FAQ |
+ | faq | FAQs |
| contact | Contact|
| news | News |
diff --git a/lms/djangoapps/courseware/tests/test_about.py b/lms/djangoapps/courseware/tests/test_about.py
index 6d19bccc4a..0c75863408 100644
--- a/lms/djangoapps/courseware/tests/test_about.py
+++ b/lms/djangoapps/courseware/tests/test_about.py
@@ -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)
diff --git a/lms/djangoapps/courseware/tests/test_footer.py b/lms/djangoapps/courseware/tests/test_footer.py
index edd873de54..1300f562f4 100644
--- a/lms/djangoapps/courseware/tests/test_footer.py
+++ b/lms/djangoapps/courseware/tests/test_footer.py
@@ -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():
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index af77744869..94d72aa61e 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -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'):
diff --git a/lms/envs/common.py b/lms/envs/common.py
index ecb8c94bcb..bbd17ef5aa 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -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"
}
}
diff --git a/lms/static/js/footer-edx.js b/lms/static/js/footer-edx.js
index 0d589f5885..0b388d1aa6 100644
--- a/lms/static/js/footer-edx.js
+++ b/lms/static/js/footer-edx.js
@@ -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);
diff --git a/lms/static/sass/_build-footer-edx.scss b/lms/static/sass/_build-footer-edx.scss
new file mode 100644
index 0000000000..09b539b5c5
--- /dev/null
+++ b/lms/static/sass/_build-footer-edx.scss
@@ -0,0 +1,19 @@
+// ----------------------------------------
+// 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';
diff --git a/lms/static/sass/_build-lms.scss b/lms/static/sass/_build-lms.scss
index 3b05db8196..c5f95fe8bf 100644
--- a/lms/static/sass/_build-lms.scss
+++ b/lms/static/sass/_build-lms.scss
@@ -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';
diff --git a/lms/static/sass/base/_variables-ltr.scss b/lms/static/sass/base/_variables-ltr.scss
new file mode 100644
index 0000000000..4e999af162
--- /dev/null
+++ b/lms/static/sass/base/_variables-ltr.scss
@@ -0,0 +1,7 @@
+// Variables for LMS (left-to-right)
+// ==================================
+
+// Neat
+// ==================================
+// Sets the default layout direction of the grid.
+$default-layout-direction: LTR !global;
diff --git a/lms/static/sass/base/_variables-rtl.scss b/lms/static/sass/base/_variables-rtl.scss
new file mode 100644
index 0000000000..b49e44b1f0
--- /dev/null
+++ b/lms/static/sass/base/_variables-rtl.scss
@@ -0,0 +1,7 @@
+// Variables for LMS (right-to-left)
+// ==================================
+
+// Neat
+// ==================================
+// Sets the default layout direction of the grid.
+$default-layout-direction: RTL !global;
diff --git a/lms/static/sass/lms-footer-edx-rtl.scss b/lms/static/sass/lms-footer-edx-rtl.scss
new file mode 100644
index 0000000000..e189fe9e6d
--- /dev/null
+++ b/lms/static/sass/lms-footer-edx-rtl.scss
@@ -0,0 +1,10 @@
+// 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'
diff --git a/lms/static/sass/lms-footer-edx.scss b/lms/static/sass/lms-footer-edx.scss
new file mode 100644
index 0000000000..afa3d26592
--- /dev/null
+++ b/lms/static/sass/lms-footer-edx.scss
@@ -0,0 +1,10 @@
+// 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'
diff --git a/lms/static/sass/lms-footer-edx-rtl.scss.mako b/lms/static/sass/lms-footer-rtl.scss.mako
similarity index 60%
rename from lms/static/sass/lms-footer-edx-rtl.scss.mako
rename to lms/static/sass/lms-footer-rtl.scss.mako
index 7c6f4466c7..cc91fab379 100644
--- a/lms/static/sass/lms-footer-edx-rtl.scss.mako
+++ b/lms/static/sass/lms-footer-rtl.scss.mako
@@ -1,14 +1,9 @@
-## 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';
diff --git a/lms/static/sass/lms-footer-edx.scss.mako b/lms/static/sass/lms-footer.scss.mako
similarity index 66%
rename from lms/static/sass/lms-footer-edx.scss.mako
rename to lms/static/sass/lms-footer.scss.mako
index ceabdbc5de..0b4aff0381 100644
--- a/lms/static/sass/lms-footer-edx.scss.mako
+++ b/lms/static/sass/lms-footer.scss.mako
@@ -1,15 +1,10 @@
-## 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';
diff --git a/lms/static/sass/lms-main-rtl.scss.mako b/lms/static/sass/lms-main-rtl.scss.mako
index 1bac12f885..dcb06ff91a 100644
--- a/lms/static/sass/lms-main-rtl.scss.mako
+++ b/lms/static/sass/lms-main-rtl.scss.mako
@@ -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*
// ====================
diff --git a/lms/static/sass/lms-main.scss.mako b/lms/static/sass/lms-main.scss.mako
index 3a1a71f6e5..83d7be23d3 100644
--- a/lms/static/sass/lms-main.scss.mako
+++ b/lms/static/sass/lms-main.scss.mako
@@ -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*
// ====================
diff --git a/lms/static/sass/shared/_footer-edx.scss b/lms/static/sass/shared/_footer-edx.scss
index fc2babf3be..b82484c91b 100644
--- a/lms/static/sass/shared/_footer-edx.scss
+++ b/lms/static/sass/shared/_footer-edx.scss
@@ -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);
diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss
index d38397475c..8240d7ce7b 100644
--- a/lms/static/sass/shared/_footer.scss
+++ b/lms/static/sass/shared/_footer.scss
@@ -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;
diff --git a/lms/templates/commerce/checkout_receipt.html b/lms/templates/commerce/checkout_receipt.html
index 2989e84597..448fd6faf7 100644
--- a/lms/templates/commerce/checkout_receipt.html
+++ b/lms/templates/commerce/checkout_receipt.html
@@ -17,7 +17,7 @@ ${_("Receipt")}
%block>
<%block name="js_extra">
-<%static:js group='rwd_header_footer'/>
+<%static:js group='rwd_header'/>
diff --git a/lms/templates/footer-edx-v2.html b/lms/templates/footer-edx-v2.html
index 5072e66700..042c06b849 100644
--- a/lms/templates/footer-edx-v2.html
+++ b/lms/templates/footer-edx-v2.html
@@ -1,4 +1,5 @@
## 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 @@