From a796b563145be9a557e9a55e925e0976d78c733c Mon Sep 17 00:00:00 2001
From: Zia Fazal <zia.fazal@arbisoft.com>
Date: Thu, 18 Feb 2016 19:07:31 +0500
Subject: [PATCH] saleem-latif/WL-328: Multi-Site Comprehensive Theming

ziafazal: improvements need for multi-tenancy
ziafazal: fixed broken tests
ziafazal: no need to add setting in test.py
ziafazal: added hostname validation
ziafazal: changes after feedback from mattdrayer
ziafazal: fixed branding and microsite broken tests
ziafazal: make STATICFILES_DIRS to list
ziafazal: added theme directory to mako lookup for tests
ziafazal: added more protection in test_util
saleem-latif: Enable SCSS Overrides for Comprehensive Theming
saleem-latif: Incoporate feedback changes, Correct test failures, add tests and enable theming for django templates
saleem-latif: Correct errors in python tests
mattdrayer: Fix invalid release reference
mattdrayer: Update django-wiki reference to latest release
saleem-latif: Update Theme storages to work with Caching, Pipeline and collectstatic
saleem-latif: Incorporate feedback changes
mattdrayer: Pylint violation fix
mattdrayer: Fix broken pavelib test
---
 cms/envs/aws.py                               |   1 +
 cms/envs/bok_choy.py                          |   4 +-
 cms/envs/common.py                            |  13 +-
 cms/envs/devstack.py                          |   1 +
 cms/envs/devstack_optimized.py                |   4 +-
 cms/envs/test.py                              |   4 +
 cms/startup.py                                |   4 +-
 .../sass/{ => partials}/_variables.scss       |   0
 .../course_modes/tests/test_views.py          |   4 +-
 common/djangoapps/edxmako/paths.py            |  31 +-
 common/djangoapps/edxmako/shortcuts.py        |  13 +-
 common/djangoapps/edxmako/tests.py            |  13 +-
 .../microsite_configuration/backends/base.py  |   4 +-
 .../tests/backends/test_database.py           |  18 +-
 .../templates/static_content.html             |   8 +-
 common/djangoapps/student/tests/test_email.py |   4 +-
 common/djangoapps/student/tests/tests.py      |   2 -
 common/test/db_fixtures/sites.json            |  20 +
 .../test/test-theme/cms/static/css/.gitignore |   1 +
 .../cms/static/sass/partials/_variables.scss  | 255 ++++++++++
 .../test/test-theme/cms/templates/login.html  |  55 ++
 .../test/test-theme/lms/static/css/.gitignore |   1 +
 .../test-theme/lms/static/images/logo.png     | Bin 0 -> 493 bytes
 .../static/sass/partials/base/_variables.scss |   5 +
 .../test/test-theme/lms/templates/footer.html |  10 +
 lms/djangoapps/branding/tests/test_views.py   |  60 +--
 lms/djangoapps/commerce/tests/test_views.py   |   5 +-
 .../tests/test_comprehensive_theming.py       |   3 +-
 lms/djangoapps/course_wiki/views.py           |  14 -
 .../tests/test_comprehensive_theming.py       |  88 +++-
 .../courseware/tests/test_course_info.py      |   3 +-
 .../courseware/tests/test_footer.py           |   7 +-
 .../student_account/test/test_views.py        |  16 +-
 .../verify_student/tests/test_views.py        |   4 +-
 lms/envs/aws.py                               |   1 +
 lms/envs/bok_choy.py                          |   4 +-
 lms/envs/common.py                            |  18 +-
 lms/envs/devstack.py                          |   1 +
 lms/envs/devstack_optimized.py                |   4 +-
 lms/envs/test.py                              |   5 +
 lms/startup.py                                |   4 +-
 .../sass/{ => partials}/base/_variables.scss  |   0
 lms/templates/main_django.html                |   2 +-
 lms/templates/wiki/base.html                  |   2 +-
 lms/templates/wiki/preview_inline.html        |   2 +-
 openedx/core/djangoapps/theming/admin.py      |  22 +
 openedx/core/djangoapps/theming/core.py       |  72 +--
 openedx/core/djangoapps/theming/finders.py    |  99 ++--
 openedx/core/djangoapps/theming/helpers.py    | 331 +++++++++++-
 .../theming/migrations/0001_initial.py        |  22 +
 .../djangoapps/theming/migrations/__init__.py |   0
 openedx/core/djangoapps/theming/models.py     |  19 +
 openedx/core/djangoapps/theming/storage.py    | 329 +++++++++---
 .../djangoapps/theming/template_loaders.py    |  27 +
 .../theming/templatetags/theme_pipeline.py    |  78 +++
 openedx/core/djangoapps/theming/test_util.py  |  84 +--
 .../djangoapps/theming/tests/test_helpers.py  | 152 ++++++
 .../djangoapps/theming/tests/test_storage.py  |  82 +++
 .../tests/test_theme_style_overrides.py       | 235 +++++++++
 openedx/core/lib/tempdir.py                   |  19 +
 openedx/core/storage.py                       |  14 +-
 pavelib/assets.py                             | 477 ++++++++++++++----
 pavelib/paver_tests/test_assets.py            | 150 +++++-
 pavelib/paver_tests/test_servers.py           |   1 -
 requirements/edx/base.txt                     |   3 +
 requirements/edx/github.txt                   |   2 +-
 themes/red-theme/cms/templates/login.html     |  55 ++
 .../red-theme/lms/static/sass/_overrides.scss |   7 -
 .../lms/static/sass/lms-main-rtl.scss         |   5 -
 .../red-theme/lms/static/sass/lms-main.scss   |   5 -
 .../static/sass/partials/base/_variables.scss |   5 +
 .../lms/static/sass/lms-main-rtl.scss         |   5 -
 .../lms/static/sass/lms-main.scss             |   5 -
 .../base/_variables.scss}                     |   2 +
 74 files changed, 2561 insertions(+), 464 deletions(-)
 rename cms/static/sass/{ => partials}/_variables.scss (100%)
 create mode 100644 common/test/db_fixtures/sites.json
 create mode 100644 common/test/test-theme/cms/static/css/.gitignore
 create mode 100644 common/test/test-theme/cms/static/sass/partials/_variables.scss
 create mode 100644 common/test/test-theme/cms/templates/login.html
 create mode 100644 common/test/test-theme/lms/static/css/.gitignore
 create mode 100644 common/test/test-theme/lms/static/images/logo.png
 create mode 100755 common/test/test-theme/lms/static/sass/partials/base/_variables.scss
 create mode 100644 common/test/test-theme/lms/templates/footer.html
 rename lms/static/sass/{ => partials}/base/_variables.scss (100%)
 create mode 100644 openedx/core/djangoapps/theming/admin.py
 create mode 100644 openedx/core/djangoapps/theming/migrations/0001_initial.py
 create mode 100644 openedx/core/djangoapps/theming/migrations/__init__.py
 create mode 100644 openedx/core/djangoapps/theming/models.py
 create mode 100644 openedx/core/djangoapps/theming/template_loaders.py
 create mode 100644 openedx/core/djangoapps/theming/templatetags/theme_pipeline.py
 create mode 100644 openedx/core/djangoapps/theming/tests/test_helpers.py
 create mode 100644 openedx/core/djangoapps/theming/tests/test_storage.py
 create mode 100644 openedx/core/djangoapps/theming/tests/test_theme_style_overrides.py
 create mode 100644 themes/red-theme/cms/templates/login.html
 delete mode 100755 themes/red-theme/lms/static/sass/_overrides.scss
 delete mode 100755 themes/red-theme/lms/static/sass/lms-main-rtl.scss
 delete mode 100755 themes/red-theme/lms/static/sass/lms-main.scss
 create mode 100755 themes/red-theme/lms/static/sass/partials/base/_variables.scss
 delete mode 100755 themes/stanford-style/lms/static/sass/lms-main-rtl.scss
 delete mode 100755 themes/stanford-style/lms/static/sass/lms-main.scss
 rename themes/stanford-style/lms/static/sass/{_overrides.scss => partials/base/_variables.scss} (82%)

diff --git a/cms/envs/aws.py b/cms/envs/aws.py
index a924de3018e..f28131c3530 100644
--- a/cms/envs/aws.py
+++ b/cms/envs/aws.py
@@ -191,6 +191,7 @@ ASSET_IGNORE_REGEX = ENV_TOKENS.get('ASSET_IGNORE_REGEX', ASSET_IGNORE_REGEX)
 # Theme overrides
 THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
 COMPREHENSIVE_THEME_DIR = path(ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', COMPREHENSIVE_THEME_DIR))
+THEME_CACHE_TIMEOUT = ENV_TOKENS.get('THEME_CACHE_TIMEOUT', THEME_CACHE_TIMEOUT)
 
 #Timezone overrides
 TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py
index 15bd0dee059..da12fb1b29e 100644
--- a/cms/envs/bok_choy.py
+++ b/cms/envs/bok_choy.py
@@ -59,9 +59,9 @@ STATIC_URL = "/static/"
 STATICFILES_FINDERS = (
     'django.contrib.staticfiles.finders.FileSystemFinder',
 )
-STATICFILES_DIRS = (
+STATICFILES_DIRS = [
     (TEST_ROOT / "staticfiles" / "cms").abspath(),
-)
+]
 
 # Silence noisy logs
 import logging
diff --git a/cms/envs/common.py b/cms/envs/common.py
index dff22311711..da262ff261b 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -61,7 +61,13 @@ from lms.envs.common import (
     # Django REST framework configuration
     REST_FRAMEWORK,
 
-    STATICI18N_OUTPUT_DIR
+    STATICI18N_OUTPUT_DIR,
+
+    # Dafault site id to use in case there is no site that matches with the request headers.
+    DEFAULT_SITE_ID,
+
+    # Cache time out settings for comprehensive theming system
+    THEME_CACHE_TIMEOUT,
 )
 from path import Path as path
 from warnings import simplefilter
@@ -344,6 +350,9 @@ MIDDLEWARE_CLASSES = (
 
     'codejail.django_integration.ConfigureCodeJailMiddleware',
 
+    # django current site middleware with default site
+    'django_sites_extensions.middleware.CurrentSiteWithDefaultMiddleware',
+
     # needs to run after locale middleware (or anything that modifies the request context)
     'edxmako.middleware.MakoMiddleware',
 
@@ -448,7 +457,6 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
 
 
 # Site info
-SITE_ID = 1
 SITE_NAME = "localhost:8001"
 HTTPS = 'on'
 ROOT_URLCONF = 'cms.urls'
@@ -520,6 +528,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
 # List of finder classes that know how to find static files in various locations.
 # Note: the pipeline finder is included to be able to discover optimized files
 STATICFILES_FINDERS = [
+    'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
     'django.contrib.staticfiles.finders.FileSystemFinder',
     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
     'pipeline.finders.PipelineFinder',
diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py
index 64d417be632..966f09b2672 100644
--- a/cms/envs/devstack.py
+++ b/cms/envs/devstack.py
@@ -41,6 +41,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
 
 # Revert to the default set of finders as we don't want the production pipeline
 STATICFILES_FINDERS = [
+    'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
     'django.contrib.staticfiles.finders.FileSystemFinder',
     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
 ]
diff --git a/cms/envs/devstack_optimized.py b/cms/envs/devstack_optimized.py
index 2eaa3da79f0..b737704d380 100644
--- a/cms/envs/devstack_optimized.py
+++ b/cms/envs/devstack_optimized.py
@@ -38,6 +38,6 @@ STATIC_URL = "/static/"
 STATICFILES_FINDERS = (
     'django.contrib.staticfiles.finders.FileSystemFinder',
 )
-STATICFILES_DIRS = (
+STATICFILES_DIRS = [
     (TEST_ROOT / "staticfiles" / "cms").abspath(),
-)
+]
diff --git a/cms/envs/test.py b/cms/envs/test.py
index 554f82cb68c..52f006a76ea 100644
--- a/cms/envs/test.py
+++ b/cms/envs/test.py
@@ -30,6 +30,8 @@ from util.db import NoOpMigrationModules
 from lms.envs.test import (
     WIKI_ENABLED,
     PLATFORM_NAME,
+    SITE_ID,
+    DEFAULT_SITE_ID,
     SITE_NAME,
     DEFAULT_FILE_STORAGE,
     MEDIA_ROOT,
@@ -282,6 +284,8 @@ MICROSITE_CONFIGURATION = {
 MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver'
 MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver'
 
+TEST_THEME = COMMON_ROOT / "test" / "test-theme"
+
 # For consistency in user-experience, keep the value of this setting in sync with
 # the one in lms/envs/test.py
 FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
diff --git a/cms/startup.py b/cms/startup.py
index 6f64dac1619..8bc45723c52 100644
--- a/cms/startup.py
+++ b/cms/startup.py
@@ -17,7 +17,7 @@ from monkey_patch import (
 import xmodule.x_module
 import cms.lib.xblock.runtime
 
-from openedx.core.djangoapps.theming.core import enable_comprehensive_theme
+from openedx.core.djangoapps.theming.core import enable_comprehensive_theming
 
 
 def run():
@@ -30,7 +30,7 @@ def run():
     # Comprehensive theming needs to be set up before django startup,
     # because modifying django template paths after startup has no effect.
     if settings.COMPREHENSIVE_THEME_DIR:
-        enable_comprehensive_theme(settings.COMPREHENSIVE_THEME_DIR)
+        enable_comprehensive_theming(settings.COMPREHENSIVE_THEME_DIR)
 
     django.setup()
 
diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/partials/_variables.scss
similarity index 100%
rename from cms/static/sass/_variables.scss
rename to cms/static/sass/partials/_variables.scss
diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py
index 60196cd9407..844c82c8f2e 100644
--- a/common/djangoapps/course_modes/tests/test_views.py
+++ b/common/djangoapps/course_modes/tests/test_views.py
@@ -22,7 +22,7 @@ from student.tests.factories import CourseEnrollmentFactory, UserFactory
 from student.models import CourseEnrollment
 import lms.djangoapps.commerce.tests.test_utils as ecomm_test_utils
 from course_modes.models import CourseMode, Mode
-from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
+from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
 
 
 @ddt.ddt
@@ -352,7 +352,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
         self.assertEquals(course_modes, expected_modes)
 
     @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
-    @with_is_edx_domain(True)
+    @with_comprehensive_theme("edx.org")
     def test_hide_nav(self):
         # Create the course modes
         for mode in ["honor", "verified"]:
diff --git a/common/djangoapps/edxmako/paths.py b/common/djangoapps/edxmako/paths.py
index 3e7bb40430e..ce0cb81b92b 100644
--- a/common/djangoapps/edxmako/paths.py
+++ b/common/djangoapps/edxmako/paths.py
@@ -9,9 +9,14 @@ import pkg_resources
 
 from django.conf import settings
 from mako.lookup import TemplateLookup
+from mako.exceptions import TopLevelLookupException
 
-from microsite_configuration import microsite
 from . import LOOKUP
+from openedx.core.djangoapps.theming.helpers import (
+    get_template as themed_template,
+    get_template_path_with_theme,
+    strip_site_theme_templates_path,
+)
 
 
 class DynamicTemplateLookup(TemplateLookup):
@@ -49,15 +54,25 @@ class DynamicTemplateLookup(TemplateLookup):
 
     def get_template(self, uri):
         """
-        Overridden method which will hand-off the template lookup to the microsite subsystem
+        Overridden method for locating a template in either the database or the site theme.
+
+        If not found, template lookup will be done in comprehensive theme for current site
+        by prefixing path to theme.
+        e.g if uri is `main.html` then new uri would be something like this `/red-theme/lms/static/main.html`
+
+        If still unable to find a template, it will fallback to the default template directories after stripping off
+        the prefix path to theme.
         """
-        microsite_template = microsite.get_template(uri)
+        template = themed_template(uri)
 
-        return (
-            microsite_template
-            if microsite_template
-            else super(DynamicTemplateLookup, self).get_template(uri)
-        )
+        if not template:
+            try:
+                template = super(DynamicTemplateLookup, self).get_template(get_template_path_with_theme(uri))
+            except TopLevelLookupException:
+                # strip off the prefix path to theme and look in default template dirs
+                template = super(DynamicTemplateLookup, self).get_template(strip_site_theme_templates_path(uri))
+
+        return template
 
 
 def clear_lookups(namespace):
diff --git a/common/djangoapps/edxmako/shortcuts.py b/common/djangoapps/edxmako/shortcuts.py
index da5ddfe3cdd..895420e32b9 100644
--- a/common/djangoapps/edxmako/shortcuts.py
+++ b/common/djangoapps/edxmako/shortcuts.py
@@ -12,16 +12,18 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-from django.template import Context
-from django.http import HttpResponse
 import logging
 
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse
+from django.template import Context
+
 from microsite_configuration import microsite
 
 from edxmako import lookup_template
 from edxmako.middleware import get_template_request_context
-from django.conf import settings
-from django.core.urlresolvers import reverse
+from openedx.core.djangoapps.theming.helpers import get_template_path
 log = logging.getLogger(__name__)
 
 
@@ -113,8 +115,7 @@ def microsite_footer_context_processor(request):
 
 def render_to_string(template_name, dictionary, context=None, namespace='main'):
 
-    # see if there is an override template defined in the microsite
-    template_name = microsite.get_template_path(template_name)
+    template_name = get_template_path(template_name)
 
     context_instance = Context(dictionary)
     # add dictionary to context_instance
diff --git a/common/djangoapps/edxmako/tests.py b/common/djangoapps/edxmako/tests.py
index 12ccb542745..fc31ace82f8 100644
--- a/common/djangoapps/edxmako/tests.py
+++ b/common/djangoapps/edxmako/tests.py
@@ -116,8 +116,12 @@ class MakoMiddlewareTest(TestCase):
         Test render_to_string() when makomiddleware has not initialized
         the threadlocal REQUEST_CONTEXT.context. This is meant to run in LMS.
         """
-        del context_mock.context
-        self.assertIn("this module is temporarily unavailable", render_to_string("courseware/error-message.html", None))
+        with patch("openedx.core.djangoapps.theming.helpers.get_current_site", return_value=None):
+            del context_mock.context
+            self.assertIn(
+                "this module is temporarily unavailable",
+                render_to_string("courseware/error-message.html", None),
+            )
 
     @unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
     @patch("edxmako.middleware.REQUEST_CONTEXT")
@@ -126,8 +130,9 @@ class MakoMiddlewareTest(TestCase):
         Test render_to_string() when makomiddleware has not initialized
         the threadlocal REQUEST_CONTEXT.context. This is meant to run in CMS.
         """
-        del context_mock.context
-        self.assertIn("We're having trouble rendering your component", render_to_string("html_error.html", None))
+        with patch("openedx.core.djangoapps.theming.helpers.get_current_site", return_value=None):
+            del context_mock.context
+            self.assertIn("We're having trouble rendering your component", render_to_string("html_error.html", None))
 
 
 def mako_middleware_process_request(request):
diff --git a/common/djangoapps/microsite_configuration/backends/base.py b/common/djangoapps/microsite_configuration/backends/base.py
index a8fec36caad..43743ee9358 100644
--- a/common/djangoapps/microsite_configuration/backends/base.py
+++ b/common/djangoapps/microsite_configuration/backends/base.py
@@ -11,7 +11,6 @@ BaseMicrositeTemplateBackend is Base Class for the microsite template backend.
 from __future__ import absolute_import
 
 import abc
-import edxmako
 import os.path
 import threading
 
@@ -272,9 +271,7 @@ class BaseMicrositeBackend(AbstractBaseMicrositeBackend):
         Configure the paths for the microsites feature
         """
         microsites_root = settings.MICROSITE_ROOT_DIR
-
         if os.path.isdir(microsites_root):
-            edxmako.paths.add_lookup('main', microsites_root)
             settings.STATICFILES_DIRS.insert(0, microsites_root)
 
             log.info('Loading microsite path at %s', microsites_root)
@@ -292,6 +289,7 @@ class BaseMicrositeBackend(AbstractBaseMicrositeBackend):
         microsites_root = settings.MICROSITE_ROOT_DIR
 
         if self.has_configuration_set():
+            settings.MAKO_TEMPLATES['main'].insert(0, microsites_root)
             settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].append(microsites_root)
 
 
diff --git a/common/djangoapps/microsite_configuration/tests/backends/test_database.py b/common/djangoapps/microsite_configuration/tests/backends/test_database.py
index 43a96cf19d4..d643dfe695e 100644
--- a/common/djangoapps/microsite_configuration/tests/backends/test_database.py
+++ b/common/djangoapps/microsite_configuration/tests/backends/test_database.py
@@ -105,6 +105,23 @@ class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase):
         microsite.clear()
         self.assertIsNone(microsite.get_value('platform_name'))
 
+    def test_enable_microsites_pre_startup(self):
+        """
+        Tests microsite.test_enable_microsites_pre_startup works as expected.
+        """
+        # remove microsite root directory paths first
+        settings.DEFAULT_TEMPLATE_ENGINE['DIRS'] = [
+            path for path in settings.DEFAULT_TEMPLATE_ENGINE['DIRS']
+            if path != settings.MICROSITE_ROOT_DIR
+        ]
+        with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': False}):
+            microsite.enable_microsites_pre_startup(log)
+            self.assertNotIn(settings.MICROSITE_ROOT_DIR, settings.DEFAULT_TEMPLATE_ENGINE['DIRS'])
+        with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': True}):
+            microsite.enable_microsites_pre_startup(log)
+            self.assertIn(settings.MICROSITE_ROOT_DIR, settings.DEFAULT_TEMPLATE_ENGINE['DIRS'])
+            self.assertIn(settings.MICROSITE_ROOT_DIR, settings.MAKO_TEMPLATES['main'])
+
     @patch('edxmako.paths.add_lookup')
     def test_enable_microsites(self, add_lookup):
         """
@@ -122,7 +139,6 @@ class DatabaseMicrositeBackendTests(DatabaseMicrositeTestCase):
         with patch.dict('django.conf.settings.FEATURES', {'USE_MICROSITES': True}):
             microsite.enable_microsites(log)
             self.assertIn(settings.MICROSITE_ROOT_DIR, settings.STATICFILES_DIRS)
-            add_lookup.assert_called_once_with('main', settings.MICROSITE_ROOT_DIR)
 
     def test_get_all_configs(self):
         """
diff --git a/common/djangoapps/pipeline_mako/templates/static_content.html b/common/djangoapps/pipeline_mako/templates/static_content.html
index 20e527f5c98..9a2754f8924 100644
--- a/common/djangoapps/pipeline_mako/templates/static_content.html
+++ b/common/djangoapps/pipeline_mako/templates/static_content.html
@@ -4,7 +4,13 @@ from pipeline_mako import compressed_css, compressed_js
 from django.utils.translation import get_language_bidi
 from mako.exceptions import TemplateLookupException
 
-from openedx.core.djangoapps.theming.helpers import get_page_title_breadcrumbs, get_value, get_template_path, get_themed_template_path, is_request_in_themed_site
+from openedx.core.djangoapps.theming.helpers import (
+  get_page_title_breadcrumbs,
+  get_value,
+  get_template_path,
+  get_themed_template_path,
+  is_request_in_themed_site,
+)
 from certificates.api import get_asset_url_by_slug
 from lang_pref.api import released_languages
 %>
diff --git a/common/djangoapps/student/tests/test_email.py b/common/djangoapps/student/tests/test_email.py
index 84b3adb20cf..6a26d3cfbd9 100644
--- a/common/djangoapps/student/tests/test_email.py
+++ b/common/djangoapps/student/tests/test_email.py
@@ -22,7 +22,7 @@ from edxmako.shortcuts import render_to_string
 from edxmako.tests import mako_middleware_process_request
 from util.request import safe_get_host
 from util.testing import EventTestMixin
-from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
+from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
 
 
 class TestException(Exception):
@@ -100,7 +100,7 @@ class ActivationEmailTests(TestCase):
         self._create_account()
         self._assert_activation_email(self.ACTIVATION_SUBJECT, self.OPENEDX_FRAGMENTS)
 
-    @with_is_edx_domain(True)
+    @with_comprehensive_theme("edx.org")
     def test_activation_email_edx_domain(self):
         self._create_account()
         self._assert_activation_email(self.ACTIVATION_SUBJECT, self.EDX_DOMAIN_FRAGMENTS)
diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py
index 657f0720995..b5aae847c34 100644
--- a/common/djangoapps/student/tests/tests.py
+++ b/common/djangoapps/student/tests/tests.py
@@ -45,7 +45,6 @@ from certificates.tests.factories import GeneratedCertificateFactory  # pylint:
 from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
 import shoppingcart  # pylint: disable=import-error
 from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
-from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
 
 # Explicitly import the cache from ConfigurationModel so we can reset it after each test
 from config_models.models import cache
@@ -493,7 +492,6 @@ class DashboardTest(ModuleStoreTestCase):
             self.assertEquals(response_2.status_code, 200)
 
     @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
-    @with_is_edx_domain(True)
     def test_dashboard_header_nav_has_find_courses(self):
         self.client.login(username="jack", password="test")
         response = self.client.get(reverse("dashboard"))
diff --git a/common/test/db_fixtures/sites.json b/common/test/db_fixtures/sites.json
new file mode 100644
index 00000000000..5a7e8bc11b6
--- /dev/null
+++ b/common/test/db_fixtures/sites.json
@@ -0,0 +1,20 @@
+[
+  {
+    "pk": 2,
+    "model": "sites.Site",
+
+    "fields": {
+      "domain": "localhost:8003",
+      "name": "lms"
+    }
+  },
+  {
+    "pk": 3,
+    "model": "sites.Site",
+
+    "fields": {
+      "domain": "localhost:8031",
+      "name": "cms"
+    }
+  }
+]
diff --git a/common/test/test-theme/cms/static/css/.gitignore b/common/test/test-theme/cms/static/css/.gitignore
new file mode 100644
index 00000000000..b3a52671178
--- /dev/null
+++ b/common/test/test-theme/cms/static/css/.gitignore
@@ -0,0 +1 @@
+*.css
diff --git a/common/test/test-theme/cms/static/sass/partials/_variables.scss b/common/test/test-theme/cms/static/sass/partials/_variables.scss
new file mode 100644
index 00000000000..4265a16fec0
--- /dev/null
+++ b/common/test/test-theme/cms/static/sass/partials/_variables.scss
@@ -0,0 +1,255 @@
+// studio - utilities - variables
+// ====================
+
+// Table of Contents
+// * +Paths
+// * +Grid
+// * +Fonts
+// * +Colors - Utility
+// * +Colors - Primary
+// * +Colors - Shadow
+// * +Color - Application
+// * +Timing
+// * +Archetype UI
+// * +Specific UI
+// * +Deprecated
+
+$baseline: 20px;
+
+// +Paths
+// ====================
+$static-path: '..' !default;
+
+// +Grid
+// ====================
+$gw-column: ($baseline*3);
+$gw-gutter: $baseline;
+$fg-column: $gw-column;
+$fg-gutter: $gw-gutter;
+$fg-max-columns: 12;
+$fg-max-width: 1280px;
+$fg-min-width: 900px;
+
+// +Fonts
+// ====================
+$f-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif;
+$f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
+
+// +Colors - Utility
+// ====================
+$transparent: rgba(0,0,0,0); // used when color value is needed for UI width/transitions but element is transparent
+
+// +Colors - Primary
+// ====================
+$black: rgb(0,0,0);
+$black-t0: rgba($black, 0.125);
+$black-t1: rgba($black, 0.25);
+$black-t2: rgba($black, 0.5);
+$black-t3: rgba($black, 0.75);
+
+$white: rgb(255,255,255);
+$white-t0: rgba($white, 0.125);
+$white-t1: rgba($white, 0.25);
+$white-t2: rgba($white, 0.5);
+$white-t3: rgba($white, 0.75);
+
+$gray: rgb(127,127,127);
+$gray-l1: tint($gray,20%);
+$gray-l2: tint($gray,40%);
+$gray-l3: tint($gray,60%);
+$gray-l4: tint($gray,80%);
+$gray-l5: tint($gray,90%);
+$gray-l6: tint($gray,95%);
+$gray-l7: tint($gray,99%);
+$gray-d1: shade($gray,20%);
+$gray-d2: shade($gray,40%);
+$gray-d3: shade($gray,60%);
+$gray-d4: shade($gray,80%);
+
+$blue: rgb(0, 159, 230);
+$blue-l1: tint($blue,20%);
+$blue-l2: tint($blue,40%);
+$blue-l3: tint($blue,60%);
+$blue-l4: tint($blue,80%);
+$blue-l5: tint($blue,90%);
+$blue-d1: shade($blue,20%);
+$blue-d2: shade($blue,40%);
+$blue-d3: shade($blue,60%);
+$blue-d4: shade($blue,80%);
+$blue-s1: saturate($blue,15%);
+$blue-s2: saturate($blue,30%);
+$blue-s3: saturate($blue,45%);
+$blue-u1: desaturate($blue,15%);
+$blue-u2: desaturate($blue,30%);
+$blue-u3: desaturate($blue,45%);
+$blue-t0: rgba($blue, 0.125);
+$blue-t1: rgba($blue, 0.25);
+$blue-t2: rgba($blue, 0.50);
+$blue-t3: rgba($blue, 0.75);
+
+$pink: rgb(183, 37, 103); // #b72567;
+$pink-l1: tint($pink,20%);
+$pink-l2: tint($pink,40%);
+$pink-l3: tint($pink,60%);
+$pink-l4: tint($pink,80%);
+$pink-l5: tint($pink,90%);
+$pink-d1: shade($pink,20%);
+$pink-d2: shade($pink,40%);
+$pink-d3: shade($pink,60%);
+$pink-d4: shade($pink,80%);
+$pink-s1: saturate($pink,15%);
+$pink-s2: saturate($pink,30%);
+$pink-s3: saturate($pink,45%);
+$pink-u1: desaturate($pink,15%);
+$pink-u2: desaturate($pink,30%);
+$pink-u3: desaturate($pink,45%);
+
+$red: rgb(178, 6, 16);  // #b20610;
+$red-l1: tint($red,20%);
+$red-l2: tint($red,40%);
+$red-l3: tint($red,60%);
+$red-l4: tint($red,80%);
+$red-l5: tint($red,90%);
+$red-d1: shade($red,20%);
+$red-d2: shade($red,40%);
+$red-d3: shade($red,60%);
+$red-d4: shade($red,80%);
+$red-s1: saturate($red,15%);
+$red-s2: saturate($red,30%);
+$red-s3: saturate($red,45%);
+$red-u1: desaturate($red,15%);
+$red-u2: desaturate($red,30%);
+$red-u3: desaturate($red,45%);
+
+$green: rgb(37, 184, 90); // #25b85a
+$green-l1: tint($green,20%);
+$green-l2: tint($green,40%);
+$green-l3: tint($green,60%);
+$green-l4: tint($green,80%);
+$green-l5: tint($green,90%);
+$green-d1: shade($green,20%);
+$green-d2: shade($green,40%);
+$green-d3: shade($green,60%);
+$green-d4: shade($green,80%);
+$green-s1: saturate($green,15%);
+$green-s2: saturate($green,30%);
+$green-s3: saturate($green,45%);
+$green-u1: desaturate($green,15%);
+$green-u2: desaturate($green,30%);
+$green-u3: desaturate($green,45%);
+
+$yellow: rgb(237, 189, 60);
+$yellow-l1: tint($yellow,20%);
+$yellow-l2: tint($yellow,40%);
+$yellow-l3: tint($yellow,60%);
+$yellow-l4: tint($yellow,80%);
+$yellow-l5: tint($yellow,90%);
+$yellow-d1: shade($yellow,20%);
+$yellow-d2: shade($yellow,40%);
+$yellow-d3: shade($yellow,60%);
+$yellow-d4: shade($yellow,80%);
+$yellow-s1: saturate($yellow,15%);
+$yellow-s2: saturate($yellow,30%);
+$yellow-s3: saturate($yellow,45%);
+$yellow-u1: desaturate($yellow,15%);
+$yellow-u2: desaturate($yellow,30%);
+$yellow-u3: desaturate($yellow,45%);
+
+$orange: rgb(237, 189, 60);
+$orange-l1: tint($orange,20%);
+$orange-l2: tint($orange,40%);
+$orange-l3: tint($orange,60%);
+$orange-l4: tint($orange,80%);
+$orange-l5: tint($orange,90%);
+$orange-d1: shade($orange,20%);
+$orange-d2: shade($orange,40%);
+$orange-d3: shade($orange,60%);
+$orange-d4: shade($orange,80%);
+$orange-s1: saturate($orange,15%);
+$orange-s2: saturate($orange,30%);
+$orange-s3: saturate($orange,45%);
+$orange-u1: desaturate($orange,15%);
+$orange-u2: desaturate($orange,30%);
+$orange-u3: desaturate($orange,45%);
+
+// +Colors - Shadows
+// ====================
+$shadow: rgba($black, 0.2);
+$shadow-l1: rgba($black, 0.1);
+$shadow-l2: rgba($black, 0.05);
+$shadow-d1: rgba($black, 0.4);
+$shadow-d2: rgba($black, 0.6);
+
+// +Colors - Application
+// ====================
+$color-draft: $gray-l3;
+$color-live: $blue;
+$color-ready: $green;
+$color-warning: $orange-l2;
+$color-error: $red-l2;
+$color-staff-only: $black;
+$color-gated: $black;
+$color-visibility-set: $black;
+
+$color-heading-base: $gray-d2;
+$color-copy-base: $gray-l1;
+$color-copy-emphasized: $gray-d2;
+
+// +Timing
+// ====================
+// used for animation/transition mixin syncing
+$tmg-s3: 3.0s;
+$tmg-s2: 2.0s;
+$tmg-s1: 1.0s;
+$tmg-avg: 0.75s;
+$tmg-f1: 0.50s;
+$tmg-f2: 0.25s;
+$tmg-f3: 0.125s;
+
+// +Archetype UI
+// ====================
+$ui-action-primary-color: $blue-u2;
+$ui-action-primary-color-focus: $blue-s1;
+
+$ui-link-color: $blue-u2;
+$ui-link-color-focus: $blue-s1;
+
+// +Specific UI
+// ====================
+$ui-notification-height: ($baseline*10);
+$ui-update-color: $blue-l4;
+
+// +Deprecated
+// ====================
+// do not use, future clean up will use updated styles
+$baseFontColor: $gray-d2;
+$lighter-base-font-color: rgb(100,100,100);
+$offBlack: #3c3c3c;
+$green: #108614;
+$lightGrey: #edf1f5;
+$mediumGrey: #b0b6c2;
+$darkGrey: #8891a1;
+$extraDarkGrey: #3d4043;
+$paleYellow: #fffcf1;
+$yellow: rgb(255, 254, 223);
+$green: rgb(37, 184, 90);
+$brightGreen: rgb(22, 202, 87);
+$disabledGreen: rgb(124, 206, 153);
+$darkGreen: rgb(52, 133, 76);
+
+// These colors are updated for testing purposes
+$lightBluishGrey: rgb(0, 250, 0);
+$lightBluishGrey2: rgb(0, 250, 0);
+$error-red: rgb(253, 87, 87);
+
+
+//carryover from LMS for xmodules
+$sidebar-color: rgb(246, 246, 246);
+
+// type
+$sans-serif: $f-sans-serif;
+$body-line-height: golden-ratio(.875em, 1);
+
+// carried over from LMS for xmodules
+$action-primary-active-bg: #1AA1DE; // $m-blue
+$very-light-text: $white;
diff --git a/common/test/test-theme/cms/templates/login.html b/common/test/test-theme/cms/templates/login.html
new file mode 100644
index 00000000000..30757bfe11e
--- /dev/null
+++ b/common/test/test-theme/cms/templates/login.html
@@ -0,0 +1,55 @@
+<%inherit file="base.html" />
+<%def name="online_help_token()"><% return "login" %></%def>
+<%!
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext as _
+%>
+<%block name="title">${_("Sign In")}</%block>
+<%block name="bodyclass">not-signedin view-signin</%block>
+
+<%block name="content">
+<div class="wrapper-content wrapper">
+  <section class="content">
+    <header>
+      <h1 class="title title-1">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</h1>
+      <a href="${reverse('signup')}" class="action action-signin">${_("Don't have a {studio_name} Account? Sign up!").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
+    </header>
+    <!-- Login Page override for test-theme. -->
+    <article class="content-primary" role="main">
+      <form id="login_form" method="post" action="login_post" onsubmit="return false;">
+
+        <fieldset>
+          <legend class="sr">${_("Required Information to Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</legend>
+          <input type="hidden" name="csrfmiddlewaretoken" value="${ csrf }" />
+
+          <ol class="list-input">
+            <li class="field text required" id="field-email">
+              <label for="email">${_("E-mail")}</label>
+              <input id="email" type="email" name="email" placeholder="${_('example: username@domain.com')}"/>
+            </li>
+
+            <li class="field text required" id="field-password">
+              <label for="password">${_("Password")}</label>
+              <input id="password" type="password" name="password" />
+              <a href="${forgot_password_link}" class="action action-forgotpassword">${_("Forgot password?")}</a>
+            </li>
+          </ol>
+        </fieldset>
+
+        <div class="form-actions">
+          <button type="submit" id="submit" name="submit" class="action action-primary">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</button>
+        </div>
+
+        <!-- no honor code for CMS, but need it because we're using the lms student object -->
+        <input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
+      </form>
+    </article>
+  </section>
+</div>
+</%block>
+
+<%block name="requirejs">
+  require(["js/factories/login"], function(LoginFactory) {
+      LoginFactory("${reverse('homepage')}");
+  });
+</%block>
diff --git a/common/test/test-theme/lms/static/css/.gitignore b/common/test/test-theme/lms/static/css/.gitignore
new file mode 100644
index 00000000000..b3a52671178
--- /dev/null
+++ b/common/test/test-theme/lms/static/css/.gitignore
@@ -0,0 +1 @@
+*.css
diff --git a/common/test/test-theme/lms/static/images/logo.png b/common/test/test-theme/lms/static/images/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..5efc6b63a436b749addf0c7a978f9cb78ba03eab
GIT binary patch
literal 493
zcmV<J0TTX+P)<h;3K|Lk000e1NJLTq002k;001xu1^@s6vYb5Y0005BNkl<Zc-rmS
zp^m~p6op|RHFbamheV)RU62S8EC#g!48b8WSOo8Y<t-3A0uO-0ku*T=o+cFyVP+|%
z?al7F$!AzI`9s)FX9^q+hr{9gqq!6uXu>ya;Rt@{2;1;)O>oj)&R5}5gerKjf&*Ma
zhww8Gs!&WbOdVDrF|DAULKqiDATf=={bZO5Y(Ns)K;;8r9GHP5Gy^AT91rBfyaaI?
zARpFvH_jX+CG$7p^gvS5iyLPNlCoutI75(>4PT8j0m;bZg*aW1jC6mC(+0^{+Y+Y+
zlChd8jti0#_eq=*NKQ)60xfb3w0II|kYk|1W1KQb&dLwZI+8Od8fdH_&^QvPqaaWx
z8tAGZ&@~dsR}jdL?y*FPdbr0DCEOrTUs0ew7S&P|s1*cSD+;v6qNa)hO@lytMS=EM
z)J{>LT@dK3D9{;;Iw=Zt3Ig2~1-fHVH${PN{07;b_#0%m<9{)`DW8MvT6~hWbGa8Y
z^SPV1>+pS$ox?YZW&^&Hwkz>e!>q=0F|#gDR?UVyn>Xw6c86Jm_k%3=gLuQtaKnsu
j6is&&IUEj$^LOS4BtH-m2H0bh00000NkvXXu0mjf=WNR>

literal 0
HcmV?d00001

diff --git a/common/test/test-theme/lms/static/sass/partials/base/_variables.scss b/common/test/test-theme/lms/static/sass/partials/base/_variables.scss
new file mode 100755
index 00000000000..43f66799a0f
--- /dev/null
+++ b/common/test/test-theme/lms/static/sass/partials/base/_variables.scss
@@ -0,0 +1,5 @@
+@import 'lms/static/sass/partials/base/variables';
+
+$header-bg: rgb(0,250,0);
+$footer-bg: rgb(0,250,0);
+$container-bg: rgb(0,250,0);
diff --git a/common/test/test-theme/lms/templates/footer.html b/common/test/test-theme/lms/templates/footer.html
new file mode 100644
index 00000000000..c9e73c01176
--- /dev/null
+++ b/common/test/test-theme/lms/templates/footer.html
@@ -0,0 +1,10 @@
+<div class="wrapper wrapper-footer">
+  <footer>
+    <div class="colophon">
+      <div class="colophon-about">
+        <p>This is a footer for test-theme.</p>
+      </div>
+    </div>
+
+  </footer>
+</div>
diff --git a/lms/djangoapps/branding/tests/test_views.py b/lms/djangoapps/branding/tests/test_views.py
index a6cb5d72495..c6d4ee8340c 100644
--- a/lms/djangoapps/branding/tests/test_views.py
+++ b/lms/djangoapps/branding/tests/test_views.py
@@ -10,7 +10,7 @@ import mock
 import ddt
 from config_models.models import cache
 from branding.models import BrandingApiConfig
-from openedx.core.djangoapps.theming.test_util import with_edx_domain_context
+from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme_context
 
 
 @ddt.ddt
@@ -30,19 +30,19 @@ class TestFooter(TestCase):
 
     @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"),
+        (None, "application/json", "application/json; charset=utf-8", "Open edX"),
+        (None, "text/html", "text/html; charset=utf-8", "lms-footer.css"),
+        (None, "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"),
+        ("edx.org", "application/json", "application/json; charset=utf-8", "edX Inc"),
+        ("edx.org", "text/html", "text/html; charset=utf-8", "lms-footer-edx.css"),
+        ("edx.org", "text/html", "text/html; charset=utf-8", "edX Inc"),
     )
     @ddt.unpack
-    def test_footer_content_types(self, is_edx_domain, accepts, content_type, content):
+    def test_footer_content_types(self, theme, accepts, content_type, content):
         self._set_feature_flag(True)
-        with with_edx_domain_context(is_edx_domain):
+        with with_comprehensive_theme_context(theme):
             resp = self._get_footer(accepts=accepts)
 
         self.assertEqual(resp.status_code, 200)
@@ -50,10 +50,10 @@ class TestFooter(TestCase):
         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):
+    @ddt.data("edx.org", None)
+    def test_footer_json(self, theme):
         self._set_feature_flag(True)
-        with with_edx_domain_context(is_edx_domain):
+        with with_comprehensive_theme_context(theme):
             resp = self._get_footer()
 
         self.assertEqual(resp.status_code, 200)
@@ -142,18 +142,18 @@ class TestFooter(TestCase):
 
     @ddt.data(
         # OpenEdX
-        (False, "en", "lms-footer.css"),
-        (False, "ar", "lms-footer-rtl.css"),
+        (None, "en", "lms-footer.css"),
+        (None, "ar", "lms-footer-rtl.css"),
 
         # EdX.org
-        (True, "en", "lms-footer-edx.css"),
-        (True, "ar", "lms-footer-edx-rtl.css"),
+        ("edx.org", "en", "lms-footer-edx.css"),
+        ("edx.org", "ar", "lms-footer-edx-rtl.css"),
     )
     @ddt.unpack
-    def test_language_rtl(self, is_edx_domain, language, static_path):
+    def test_language_rtl(self, theme, language, static_path):
         self._set_feature_flag(True)
 
-        with with_edx_domain_context(is_edx_domain):
+        with with_comprehensive_theme_context(theme):
             resp = self._get_footer(accepts="text/html", params={'language': language})
 
         self.assertEqual(resp.status_code, 200)
@@ -161,18 +161,18 @@ class TestFooter(TestCase):
 
     @ddt.data(
         # OpenEdX
-        (False, True),
-        (False, False),
+        (None, True),
+        (None, False),
 
         # EdX.org
-        (True, True),
-        (True, False),
+        ("edx.org", True),
+        ("edx.org", False),
     )
     @ddt.unpack
-    def test_show_openedx_logo(self, is_edx_domain, show_logo):
+    def test_show_openedx_logo(self, theme, show_logo):
         self._set_feature_flag(True)
 
-        with with_edx_domain_context(is_edx_domain):
+        with with_comprehensive_theme_context(theme):
             params = {'show-openedx-logo': 1} if show_logo else {}
             resp = self._get_footer(accepts="text/html", params=params)
 
@@ -185,17 +185,17 @@ class TestFooter(TestCase):
 
     @ddt.data(
         # OpenEdX
-        (False, False),
-        (False, True),
+        (None, False),
+        (None, True),
 
         # EdX.org
-        (True, False),
-        (True, True),
+        ("edx.org", False),
+        ("edx.org", True),
     )
     @ddt.unpack
-    def test_include_dependencies(self, is_edx_domain, include_dependencies):
+    def test_include_dependencies(self, theme, include_dependencies):
         self._set_feature_flag(True)
-        with with_edx_domain_context(is_edx_domain):
+        with with_comprehensive_theme_context(theme):
             params = {'include-dependencies': 1} if include_dependencies else {}
             resp = self._get_footer(accepts="text/html", params=params)
 
diff --git a/lms/djangoapps/commerce/tests/test_views.py b/lms/djangoapps/commerce/tests/test_views.py
index 59eadcb9e5f..1e5cd24f2a2 100644
--- a/lms/djangoapps/commerce/tests/test_views.py
+++ b/lms/djangoapps/commerce/tests/test_views.py
@@ -4,13 +4,12 @@ from uuid import uuid4
 from nose.plugins.attrib import attr
 
 import ddt
-from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.test import TestCase
 import mock
 
 from student.tests.factories import UserFactory
-from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
+from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
 
 
 class UserMixin(object):
@@ -87,7 +86,7 @@ class ReceiptViewTests(UserMixin, TestCase):
         self.assertRegexpMatches(response.content, user_message if is_user_message_expected else system_message)
         self.assertNotRegexpMatches(response.content, user_message if not is_user_message_expected else system_message)
 
-    @with_is_edx_domain(True)
+    @with_comprehensive_theme("edx.org")
     def test_hide_nav_header(self):
         self._login()
         post_data = {'decision': 'ACCEPT', 'reason_code': '200', 'signed_field_names': 'dummy'}
diff --git a/lms/djangoapps/course_wiki/tests/test_comprehensive_theming.py b/lms/djangoapps/course_wiki/tests/test_comprehensive_theming.py
index f763510efb5..3042809e6d7 100644
--- a/lms/djangoapps/course_wiki/tests/test_comprehensive_theming.py
+++ b/lms/djangoapps/course_wiki/tests/test_comprehensive_theming.py
@@ -1,7 +1,6 @@
 """
 Tests for wiki middleware.
 """
-from django.conf import settings
 from django.test.client import Client
 from nose.plugins.attrib import attr
 from wiki.models import URLPath
@@ -33,7 +32,7 @@ class TestComprehensiveTheming(ModuleStoreTestCase):
         self.client = Client()
         self.client.login(username='instructor', password='secret')
 
-    @with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
+    @with_comprehensive_theme('red-theme')
     def test_themed_footer(self):
         """
         Tests that theme footer is used rather than standard
diff --git a/lms/djangoapps/course_wiki/views.py b/lms/djangoapps/course_wiki/views.py
index 06dfc63bcaf..1913454e8c6 100644
--- a/lms/djangoapps/course_wiki/views.py
+++ b/lms/djangoapps/course_wiki/views.py
@@ -6,8 +6,6 @@ import re
 import cgi
 
 from django.conf import settings
-from django.contrib.sites.models import Site
-from django.core.exceptions import ImproperlyConfigured
 from django.shortcuts import redirect
 from django.utils.translation import ugettext as _
 
@@ -50,18 +48,6 @@ def course_wiki_redirect(request, course_id):  # pylint: disable=unused-argument
     if not valid_slug:
         return redirect("wiki:get", path="")
 
-    # The wiki needs a Site object created. We make sure it exists here
-    try:
-        Site.objects.get_current()
-    except Site.DoesNotExist:
-        new_site = Site()
-        new_site.domain = settings.SITE_NAME
-        new_site.name = "edX"
-        new_site.save()
-        site_id = str(new_site.id)
-        if site_id != str(settings.SITE_ID):
-            raise ImproperlyConfigured("No site object was created and the SITE_ID doesn't match the newly created one. {} != {}".format(site_id, settings.SITE_ID))
-
     try:
         urlpath = URLPath.get_by_path(course_slug, select_related=True)
 
diff --git a/lms/djangoapps/courseware/tests/test_comprehensive_theming.py b/lms/djangoapps/courseware/tests/test_comprehensive_theming.py
index 752eb810301..54ef5ff84fd 100644
--- a/lms/djangoapps/courseware/tests/test_comprehensive_theming.py
+++ b/lms/djangoapps/courseware/tests/test_comprehensive_theming.py
@@ -6,21 +6,33 @@ from django.test import TestCase
 from path import path           # pylint: disable=no-name-in-module
 from django.contrib import staticfiles
 
+from paver.easy import call_task
+
 from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
-from openedx.core.lib.tempdir import mkdtemp_clean
+from openedx.core.lib.tempdir import mkdtemp_clean, create_symlink, delete_symlink
 
 
 class TestComprehensiveTheming(TestCase):
     """Test comprehensive theming."""
 
+    @classmethod
+    def setUpClass(cls):
+        compile_sass('lms')
+        super(TestComprehensiveTheming, cls).setUpClass()
+
     def setUp(self):
         super(TestComprehensiveTheming, self).setUp()
 
         # Clear the internal staticfiles caches, to get test isolation.
         staticfiles.finders.get_finder.cache_clear()
 
-    @with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
+    @with_comprehensive_theme('red-theme')
     def test_red_footer(self):
+        """
+        Tests templates from theme are rendered if available.
+        `red-theme` has header.html and footer.html so this test
+        asserts presence of the content from header.html and footer.html
+        """
         resp = self.client.get('/')
         self.assertEqual(resp.status_code, 200)
         # This string comes from footer.html
@@ -34,12 +46,16 @@ class TestComprehensiveTheming(TestCase):
         # of test.
 
         # Make a temp directory as a theme.
-        tmp_theme = path(mkdtemp_clean())
-        template_dir = tmp_theme / "lms/templates"
+        themes_dir = path(mkdtemp_clean())
+        tmp_theme = "temp_theme"
+        template_dir = themes_dir / tmp_theme / "lms/templates"
         template_dir.makedirs()
         with open(template_dir / "footer.html", "w") as footer:
             footer.write("<footer>TEMPORARY THEME</footer>")
 
+        dest_path = path(settings.COMPREHENSIVE_THEME_DIR) / tmp_theme
+        create_symlink(themes_dir / tmp_theme, dest_path)
+
         @with_comprehensive_theme(tmp_theme)
         def do_the_test(self):
             """A function to do the work so we can use the decorator."""
@@ -48,18 +64,22 @@ class TestComprehensiveTheming(TestCase):
             self.assertContains(resp, "TEMPORARY THEME")
 
         do_the_test(self)
+        # remove symlinks before running subsequent tests
+        delete_symlink(dest_path)
 
     def test_theme_adjusts_staticfiles_search_path(self):
-        # Test that a theme adds itself to the staticfiles search path.
+        """
+        Tests theme directories are added to  staticfiles search path.
+        """
         before_finders = list(settings.STATICFILES_FINDERS)
         before_dirs = list(settings.STATICFILES_DIRS)
 
-        @with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
+        @with_comprehensive_theme('red-theme')
         def do_the_test(self):
             """A function to do the work so we can use the decorator."""
             self.assertEqual(list(settings.STATICFILES_FINDERS), before_finders)
-            self.assertEqual(settings.STATICFILES_DIRS[0], settings.REPO_ROOT / 'themes/red-theme/lms/static')
-            self.assertEqual(settings.STATICFILES_DIRS[1:], before_dirs)
+            self.assertIn(settings.REPO_ROOT / 'themes/red-theme/lms/static', settings.STATICFILES_DIRS)
+            self.assertEqual(settings.STATICFILES_DIRS, before_dirs)
 
         do_the_test(self)
 
@@ -67,9 +87,9 @@ class TestComprehensiveTheming(TestCase):
         result = staticfiles.finders.find('images/logo.png')
         self.assertEqual(result, settings.REPO_ROOT / 'lms/static/images/logo.png')
 
-    @with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
+    @with_comprehensive_theme('red-theme')
     def test_overridden_logo_image(self):
-        result = staticfiles.finders.find('images/logo.png')
+        result = staticfiles.finders.find('red-theme/images/logo.png')
         self.assertEqual(result, settings.REPO_ROOT / 'themes/red-theme/lms/static/images/logo.png')
 
     def test_default_favicon(self):
@@ -79,10 +99,54 @@ class TestComprehensiveTheming(TestCase):
         result = staticfiles.finders.find('images/favicon.ico')
         self.assertEqual(result, settings.REPO_ROOT / 'lms/static/images/favicon.ico')
 
-    @with_comprehensive_theme(settings.REPO_ROOT / 'themes/red-theme')
+    @with_comprehensive_theme('red-theme')
+    def test_css(self):
+        """
+        Test that static files finders are adjusted according to the applied comprehensive theme.
+        """
+        result = staticfiles.finders.find('red-theme/css/lms-main.css')
+        self.assertEqual(result, settings.REPO_ROOT / "themes/red-theme/lms/static/css/lms-main.css")
+
+        lms_main_css = ""
+        with open(result) as css_file:
+            lms_main_css += css_file.read()
+
+        self.assertIn("background:#fa0000", lms_main_css)
+
+    def test_default_css(self):
+        """
+        Test default css is served if no theme is applied
+        """
+        result = staticfiles.finders.find('css/lms-main.css')
+        self.assertEqual(result, settings.REPO_ROOT / "lms/static/css/lms-main.css")
+
+        lms_main_css = ""
+        with open(result) as css_file:
+            lms_main_css += css_file.read()
+
+        self.assertNotIn("background:#00fa00", lms_main_css)
+
+    @with_comprehensive_theme('red-theme')
     def test_overridden_favicon(self):
         """
         Test comprehensive theme override on favicon image.
         """
-        result = staticfiles.finders.find('images/favicon.ico')
+        result = staticfiles.finders.find('red-theme/images/favicon.ico')
         self.assertEqual(result, settings.REPO_ROOT / 'themes/red-theme/lms/static/images/favicon.ico')
+
+
+def compile_sass(system):
+    """
+    Process xmodule assets and compile sass files.
+
+    :param system - 'lms' or 'cms', specified the system to compile sass for.
+    """
+    # Compile system sass files
+    call_task(
+        'pavelib.assets.update_assets',
+        args=(
+            system,
+            "--themes_dir={themes_dir}".format(themes_dir=settings.COMPREHENSIVE_THEME_DIR),
+            "--themes=red-theme",
+            "--settings=test"),
+    )
diff --git a/lms/djangoapps/courseware/tests/test_course_info.py b/lms/djangoapps/courseware/tests/test_course_info.py
index 952e1bc21f3..f71db15908d 100644
--- a/lms/djangoapps/courseware/tests/test_course_info.py
+++ b/lms/djangoapps/courseware/tests/test_course_info.py
@@ -265,7 +265,8 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
         url = reverse('info', args=[unicode(course.id)])
         with self.assertNumQueries(sql_queries):
             with check_mongo_calls(mongo_queries):
-                resp = self.client.get(url)
+                with mock.patch("openedx.core.djangoapps.theming.helpers.get_current_site", return_value=None):
+                    resp = self.client.get(url)
         self.assertEqual(resp.status_code, 200)
 
     def test_num_queries_instructor_paced(self):
diff --git a/lms/djangoapps/courseware/tests/test_footer.py b/lms/djangoapps/courseware/tests/test_footer.py
index 0a14ee18604..c1db2a77bea 100644
--- a/lms/djangoapps/courseware/tests/test_footer.py
+++ b/lms/djangoapps/courseware/tests/test_footer.py
@@ -9,7 +9,7 @@ from django.conf import settings
 from django.test import TestCase
 from django.test.utils import override_settings
 
-from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
+from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
 
 
 @attr('shard_1')
@@ -37,7 +37,7 @@ class TestFooter(TestCase):
         "youtube": "https://www.youtube.com/"
     }
 
-    @with_is_edx_domain(True)
+    @with_comprehensive_theme("edx.org")
     def test_edx_footer(self):
         """
         Verify that the homepage, when accessed at edx.org, has the edX footer
@@ -46,7 +46,6 @@ class TestFooter(TestCase):
         self.assertEqual(resp.status_code, 200)
         self.assertContains(resp, 'footer-edx-v3')
 
-    @with_is_edx_domain(False)
     def test_openedx_footer(self):
         """
         Verify that the homepage, when accessed at something other than
@@ -56,7 +55,7 @@ class TestFooter(TestCase):
         self.assertEqual(resp.status_code, 200)
         self.assertContains(resp, 'footer-openedx')
 
-    @with_is_edx_domain(True)
+    @with_comprehensive_theme("edx.org")
     @override_settings(
         SOCIAL_MEDIA_FOOTER_NAMES=SOCIAL_MEDIA_NAMES,
         SOCIAL_MEDIA_FOOTER_URLS=SOCIAL_MEDIA_URLS
diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py
index 585233cb939..b9376b75c56 100644
--- a/lms/djangoapps/student_account/test/test_views.py
+++ b/lms/djangoapps/student_account/test/test_views.py
@@ -26,7 +26,7 @@ from student_account.views import account_settings_context
 from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin
 from util.testing import UrlResetMixin
 from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
-from openedx.core.djangoapps.theming.test_util import with_edx_domain_context
+from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme_context
 
 
 @ddt.ddt
@@ -241,13 +241,13 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
         self.assertRedirects(response, reverse("dashboard"))
 
     @ddt.data(
-        (False, "signin_user"),
-        (False, "register_user"),
-        (True, "signin_user"),
-        (True, "register_user"),
+        (None, "signin_user"),
+        (None, "register_user"),
+        ("edx.org", "signin_user"),
+        ("edx.org", "register_user"),
     )
     @ddt.unpack
-    def test_login_and_registration_form_signin_preserves_params(self, is_edx_domain, url_name):
+    def test_login_and_registration_form_signin_preserves_params(self, theme, url_name):
         params = [
             ('course_id', 'edX/DemoX/Demo_Course'),
             ('enrollment_action', 'enroll'),
@@ -255,7 +255,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
 
         # The response should have a "Sign In" button with the URL
         # that preserves the querystring params
-        with with_edx_domain_context(is_edx_domain):
+        with with_comprehensive_theme_context(theme):
             response = self.client.get(reverse(url_name), params)
 
         expected_url = '/login?{}'.format(self._finish_auth_url_param(params + [('next', '/dashboard')]))
@@ -271,7 +271,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
         ]
 
         # Verify that this parameter is also preserved
-        with with_edx_domain_context(is_edx_domain):
+        with with_comprehensive_theme_context(theme):
             response = self.client.get(reverse(url_name), params)
 
         expected_url = '/login?{}'.format(self._finish_auth_url_param(params))
diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py
index 7acc7194c51..28430f7d5f9 100644
--- a/lms/djangoapps/verify_student/tests/test_views.py
+++ b/lms/djangoapps/verify_student/tests/test_views.py
@@ -38,7 +38,7 @@ from common.test.utils import XssTestMixin
 from commerce.tests import TEST_PAYMENT_DATA, TEST_API_URL, TEST_API_SIGNING_KEY
 from embargo.test_utils import restrict_course
 from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
-from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
+from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
 from shoppingcart.models import Order, CertificateItem
 from student.tests.factories import UserFactory, CourseEnrollmentFactory
 from student.models import CourseEnrollment
@@ -290,7 +290,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
         )
         self._assert_redirects_to_dashboard(response)
 
-    @with_is_edx_domain(True)
+    @with_comprehensive_theme("edx.org")
     @ddt.data("verify_student_start_flow", "verify_student_begin_flow")
     def test_pay_and_verify_hides_header_nav(self, payment_flow):
         course = self._create_course("verified")
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index f3fa7a5d3dc..8727c445b15 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -244,6 +244,7 @@ BULK_EMAIL_ROUTING_KEY_SMALL_JOBS = LOW_PRIORITY_QUEUE
 # Theme overrides
 THEME_NAME = ENV_TOKENS.get('THEME_NAME', None)
 COMPREHENSIVE_THEME_DIR = path(ENV_TOKENS.get('COMPREHENSIVE_THEME_DIR', COMPREHENSIVE_THEME_DIR))
+THEME_CACHE_TIMEOUT = ENV_TOKENS.get('THEME_CACHE_TIMEOUT', THEME_CACHE_TIMEOUT)
 
 # Marketing link overrides
 MKTG_URL_LINK_MAP.update(ENV_TOKENS.get('MKTG_URL_LINK_MAP', {}))
diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py
index d461d4bbf31..d4d8ff2be40 100644
--- a/lms/envs/bok_choy.py
+++ b/lms/envs/bok_choy.py
@@ -61,9 +61,9 @@ STATIC_URL = "/static/"
 STATICFILES_FINDERS = (
     'django.contrib.staticfiles.finders.FileSystemFinder',
 )
-STATICFILES_DIRS = (
+STATICFILES_DIRS = [
     (TEST_ROOT / "staticfiles" / "lms").abspath(),
-)
+]
 
 DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
 MEDIA_ROOT = TEST_ROOT / "uploads"
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 1849c5af872..bce4e34aac9 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -397,7 +397,7 @@ COURSES_ROOT = ENV_ROOT / "data"
 DATA_DIR = COURSES_ROOT
 
 # comprehensive theming system
-COMPREHENSIVE_THEME_DIR = ""
+COMPREHENSIVE_THEME_DIR = REPO_ROOT / "themes"
 
 # TODO: Remove the rest of the sys.path modification here and in cms/envs/common.py
 sys.path.append(REPO_ROOT)
@@ -486,6 +486,7 @@ TEMPLATES = [
             'loaders': [
                 # We have to use mako-aware template loaders to be able to include
                 # mako templates inside django templates (such as main_django.html).
+                'openedx.core.djangoapps.theming.template_loaders.ThemeFilesystemLoader',
                 'edxmako.makoloader.MakoFilesystemLoader',
                 'edxmako.makoloader.MakoAppDirectoriesLoader',
             ],
@@ -785,7 +786,6 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
 CMS_BASE = 'localhost:8001'
 
 # Site info
-SITE_ID = 1
 SITE_NAME = "example.com"
 HTTPS = 'on'
 ROOT_URLCONF = 'lms.urls'
@@ -1145,6 +1145,10 @@ MIDDLEWARE_CLASSES = (
 
     # catches any uncaught RateLimitExceptions and returns a 403 instead of a 500
     'ratelimitbackend.middleware.RateLimitMiddleware',
+
+    # django current site middleware with default site
+    'django_sites_extensions.middleware.CurrentSiteWithDefaultMiddleware',
+
     # needs to run after locale middleware (or anything that modifies the request context)
     'edxmako.middleware.MakoMiddleware',
 
@@ -1178,6 +1182,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.ProductionStorage'
 # List of finder classes that know how to find static files in various locations.
 # Note: the pipeline finder is included to be able to discover optimized files
 STATICFILES_FINDERS = [
+    'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
     'django.contrib.staticfiles.finders.FileSystemFinder',
     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
     'pipeline.finders.PipelineFinder',
@@ -2817,3 +2822,12 @@ AUDIT_CERT_CUTOFF_DATE = None
 
 CREDENTIALS_SERVICE_USERNAME = 'credentials_service_user'
 CREDENTIALS_GENERATION_ROUTING_KEY = HIGH_PRIORITY_QUEUE
+
+WIKI_REQUEST_CACHE_MIDDLEWARE_CLASS = "request_cache.middleware.RequestCache"
+
+# Dafault site id to use in case there is no site that matches with the request headers.
+DEFAULT_SITE_ID = 1
+
+# Cache time out settings
+# by Comprehensive Theme system
+THEME_CACHE_TIMEOUT = 30 * 60
diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py
index e8ef6cd0a59..350ccafe33d 100644
--- a/lms/envs/devstack.py
+++ b/lms/envs/devstack.py
@@ -99,6 +99,7 @@ STATICFILES_STORAGE = 'openedx.core.storage.DevelopmentStorage'
 
 # Revert to the default set of finders as we don't want the production pipeline
 STATICFILES_FINDERS = [
+    'openedx.core.djangoapps.theming.finders.ThemeFilesFinder',
     'django.contrib.staticfiles.finders.FileSystemFinder',
     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
 ]
diff --git a/lms/envs/devstack_optimized.py b/lms/envs/devstack_optimized.py
index 251ed4f3437..e0210ef6dbf 100644
--- a/lms/envs/devstack_optimized.py
+++ b/lms/envs/devstack_optimized.py
@@ -38,6 +38,6 @@ STATIC_URL = "/static/"
 STATICFILES_FINDERS = (
     'django.contrib.staticfiles.finders.FileSystemFinder',
 )
-STATICFILES_DIRS = (
+STATICFILES_DIRS = [
     (TEST_ROOT / "staticfiles" / "lms").abspath(),
-)
+]
diff --git a/lms/envs/test.py b/lms/envs/test.py
index 505d32958ff..886cc1ec4e8 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -428,6 +428,9 @@ openid.oidutil.log = lambda message, level=0: None
 PLATFORM_NAME = "edX"
 SITE_NAME = "edx.org"
 
+# use default site for tests
+SITE_ID = 1
+
 # set up some testing for microsites
 FEATURES['USE_MICROSITES'] = True
 MICROSITE_ROOT_DIR = COMMON_ROOT / 'test' / 'test_microsites'
@@ -497,6 +500,8 @@ MICROSITE_CONFIGURATION = {
 MICROSITE_TEST_HOSTNAME = 'testmicrosite.testserver'
 MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver'
 
+TEST_THEME = COMMON_ROOT / "test" / "test-theme"
+
 # add extra template directory for test-only templates
 MAKO_TEMPLATES['main'].extend([
     COMMON_ROOT / 'test' / 'templates',
diff --git a/lms/startup.py b/lms/startup.py
index b6e63c32320..2762e900ab1 100644
--- a/lms/startup.py
+++ b/lms/startup.py
@@ -20,7 +20,7 @@ from monkey_patch import (
 import xmodule.x_module
 import lms_xblock.runtime
 
-from openedx.core.djangoapps.theming.core import enable_comprehensive_theme
+from openedx.core.djangoapps.theming.core import enable_comprehensive_theming
 from microsite_configuration import microsite
 
 log = logging.getLogger(__name__)
@@ -40,7 +40,7 @@ def run():
     # Comprehensive theming needs to be set up before django startup,
     # because modifying django template paths after startup has no effect.
     if settings.COMPREHENSIVE_THEME_DIR:
-        enable_comprehensive_theme(settings.COMPREHENSIVE_THEME_DIR)
+        enable_comprehensive_theming(settings.COMPREHENSIVE_THEME_DIR)
 
     # We currently use 2 template rendering engines, mako and django_templates,
     # and one of them (django templates), requires the directories be added
diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/partials/base/_variables.scss
similarity index 100%
rename from lms/static/sass/base/_variables.scss
rename to lms/static/sass/partials/base/_variables.scss
diff --git a/lms/templates/main_django.html b/lms/templates/main_django.html
index 59c5e220789..fe9d8a65a8c 100644
--- a/lms/templates/main_django.html
+++ b/lms/templates/main_django.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-{% load sekizai_tags i18n microsite pipeline optional_include %}
+{% load sekizai_tags i18n microsite theme_pipeline optional_include %}
 {% load url from future %}
 <html lang="{{LANGUAGE_CODE}}">
 <head>
diff --git a/lms/templates/wiki/base.html b/lms/templates/wiki/base.html
index 6223193d508..0e3321986e3 100644
--- a/lms/templates/wiki/base.html
+++ b/lms/templates/wiki/base.html
@@ -1,5 +1,5 @@
 {% extends "main_django.html" %}
-{% load pipeline %}{% load sekizai_tags i18n microsite %}{% load url from future %}{% load staticfiles %}
+{% load theme_pipeline %}{% load sekizai_tags i18n microsite %}{% load url from future %}{% load staticfiles %}
 
 {% block title %}<title>{% block pagetitle %}{% endblock %} | {% trans "Wiki" %} | {% platform_name %}</title>{% endblock %}
 
diff --git a/lms/templates/wiki/preview_inline.html b/lms/templates/wiki/preview_inline.html
index 75f57e96038..a2e44b3526c 100644
--- a/lms/templates/wiki/preview_inline.html
+++ b/lms/templates/wiki/preview_inline.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-{% load wiki_tags i18n %}{% load pipeline %}
+{% load wiki_tags i18n %}{% load theme_pipeline %}
 <html lang="{{LANGUAGE_CODE}}">
 <head>
   {% stylesheet 'course' %}
diff --git a/openedx/core/djangoapps/theming/admin.py b/openedx/core/djangoapps/theming/admin.py
new file mode 100644
index 00000000000..690016f8c8c
--- /dev/null
+++ b/openedx/core/djangoapps/theming/admin.py
@@ -0,0 +1,22 @@
+"""
+Django admin page for theming models
+"""
+from django.contrib import admin
+
+from .models import (
+    SiteTheme,
+)
+
+
+class SiteThemeAdmin(admin.ModelAdmin):
+    """ Admin interface for the SiteTheme object. """
+    list_display = ('site', 'theme_dir_name')
+    search_fields = ('site__domain', 'theme_dir_name')
+
+    class Meta(object):
+        """
+        Meta class for SiteTheme admin model
+        """
+        model = SiteTheme
+
+admin.site.register(SiteTheme, SiteThemeAdmin)
diff --git a/openedx/core/djangoapps/theming/core.py b/openedx/core/djangoapps/theming/core.py
index 9203cc5c1f4..86bc27ed9fd 100644
--- a/openedx/core/djangoapps/theming/core.py
+++ b/openedx/core/djangoapps/theming/core.py
@@ -1,62 +1,32 @@
 """
 Core logic for Comprehensive Theming.
 """
-from path import Path
-
+import os.path
+from path import Path as path
 from django.conf import settings
 
+from .helpers import (
+    get_project_root_name,
+)
 
-def comprehensive_theme_changes(theme_dir):
-    """
-    Calculate the set of changes needed to enable a comprehensive theme.
-
-    Arguments:
-        theme_dir (path.path): the full path to the theming directory to use.
-
-    Returns:
-        A dict indicating the changes to make:
-
-            * 'settings': a dictionary of settings names and their new values.
-
-            * 'template_paths': a list of directories to prepend to template
-                lookup path.
 
-    """
-
-    changes = {
-        'settings': {},
-        'template_paths': [],
-    }
-    root = Path(settings.PROJECT_ROOT)
-    if root.name == "":
-        root = root.parent
-
-    component_dir = theme_dir / root.name
-
-    templates_dir = component_dir / "templates"
-    if templates_dir.isdir():
-        changes['template_paths'].append(templates_dir)
-
-    staticfiles_dir = component_dir / "static"
-    if staticfiles_dir.isdir():
-        changes['settings']['STATICFILES_DIRS'] = [staticfiles_dir] + settings.STATICFILES_DIRS
-
-    locale_dir = component_dir / "conf" / "locale"
-    if locale_dir.isdir():
-        changes['settings']['LOCALE_PATHS'] = [locale_dir] + settings.LOCALE_PATHS
-
-    return changes
-
-
-def enable_comprehensive_theme(theme_dir):
+def enable_comprehensive_theming(themes_dir):
     """
     Add directories to relevant paths for comprehensive theming.
+    :param themes_dir: path to base theme directory
     """
-    changes = comprehensive_theme_changes(theme_dir)
+    if isinstance(themes_dir, basestring):
+        themes_dir = path(themes_dir)
+
+    if themes_dir.isdir():
+        settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].insert(0, themes_dir)
+        settings.MAKO_TEMPLATES['main'].insert(0, themes_dir)
+
+    for theme_dir in os.listdir(themes_dir):
+        staticfiles_dir = os.path.join(themes_dir, theme_dir, get_project_root_name(), "static")
+        if staticfiles_dir.isdir():
+            settings.STATICFILES_DIRS = settings.STATICFILES_DIRS + [staticfiles_dir]
 
-    # Use the changes
-    for name, value in changes['settings'].iteritems():
-        setattr(settings, name, value)
-    for template_dir in changes['template_paths']:
-        settings.DEFAULT_TEMPLATE_ENGINE['DIRS'].insert(0, template_dir)
-        settings.MAKO_TEMPLATES['main'].insert(0, template_dir)
+        locale_dir = os.path.join(themes_dir, theme_dir, get_project_root_name(), "conf", "locale")
+        if locale_dir.isdir():
+            settings.LOCALE_PATHS = (locale_dir, ) + settings.LOCALE_PATHS
diff --git a/openedx/core/djangoapps/theming/finders.py b/openedx/core/djangoapps/theming/finders.py
index cbf4366f5a6..7db3f82f39d 100644
--- a/openedx/core/djangoapps/theming/finders.py
+++ b/openedx/core/djangoapps/theming/finders.py
@@ -17,63 +17,80 @@ interface, as well.
 .. _Django-Pipeline: http://django-pipeline.readthedocs.org/
 .. _Django-Require: https://github.com/etianen/django-require
 """
-from path import Path
-from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
+import os
+from collections import OrderedDict
+
 from django.contrib.staticfiles import utils
 from django.contrib.staticfiles.finders import BaseFinder
-from openedx.core.djangoapps.theming.storage import CachedComprehensiveThemingStorage
+from django.utils import six
+
+from openedx.core.djangoapps.theming.helpers import get_themes
+from openedx.core.djangoapps.theming.storage import ThemeStorage
 
 
-class ComprehensiveThemeFinder(BaseFinder):
+class ThemeFilesFinder(BaseFinder):
     """
-    A static files finder that searches the active comprehensive theme
-    for static files. If the ``COMPREHENSIVE_THEME_DIR`` setting is unset,
-    or the ``COMPREHENSIVE_THEME_DIR`` does not exist on the file system,
-    this finder will never find any files.
+    A static files finder that looks in the directory of each theme as
+    specified in the source_dir attribute.
     """
+    storage_class = ThemeStorage
+    source_dir = 'static'
+
     def __init__(self, *args, **kwargs):
-        super(ComprehensiveThemeFinder, self).__init__(*args, **kwargs)
+        # The list of themes that are handled
+        self.themes = []
+        # Mapping of theme names to storage instances
+        self.storages = OrderedDict()
 
-        theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
-        if not theme_dir:
-            self.storage = None
-            return
+        themes = get_themes()
+        for theme in themes:
+            theme_storage = self.storage_class(
+                os.path.join(theme.path, self.source_dir),
+                prefix=theme.theme_dir,
+            )
 
-        if not isinstance(theme_dir, basestring):
-            raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
+            self.storages[theme.theme_dir] = theme_storage
+            if theme.theme_dir not in self.themes:
+                self.themes.append(theme.theme_dir)
 
-        root = Path(settings.PROJECT_ROOT)
-        if root.name == "":
-            root = root.parent
+        super(ThemeFilesFinder, self).__init__(*args, **kwargs)
 
-        component_dir = Path(theme_dir) / root.name
-        static_dir = component_dir / "static"
-        self.storage = CachedComprehensiveThemingStorage(location=static_dir)
+    def list(self, ignore_patterns):
+        """
+        List all files in all app storages.
+        """
+        for storage in six.itervalues(self.storages):
+            if storage.exists(''):  # check if storage location exists
+                for path in utils.get_files(storage, ignore_patterns):
+                    yield path, storage
 
     def find(self, path, all=False):  # pylint: disable=redefined-builtin
         """
-        Looks for files in the default file storage, if it's local.
+        Looks for files in the theme directories.
         """
-        if not self.storage:
-            return []
-
-        if path.startswith(self.storage.prefix):
-            # strip the prefix
-            path = path[len(self.storage.prefix):]
+        matches = []
+        theme_dir = path.split("/", 1)[0]
 
-        if self.storage.exists(path):
-            match = self.storage.path(path)
-            if all:
-                match = [match]
-            return match
+        themes = {t.theme_dir: t for t in get_themes()}
+        # if path is prefixed by theme name then search in the corresponding storage other wise search all storages.
+        if theme_dir in themes:
+            theme = themes[theme_dir]
+            path = "/".join(path.split("/")[1:])
+            match = self.find_in_theme(theme.theme_dir, path)
+            if match:
+                if not all:
+                    return match
+                matches.append(match)
+        return matches
 
-        return []
-
-    def list(self, ignore_patterns):
+    def find_in_theme(self, theme, path):
         """
-        List all files of the storage.
+        Find a requested static file in an theme's static locations.
         """
-        if self.storage and self.storage.exists(''):
-            for path in utils.get_files(self.storage, ignore_patterns):
-                yield path, self.storage
+        storage = self.storages.get(theme, None)
+        if storage:
+            # only try to find a file if the source dir actually exists
+            if storage.exists(path):
+                matched_path = storage.path(path)
+                if matched_path:
+                    return matched_path
diff --git a/openedx/core/djangoapps/theming/helpers.py b/openedx/core/djangoapps/theming/helpers.py
index 28ce710ebe8..7d927c0fe71 100644
--- a/openedx/core/djangoapps/theming/helpers.py
+++ b/openedx/core/djangoapps/theming/helpers.py
@@ -1,9 +1,16 @@
 """
     Helpers for accessing comprehensive theming related variables.
 """
+import re
+import os
+from path import Path
+
+from django.conf import settings, ImproperlyConfigured
+from django.core.cache import cache
+from django.contrib.staticfiles.storage import staticfiles_storage
+
 from microsite_configuration import microsite
 from microsite_configuration import page_title_breadcrumbs
-from django.conf import settings
 
 
 def get_page_title_breadcrumbs(*args):
@@ -24,7 +31,11 @@ def get_template_path(relative_path, **kwargs):
     """
     This is a proxy function to hide microsite_configuration behind comprehensive theming.
     """
-    return microsite.get_template_path(relative_path, **kwargs)
+    template_path = get_template_path_with_theme(relative_path)
+    if template_path == relative_path:  # we don't have a theme now look into microsites
+        template_path = microsite.get_template_path(relative_path, **kwargs)
+
+    return template_path
 
 
 def is_request_in_themed_site():
@@ -34,6 +45,14 @@ def is_request_in_themed_site():
     return microsite.is_request_in_microsite()
 
 
+def get_template(uri):
+    """
+    This is a proxy function to hide microsite_configuration behind comprehensive theming.
+    :param uri: uri of the template
+    """
+    return microsite.get_template(uri)
+
+
 def get_themed_template_path(relative_path, default_path, **kwargs):
     """
     This is a proxy function to hide microsite_configuration behind comprehensive theming.
@@ -52,3 +71,311 @@ def get_themed_template_path(relative_path, default_path, **kwargs):
     if is_stanford_theming_enabled and not is_microsite:
         return relative_path
     return microsite.get_template_path(default_path, **kwargs)
+
+
+def get_template_path_with_theme(relative_path):
+    """
+    Returns template path in current site's theme if it finds one there otherwise returns same path.
+
+    Example:
+        >> get_template_path_with_theme('header')
+        '/red-theme/lms/templates/header.html'
+
+    Parameters:
+        relative_path (str): template's path relative to the templates directory e.g. 'footer.html'
+
+    Returns:
+        (str): template path in current site's theme
+    """
+    site_theme_dir = get_current_site_theme_dir()
+    if not site_theme_dir:
+        return relative_path
+
+    base_theme_dir = get_base_theme_dir()
+    root_name = get_project_root_name()
+    template_path = "/".join([
+        base_theme_dir,
+        site_theme_dir,
+        root_name,
+        "templates"
+    ])
+
+    # strip `/` if present at the start of relative_path
+    template_name = re.sub(r'^/+', '', relative_path)
+    search_path = os.path.join(template_path, template_name)
+    if os.path.isfile(search_path):
+        path = '/{site_theme_dir}/{root_name}/templates/{template_name}'.format(
+            site_theme_dir=site_theme_dir,
+            root_name=root_name,
+            template_name=template_name,
+        )
+        return path
+    else:
+        return relative_path
+
+
+def get_current_theme_template_dirs():
+    """
+    Returns template directories for the current theme.
+
+    Example:
+        >> get_current_theme_template_dirs('header.html')
+        ['/edx/app/edxapp/edx-platform/themes/red-theme/lms/templates/', ]
+
+    Returns:
+        (list): list of directories containing theme templates.
+    """
+    site_theme_dir = get_current_site_theme_dir()
+    if not site_theme_dir:
+        return None
+
+    base_theme_dir = get_base_theme_dir()
+    root_name = get_project_root_name()
+    template_path = "/".join([
+        base_theme_dir,
+        site_theme_dir,
+        root_name,
+        "templates"
+    ])
+
+    return [template_path]
+
+
+def strip_site_theme_templates_path(uri):
+    """
+    Remove site template theme path from the uri.
+
+    Example:
+        >> strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
+        'header.html'
+
+    Arguments:
+        uri (str): template path from which to remove site theme path. e.g. '/red-theme/lms/templates/header.html'
+
+    Returns:
+        (str): template path with site theme path removed.
+    """
+    site_theme_dir = get_current_site_theme_dir()
+    if not site_theme_dir:
+        return uri
+
+    root_name = get_project_root_name()
+    templates_path = "/".join([
+        site_theme_dir,
+        root_name,
+        "templates"
+    ])
+
+    uri = re.sub(r'^/*' + templates_path + '/*', '', uri)
+    return uri
+
+
+def get_current_site():
+    """
+    Return current site.
+
+    Returns:
+         (django.contrib.sites.models.Site): theme directory for current site
+    """
+    from edxmako.middleware import REQUEST_CONTEXT
+    request = getattr(REQUEST_CONTEXT, 'request', None)
+    if not request:
+        return None
+    return getattr(request, 'site', None)
+
+
+def get_current_site_theme_dir():
+    """
+    Return theme directory for the current site.
+
+    Example:
+        >> get_current_site_theme_dir()
+        'red-theme'
+
+    Returns:
+         (str): theme directory for current site
+    """
+    site = get_current_site()
+    if not site:
+        return None
+    site_theme_dir = cache.get(get_site_theme_cache_key(site))
+
+    # if site theme dir is not in cache and comprehensive theming is enabled then pull it from db.
+    if not site_theme_dir and is_comprehensive_theming_enabled():
+        site_theme = site.themes.first()  # pylint: disable=no-member
+        if site_theme:
+            site_theme_dir = site_theme.theme_dir_name
+            cache_site_theme_dir(site, site_theme_dir)
+    return site_theme_dir
+
+
+def get_project_root_name():
+    """
+    Return root name for the current project
+
+    Example:
+        >> get_project_root_name()
+        'lms'
+        # from studio
+        >> get_project_root_name()
+        'cms'
+
+    Returns:
+        (str): component name of platform e.g lms, cms
+    """
+    root = Path(settings.PROJECT_ROOT)
+    if root.name == "":
+        root = root.parent
+    return root.name
+
+
+def get_base_theme_dir():
+    """
+    Return base directory that contains all the themes.
+
+    Example:
+        >> get_base_theme_dir()
+        '/edx/app/edxapp/edx-platform/themes'
+
+    Returns:
+         (Path): Base theme directory path
+    """
+    themes_dir = settings.COMPREHENSIVE_THEME_DIR
+    if not isinstance(themes_dir, basestring):
+        raise ImproperlyConfigured("COMPREHENSIVE_THEME_DIR must be a string.")
+    return Path(themes_dir)
+
+
+def is_comprehensive_theming_enabled():
+    """
+    Returns boolean indicating whether comprehensive theming functionality is enabled or disabled.
+    Example:
+        >> is_comprehensive_theming_enabled()
+        True
+
+    Returns:
+         (bool): True if comprehensive theming is enabled else False
+    """
+    return True if settings.COMPREHENSIVE_THEME_DIR else False
+
+
+def get_site_theme_cache_key(site):
+    """
+    Return cache key for the given site.
+
+    Example:
+        >> site = Site(domain='red-theme.org', name='Red Theme')
+        >> get_site_theme_cache_key(site)
+        'theming.site.red-theme.org'
+
+    Parameters:
+        site (django.contrib.sites.models.Site): site where key needs to generated
+    Returns:
+        (str): a key to be used as cache key
+    """
+    cache_key = "theming.site.{domain}".format(
+        domain=site.domain
+    )
+    return cache_key
+
+
+def cache_site_theme_dir(site, theme_dir):
+    """
+    Cache site's theme directory.
+
+    Example:
+        >> site = Site(domain='red-theme.org', name='Red Theme')
+        >> cache_site_theme_dir(site, 'red-theme')
+
+    Parameters:
+        site (django.contrib.sites.models.Site): site for to cache
+        theme_dir (str): theme directory for the given site
+    """
+    cache.set(get_site_theme_cache_key(site), theme_dir, settings.THEME_CACHE_TIMEOUT)
+
+
+def get_static_file_url(asset):
+    """
+    Returns url of the themed asset if asset is not themed than returns the default asset url.
+
+    Example:
+        >> get_static_file_url('css/lms-main.css')
+        '/static/red-theme/css/lms-main.css'
+
+    Parameters:
+        asset (str): asset's path relative to the static files directory
+
+    Returns:
+        (str): static asset's url
+    """
+    return staticfiles_storage.url(asset)
+
+
+def get_themes():
+    """
+    get a list of all themes known to the system.
+    Returns:
+        list of themes known to the system.
+    """
+    themes_dir = get_base_theme_dir()
+    # pick only directories and discard files in themes directory
+    theme_names = []
+    if themes_dir:
+        theme_names = [_dir for _dir in os.listdir(themes_dir) if is_theme_dir(themes_dir / _dir)]
+
+    return [Theme(name, name) for name in theme_names]
+
+
+def is_theme_dir(_dir):
+    """
+    Returns true if given dir contains theme overrides.
+    A theme dir must have subdirectory 'lms' or 'cms' or both.
+
+    Args:
+        _dir: directory path to check for a theme
+
+    Returns:
+        Returns true if given dir is a theme directory.
+    """
+    theme_sub_directories = {'lms', 'cms'}
+    return bool(os.path.isdir(_dir) and theme_sub_directories.intersection(os.listdir(_dir)))
+
+
+class Theme(object):
+    """
+    class to encapsulate theme related information.
+    """
+    name = ''
+    theme_dir = ''
+    path = ''
+
+    def __init__(self, name='', theme_dir=''):
+        """
+        init method for Theme
+        Args:
+            name: name if the theme
+            theme_dir: directory name of the theme
+        """
+        self.name = name
+        self.theme_dir = theme_dir
+        self.path = Path(get_base_theme_dir()) / theme_dir / get_project_root_name()
+
+    def __eq__(self, other):
+        """
+        Returns True if given theme is same as the self
+        Args:
+            other: Theme object to compare with self
+
+        Returns:
+            (bool) True if two themes are the same else False
+        """
+        return (self.theme_dir, self.path) == (other.theme_dir, other.path)
+
+    def __hash__(self):
+        return hash((self.theme_dir, self.path))
+
+    def __unicode__(self):
+        return u"<Theme: {name} at '{path}'>".format(name=self.name, path=self.path)
+
+    def __repr__(self):
+        return self.__unicode__()
diff --git a/openedx/core/djangoapps/theming/migrations/0001_initial.py b/openedx/core/djangoapps/theming/migrations/0001_initial.py
new file mode 100644
index 00000000000..ebf80f9d3e0
--- /dev/null
+++ b/openedx/core/djangoapps/theming/migrations/0001_initial.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('sites', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SiteTheme',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('theme_dir_name', models.CharField(max_length=255)),
+                ('site', models.ForeignKey(related_name='themes', to='sites.Site')),
+            ],
+        ),
+    ]
diff --git a/openedx/core/djangoapps/theming/migrations/__init__.py b/openedx/core/djangoapps/theming/migrations/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/openedx/core/djangoapps/theming/models.py b/openedx/core/djangoapps/theming/models.py
new file mode 100644
index 00000000000..d3e70189ded
--- /dev/null
+++ b/openedx/core/djangoapps/theming/models.py
@@ -0,0 +1,19 @@
+"""
+Comprehensive Theme related models.
+"""
+from django.db import models
+from django.contrib.sites.models import Site
+
+
+class SiteTheme(models.Model):
+    """
+    This is where the information about the site's theme gets stored to the db.
+
+    `site` field is foreignkey to django Site model
+    `theme_dir_name` contains directory name having Site's theme
+    """
+    site = models.ForeignKey(Site, related_name='themes')
+    theme_dir_name = models.CharField(max_length=255)
+
+    def __unicode__(self):
+        return self.theme_dir_name
diff --git a/openedx/core/djangoapps/theming/storage.py b/openedx/core/djangoapps/theming/storage.py
index 3fb5311b5a6..3386e250a46 100644
--- a/openedx/core/djangoapps/theming/storage.py
+++ b/openedx/core/djangoapps/theming/storage.py
@@ -2,87 +2,300 @@
 Comprehensive Theming support for Django's collectstatic functionality.
 See https://docs.djangoproject.com/en/1.8/ref/contrib/staticfiles/
 """
-from path import Path
+import posixpath
 import os.path
 from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
-from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin
 from django.utils._os import safe_join
+from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin
+from django.contrib.staticfiles.finders import find
+from django.utils.six.moves.urllib.parse import (  # pylint: disable=no-name-in-module, import-error
+    unquote, urlsplit,
+)
+
+from pipeline.storage import PipelineMixin
 
+from openedx.core.djangoapps.theming.helpers import (
+    get_base_theme_dir,
+    get_project_root_name,
+    get_current_site_theme_dir,
+    get_themes,
+)
 
-class ComprehensiveThemingAwareMixin(object):
+
+class ThemeStorage(StaticFilesStorage):
     """
-    Mixin for Django storage system to make it aware of the currently-active
-    comprehensive theme, so that it can generate theme-scoped URLs for themed
-    static assets.
+    Comprehensive theme aware Static files storage.
     """
-    def __init__(self, *args, **kwargs):
-        super(ComprehensiveThemingAwareMixin, self).__init__(*args, **kwargs)
-        theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
-        if not theme_dir:
-            self.theme_location = None
-            return
-
-        if not isinstance(theme_dir, basestring):
-            raise ImproperlyConfigured("Your COMPREHENSIVE_THEME_DIR setting must be a string")
+    # prefix for file path, this prefix is added at the beginning of file path before saving static files during
+    # collectstatic command.
+    # e.g. having "edx.org" as prefix will cause files to be saved as "edx.org/images/logo.png"
+    # instead of "images/logo.png"
+    prefix = None
 
-        root = Path(settings.PROJECT_ROOT)
-        if root.name == "":
-            root = root.parent
+    def __init__(self, location=None, base_url=None, file_permissions_mode=None,
+                 directory_permissions_mode=None, prefix=None):
 
-        component_dir = Path(theme_dir) / root.name
-        self.theme_location = component_dir / "static"
+        self.prefix = prefix
+        super(ThemeStorage, self).__init__(
+            location=location,
+            base_url=base_url,
+            file_permissions_mode=file_permissions_mode,
+            directory_permissions_mode=directory_permissions_mode,
+        )
 
-    @property
-    def prefix(self):
+    def url(self, name):
         """
-        This is used by the ComprehensiveThemeFinder in the collection step.
+        Returns url of the asset, themed url will be returned if the asset is themed otherwise default
+        asset url will be returned.
+
+        Args:
+            name: name of the asset, e.g. 'images/logo.png'
+
+        Returns:
+            url of the asset, e.g. '/static/red-theme/images/logo.png' if current theme is red-theme and logo
+            is provided by red-theme otherwise '/static/images/logo.png'
         """
-        theme_dir = getattr(settings, "COMPREHENSIVE_THEME_DIR", "")
-        if not theme_dir:
-            return None
-        theme_name = os.path.basename(os.path.normpath(theme_dir))
-        return "themes/{name}/".format(name=theme_name)
+        prefix = ''
+        theme_dir = get_current_site_theme_dir()
+
+        # get theme prefix from site address if if asset is accessed via a url
+        if theme_dir:
+            prefix = theme_dir
+
+        # get theme prefix from storage class, if asset is accessed during collectstatic run
+        elif self.prefix:
+            prefix = self.prefix
+
+        # join theme prefix with asset name if theme is applied and themed asset exists
+        if prefix and self.themed(name, prefix):
+            name = os.path.join(prefix, name)
 
-    def themed(self, name):
+        return super(ThemeStorage, self).url(name)
+
+    def themed(self, name, theme):
         """
-        Given a name, return a boolean indicating whether that name exists
-        as a themed asset in the comprehensive theme.
+        Returns True if given asset override is provided by the given theme otherwise returns False.
+        Args:
+            name: asset name e.g. 'images/logo.png'
+            theme: theme name e.g. 'red-theme', 'edx.org'
+
+        Returns:
+            True if given asset override is provided by the given theme otherwise returns False
         """
-        # Nothing can be themed if we don't have a theme location.
-        if not self.theme_location:
-            return False
+        # in debug mode check static asset from within the project directory
+        if settings.DEBUG:
+            themes_location = get_base_theme_dir()
+            # Nothing can be themed if we don't have a theme location or required params.
+            if not all((themes_location, theme, name)):
+                return False
+
+            themed_path = "/".join([
+                themes_location,
+                theme,
+                get_project_root_name(),
+                "static/"
+            ])
+            name = name[1:] if name.startswith("/") else name
+            path = safe_join(themed_path, name)
+            return os.path.exists(path)
+        # in live mode check static asset in the static files dir defined by "STATIC_ROOT" setting
+        else:
+            return self.exists(os.path.join(theme, name))
+
 
-        path = safe_join(self.theme_location, name)
-        return os.path.exists(path)
+class ComprehensiveThemingCachedFilesMixin(CachedFilesMixin):
+    """
+    Comprehensive theme aware CachedFilesMixin.
+    Main purpose of subclassing CachedFilesMixin is to override the following methods.
+    1 - url
+    2 - url_converter
 
-    def path(self, name):
+    url:
+        This method takes asset name as argument and is responsible for adding hash to the name to support caching.
+        This method is called during both collectstatic command and live server run.
+
+        When called during collectstatic command that name argument will be asset name inside STATIC_ROOT,
+        for non themed assets it will be the usual path (e.g. 'images/logo.png') but for themed asset it will
+        also contain themes dir prefix (e.g. 'red-theme/images/logo.png'). So, here we check whether the themed asset
+        exists or not, if it exists we pass the same name up in the MRO chain for further processing and if it does not
+        exists we strip theme name and pass the new asset name to the MRO chain for further processing.
+
+        When called during server run, we get the theme dir for the current site using `get_current_site_theme_dir` and
+        make sure to prefix theme dir to the asset name. This is done to ensure the usage of correct hash in file name.
+        e.g. if our red-theme overrides 'images/logo.png' and we do not prefix theme dir to the asset name, the hash for
+        '{platform-dir}/lms/static/images/logo.png' would be used instead of
+        '{themes_base_dir}/red-theme/images/logo.png'
+
+    url_converter:
+        This function returns another function that is responsible for hashing urls that appear inside assets
+        (e.g. url("images/logo.png") inside css). The method defined in the superclass adds a hash to file and returns
+        relative url of the file.
+        e.g. for url("../images/logo.png") it would return url("../images/logo.790c9a5340cb.png"). However we would
+        want it to return absolute url (e.g. url("/static/images/logo.790c9a5340cb.png")) so that it works properly
+        with themes.
+
+        The overridden method here simply comments out the two lines that convert absolute url to relative url,
+        hence absolute urls are used instead of relative urls.
+    """
+
+    def url(self, name, force=False):
         """
-        Get the path to the real asset on disk
+        Returns themed url for the given asset.
         """
-        if self.themed(name):
-            base = self.theme_location
-        else:
-            base = self.location
-        path = safe_join(base, name)
-        return os.path.normpath(path)
+        theme_dir = get_current_site_theme_dir()
+        if theme_dir and theme_dir not in name:
+            # during server run, append theme name to the asset name if it is not already there
+            # this is ensure that correct hash is created and default asset is not always
+            # used to create hash of themed assets.
+            name = os.path.join(theme_dir, name)
+        parsed_name = urlsplit(unquote(name))
+        clean_name = parsed_name.path.strip()
+        asset_name = name
+        if not self.exists(clean_name):
+            # if themed asset does not exists then use default asset
+            theme = name.split("/", 1)[0]
+            # verify that themed asset was accessed
+            if theme in [theme.theme_dir for theme in get_themes()]:
+                asset_name = "/".join(name.split("/")[1:])
+
+        return super(ComprehensiveThemingCachedFilesMixin, self).url(asset_name, force)
 
-    def url(self, name, *args, **kwargs):
+    def url_converter(self, name, template=None):
         """
-        Add the theme prefix to the asset URL
+        This is an override of url_converter from CachedFilesMixin.
+        It just comments out two lines at the end of the method.
+
+        The purpose of this override is to make converter method return absolute urls instead of relative urls.
+        This behavior is necessary for theme overrides, as we get 404 on assets with relative urls on a themed site.
         """
-        if self.themed(name):
-            name = self.prefix + name
-        return super(ComprehensiveThemingAwareMixin, self).url(name, *args, **kwargs)
+        if template is None:
+            template = self.default_template
+
+        def converter(matchobj):
+            """
+            Converts the matched URL depending on the parent level (`..`)
+            and returns the normalized and hashed URL using the url method
+            of the storage.
+            """
+            matched, url = matchobj.groups()
+            # Completely ignore http(s) prefixed URLs,
+            # fragments and data-uri URLs
+            if url.startswith(('#', 'http:', 'https:', 'data:', '//')):
+                return matched
+            name_parts = name.split(os.sep)
+            # Using posix normpath here to remove duplicates
+            url = posixpath.normpath(url)
+            url_parts = url.split('/')
+            parent_level, sub_level = url.count('..'), url.count('/')
+            if url.startswith('/'):
+                sub_level -= 1
+                url_parts = url_parts[1:]
+            if parent_level or not url.startswith('/'):
+                start, end = parent_level + 1, parent_level
+            else:
+                if sub_level:
+                    if sub_level == 1:
+                        parent_level -= 1
+                    start, end = parent_level, 1
+                else:
+                    start, end = 1, sub_level - 1
+            joined_result = '/'.join(name_parts[:-start] + url_parts[end:])
+            hashed_url = self.url(unquote(joined_result), force=True)
+
+            # NOTE:
+            # following two lines are commented out so that absolute urls are used instead of relative urls
+            # to make themed assets work correctly.
+            #
+            # The lines are commented and not removed to make future django upgrade easier and
+            # show exactly what is changed in this method override
+            #
+            # file_name = hashed_url.split('/')[-1:]
+            # relative_url = '/'.join(url.split('/')[:-1] + file_name)
 
+            # Return the hashed version to the file
+            return template % unquote(hashed_url)
 
-class CachedComprehensiveThemingStorage(
-        ComprehensiveThemingAwareMixin,
-        CachedFilesMixin,
-        StaticFilesStorage
-):
+        return converter
+
+
+class ThemePipelineMixin(PipelineMixin):
     """
-    Used by the ComprehensiveThemeFinder class. Mixes in support for cached
-    files and comprehensive theming in static files.
+    Mixin to make sure themed assets are also packaged and used along with non themed assets.
+    if a source asset for a particular package is not present then the default asset is used.
+
+    e.g. in the following package and for 'red-theme'
+    'style-vendor': {
+        'source_filenames': [
+            'js/vendor/afontgarde/afontgarde.css',
+            'css/vendor/font-awesome.css',
+            'css/vendor/jquery.qtip.min.css',
+            'css/vendor/responsive-carousel/responsive-carousel.css',
+            'css/vendor/responsive-carousel/responsive-carousel.slide.css',
+        ],
+        'output_filename': 'css/lms-style-vendor.css'
+    }
+    'red-theme/css/vendor/responsive-carousel/responsive-carousel.css' will be used of it exists otherwise
+    'css/vendor/responsive-carousel/responsive-carousel.css' will be used to create 'red-theme/css/lms-style-vendor.css'
     """
-    pass
+    packing = True
+
+    def post_process(self, paths, dry_run=False, **options):
+        """
+        This post_process hook is used to package all themed assets.
+        """
+        if dry_run:
+            return
+        themes = get_themes()
+
+        for theme in themes:
+            css_packages = self.get_themed_packages(theme.theme_dir, settings.PIPELINE_CSS)
+            js_packages = self.get_themed_packages(theme.theme_dir, settings.PIPELINE_JS)
+
+            from pipeline.packager import Packager
+            packager = Packager(storage=self, css_packages=css_packages, js_packages=js_packages)
+            for package_name in packager.packages['css']:
+                package = packager.package_for('css', package_name)
+                output_file = package.output_filename
+                if self.packing:
+                    packager.pack_stylesheets(package)
+                paths[output_file] = (self, output_file)
+                yield output_file, output_file, True
+            for package_name in packager.packages['js']:
+                package = packager.package_for('js', package_name)
+                output_file = package.output_filename
+                if self.packing:
+                    packager.pack_javascripts(package)
+                paths[output_file] = (self, output_file)
+                yield output_file, output_file, True
+
+        super_class = super(ThemePipelineMixin, self)
+        if hasattr(super_class, 'post_process'):
+            for name, hashed_name, processed in super_class.post_process(paths.copy(), dry_run, **options):
+                yield name, hashed_name, processed
+
+    @staticmethod
+    def get_themed_packages(prefix, packages):
+        """
+        Update paths with the themed assets,
+        Args:
+            prefix: theme prefix for which to update asset paths e.g. 'red-theme', 'edx.org' etc.
+            packages: packages to update
+
+        Returns: list of updated paths and a boolean indicating whether any path was path or not
+        """
+        themed_packages = {}
+        for name in packages:
+            # collect source file names for the package
+            source_files = []
+            for path in packages[name].get('source_filenames', []):
+                # if themed asset exists use that, otherwise use default asset.
+                if find(os.path.join(prefix, path)):
+                    source_files.append(os.path.join(prefix, path))
+                else:
+                    source_files.append(path)
+
+            themed_packages[name] = {
+                'output_filename': os.path.join(prefix, packages[name].get('output_filename', '')),
+                'source_filenames': source_files,
+            }
+        return themed_packages
diff --git a/openedx/core/djangoapps/theming/template_loaders.py b/openedx/core/djangoapps/theming/template_loaders.py
new file mode 100644
index 00000000000..5fd82712303
--- /dev/null
+++ b/openedx/core/djangoapps/theming/template_loaders.py
@@ -0,0 +1,27 @@
+"""
+Theming aware template loaders.
+"""
+from django.template.loaders.filesystem import Loader as FilesystemLoader
+
+from edxmako.makoloader import MakoLoader
+from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme
+
+
+class ThemeTemplateLoader(MakoLoader):
+    """
+    This is a Django loader object which will load the template based on current request and its corresponding theme.
+    """
+    def __call__(self, template_name, template_dirs=None):
+        template_name = get_template_path_with_theme(template_name).lstrip("/")
+        return self.load_template(template_name, template_dirs)
+
+
+class ThemeFilesystemLoader(ThemeTemplateLoader):
+    """
+    Filesystem Template loaders to pickup templates from theme directory based on the current site.
+    """
+    is_usable = True
+    _accepts_engine_in_init = True
+
+    def __init__(self, *args):
+        ThemeTemplateLoader.__init__(self, FilesystemLoader(*args))
diff --git a/openedx/core/djangoapps/theming/templatetags/theme_pipeline.py b/openedx/core/djangoapps/theming/templatetags/theme_pipeline.py
new file mode 100644
index 00000000000..7beb99ca55f
--- /dev/null
+++ b/openedx/core/djangoapps/theming/templatetags/theme_pipeline.py
@@ -0,0 +1,78 @@
+"""
+Theme aware pipeline template tags.
+"""
+
+from django import template
+from django.template.loader import render_to_string
+from django.utils.safestring import mark_safe
+
+from pipeline.templatetags.pipeline import StylesheetNode, JavascriptNode
+from pipeline.utils import guess_type
+
+from openedx.core.djangoapps.theming.helpers import get_static_file_url
+
+register = template.Library()  # pylint: disable=invalid-name
+
+
+class ThemeStylesheetNode(StylesheetNode):
+    """
+    Overrides StyleSheetNode from django pipeline so that stylesheets are served based on the applied theme.
+    """
+    def render_css(self, package, path):
+        """
+        Override render_css from django-pipline so that stylesheets urls are based on the applied theme
+        """
+        template_name = package.template_name or "pipeline/css.html"
+        context = package.extra_context
+        context.update({
+            'type': guess_type(path, 'text/css'),
+            'url': mark_safe(get_static_file_url(path))
+        })
+        return render_to_string(template_name, context)
+
+
+class ThemeJavascriptNode(JavascriptNode):
+    """
+    Overrides JavascriptNode from django pipeline so that js files are served based on the applied theme.
+    """
+    def render_js(self, package, path):
+        """
+        Override render_js from django-pipline so that js file urls are based on the applied theme
+        """
+        template_name = package.template_name or "pipeline/js.html"
+        context = package.extra_context
+        context.update({
+            'type': guess_type(path, 'text/javascript'),
+            'url': mark_safe(get_static_file_url(path))
+        })
+        return render_to_string(template_name, context)
+
+
+@register.tag
+def stylesheet(parser, token):  # pylint: disable=unused-argument
+    """
+    Template tag to serve stylesheets from django-pipeline. This definition uses the theming aware ThemeStyleSheetNode.
+    """
+    try:
+        _, name = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError(
+            '%r requires exactly one argument: the name of a group in the PIPELINE_CSS setting' %
+            token.split_contents()[0]
+        )
+    return ThemeStylesheetNode(name)
+
+
+@register.tag
+def javascript(parser, token):  # pylint: disable=unused-argument
+    """
+    Template tag to serve javascript from django-pipeline. This definition uses the theming aware ThemeJavascriptNode.
+    """
+    try:
+        _, name = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError(
+            '%r requires exactly one argument: the name of a group in the PIPELINE_JS setting' %
+            token.split_contents()[0]
+        )
+    return ThemeJavascriptNode(name)
diff --git a/openedx/core/djangoapps/theming/test_util.py b/openedx/core/djangoapps/theming/test_util.py
index fbd871c2138..f75957ba1fc 100644
--- a/openedx/core/djangoapps/theming/test_util.py
+++ b/openedx/core/djangoapps/theming/test_util.py
@@ -6,87 +6,57 @@ from functools import wraps
 import os
 import os.path
 import contextlib
+import re
 
 from mock import patch
 
 from django.conf import settings
-from django.template import Engine
-from django.test.utils import override_settings
+from django.contrib.sites.models import Site
 
 import edxmako
+from .models import SiteTheme
 
-from .core import comprehensive_theme_changes
 
-EDX_THEME_DIR = settings.REPO_ROOT / "themes" / "edx.org"
-
-
-def with_comprehensive_theme(theme_dir):
+def with_comprehensive_theme(theme_dir_name):
     """
-    A decorator to run a test with a particular comprehensive theme.
-
+    A decorator to run a test with a comprehensive theming enabled.
     Arguments:
-        theme_dir (str): the full path to the theme directory to use.
-            This will likely use `settings.REPO_ROOT` to get the full path.
-
+        theme_dir_name (str): directory name of the site for which we want comprehensive theming enabled.
     """
-    # This decorator gets the settings changes needed for a theme, and applies
-    # them using the override_settings and edxmako.paths.add_lookup context
-    # managers.
-
-    changes = comprehensive_theme_changes(theme_dir)
-
+    # This decorator creates Site and SiteTheme models for given domain
     def _decorator(func):                       # pylint: disable=missing-docstring
         @wraps(func)
         def _decorated(*args, **kwargs):        # pylint: disable=missing-docstring
-            with override_settings(COMPREHENSIVE_THEME_DIR=theme_dir, **changes['settings']):
-                default_engine = Engine.get_default()
-                dirs = default_engine.dirs[:]
-                with edxmako.save_lookups():
-                    for template_dir in changes['template_paths']:
-                        edxmako.paths.add_lookup('main', template_dir, prepend=True)
-                        dirs.insert(0, template_dir)
-                    with patch.object(default_engine, 'dirs', dirs):
-                        return func(*args, **kwargs)
+            # make a domain name out of directory name
+            domain = "{theme_dir_name}.org".format(theme_dir_name=re.sub(r"\.org$", "", theme_dir_name))
+            site, __ = Site.objects.get_or_create(domain=domain, name=domain)
+            SiteTheme.objects.get_or_create(site=site, theme_dir_name=theme_dir_name)
+            edxmako.paths.add_lookup('main', settings.COMPREHENSIVE_THEME_DIR, prepend=True)
+            with patch('openedx.core.djangoapps.theming.helpers.get_current_site_theme_dir',
+                       return_value=theme_dir_name):
+                with patch('openedx.core.djangoapps.theming.helpers.get_current_site', return_value=site):
+                    return func(*args, **kwargs)
         return _decorated
     return _decorator
 
 
-def with_is_edx_domain(is_edx_domain):
-    """
-    A decorator to run a test as if request originated from edX domain or not.
-
-    Arguments:
-        is_edx_domain (bool): are we an edX domain or not?
-
-    """
-    # This is weird, it's a decorator that conditionally applies other
-    # decorators, which is confusing.
-    def _decorator(func):                       # pylint: disable=missing-docstring
-        if is_edx_domain:
-            # This applies @with_comprehensive_theme to the func.
-            func = with_comprehensive_theme(EDX_THEME_DIR)(func)
-
-        return func
-
-    return _decorator
-
-
 @contextlib.contextmanager
-def with_edx_domain_context(is_edx_domain):
+def with_comprehensive_theme_context(theme=None):
     """
-    A function to run a test as if request originated from edX domain or not.
+    A function to run a test as if request was made to the given theme.
 
     Arguments:
-        is_edx_domain (bool): are we an edX domain or not?
+        theme (str): name if the theme or None if no theme is applied
 
     """
-    if is_edx_domain:
-        changes = comprehensive_theme_changes(EDX_THEME_DIR)
-        with override_settings(COMPREHENSIVE_THEME_DIR=EDX_THEME_DIR, **changes['settings']):
-            with edxmako.save_lookups():
-                for template_dir in changes['template_paths']:
-                    edxmako.paths.add_lookup('main', template_dir, prepend=True)
-
+    if theme:
+        domain = '{theme}.org'.format(theme=re.sub(r"\.org$", "", theme))
+        site, __ = Site.objects.get_or_create(domain=domain, name=theme)
+        SiteTheme.objects.get_or_create(site=site, theme_dir_name=theme)
+        edxmako.paths.add_lookup('main', settings.COMPREHENSIVE_THEME_DIR, prepend=True)
+        with patch('openedx.core.djangoapps.theming.helpers.get_current_site_theme_dir',
+                   return_value=theme):
+            with patch('openedx.core.djangoapps.theming.helpers.get_current_site', return_value=site):
                 yield
     else:
         yield
diff --git a/openedx/core/djangoapps/theming/tests/test_helpers.py b/openedx/core/djangoapps/theming/tests/test_helpers.py
new file mode 100644
index 00000000000..9c4e957cb9b
--- /dev/null
+++ b/openedx/core/djangoapps/theming/tests/test_helpers.py
@@ -0,0 +1,152 @@
+"""Tests of comprehensive theming."""
+import unittest
+from mock import patch
+
+from django.test import TestCase, RequestFactory, override_settings
+from django.conf import settings
+
+from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
+from openedx.core.djangoapps.theming.helpers import get_template_path_with_theme, strip_site_theme_templates_path, \
+    get_current_site_theme_dir, get_themes, Theme
+
+
+class TestHelpers(TestCase):
+    """Test comprehensive theming helper functions."""
+
+    def test_get_themes(self):
+        """
+        Tests template paths are returned from enabled theme.
+        """
+        expected_themes = [
+            Theme('red-theme', 'red-theme'),
+            Theme('edge.edx.org', 'edge.edx.org'),
+            Theme('edx.org', 'edx.org'),
+            Theme('stanford-style', 'stanford-style'),
+        ]
+        actual_themes = get_themes()
+        self.assertItemsEqual(expected_themes, actual_themes)
+
+    @override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
+    def test_get_themes_2(self):
+        """
+        Tests template paths are returned from enabled theme.
+        """
+        expected_themes = [
+            Theme('test-theme', 'test-theme'),
+        ]
+        actual_themes = get_themes()
+        self.assertItemsEqual(expected_themes, actual_themes)
+
+
+@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+class TestHelpersLMS(TestCase):
+    """Test comprehensive theming helper functions."""
+
+    @with_comprehensive_theme('red-theme')
+    def test_get_template_path_with_theme_enabled(self):
+        """
+        Tests template paths are returned from enabled theme.
+        """
+        template_path = get_template_path_with_theme('header.html')
+        self.assertEqual(template_path, '/red-theme/lms/templates/header.html')
+
+    @with_comprehensive_theme('red-theme')
+    def test_get_template_path_with_theme_for_missing_template(self):
+        """
+        Tests default template paths are returned if template is not found in the theme.
+        """
+        template_path = get_template_path_with_theme('course.html')
+        self.assertEqual(template_path, 'course.html')
+
+    def test_get_template_path_with_theme_disabled(self):
+        """
+        Tests default template paths are returned when theme is non theme is enabled.
+        """
+        template_path = get_template_path_with_theme('header.html')
+        self.assertEqual(template_path, 'header.html')
+
+    @with_comprehensive_theme('red-theme')
+    def test_strip_site_theme_templates_path_theme_enabled(self):
+        """
+        Tests site theme templates path is stripped from the given template path.
+        """
+        template_path = strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
+        self.assertEqual(template_path, 'header.html')
+
+    def test_strip_site_theme_templates_path_theme_disabled(self):
+        """
+        Tests site theme templates path returned unchanged if no theme is applied.
+        """
+        template_path = strip_site_theme_templates_path('/red-theme/lms/templates/header.html')
+        self.assertEqual(template_path, '/red-theme/lms/templates/header.html')
+
+    @with_comprehensive_theme('red-theme')
+    def test_get_current_site_theme_dir(self):
+        """
+        Tests current site theme name.
+        """
+        factory = RequestFactory()
+        with patch(
+            'edxmako.middleware.REQUEST_CONTEXT.request',
+            factory.get('/', SERVER_NAME="red-theme.org"),
+            create=True,
+        ):
+            current_site = get_current_site_theme_dir()
+            self.assertEqual(current_site, 'red-theme')
+
+
+@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
+class TestHelpersCMS(TestCase):
+    """Test comprehensive theming helper functions."""
+
+    @with_comprehensive_theme('red-theme')
+    def test_get_template_path_with_theme_enabled(self):
+        """
+        Tests template paths are returned from enabled theme.
+        """
+        template_path = get_template_path_with_theme('login.html')
+        self.assertEqual(template_path, '/red-theme/cms/templates/login.html')
+
+    @with_comprehensive_theme('red-theme')
+    def test_get_template_path_with_theme_for_missing_template(self):
+        """
+        Tests default template paths are returned if template is not found in the theme.
+        """
+        template_path = get_template_path_with_theme('certificates.html')
+        self.assertEqual(template_path, 'certificates.html')
+
+    def test_get_template_path_with_theme_disabled(self):
+        """
+        Tests default template paths are returned when theme is non theme is enabled.
+        """
+        template_path = get_template_path_with_theme('login.html')
+        self.assertEqual(template_path, 'login.html')
+
+    @with_comprehensive_theme('red-theme')
+    def test_strip_site_theme_templates_path_theme_enabled(self):
+        """
+        Tests site theme templates path is stripped from the given template path.
+        """
+        template_path = strip_site_theme_templates_path('/red-theme/cms/templates/login.html')
+        self.assertEqual(template_path, 'login.html')
+
+    def test_strip_site_theme_templates_path_theme_disabled(self):
+        """
+        Tests site theme templates path returned unchanged if no theme is applied.
+        """
+        template_path = strip_site_theme_templates_path('/red-theme/cms/templates/login.html')
+        self.assertEqual(template_path, '/red-theme/cms/templates/login.html')
+
+    @with_comprehensive_theme('red-theme')
+    def test_get_current_site_theme_dir(self):
+        """
+        Tests current site theme name.
+        """
+        factory = RequestFactory()
+        with patch(
+            'edxmako.middleware.REQUEST_CONTEXT.request',
+            factory.get('/', SERVER_NAME="red-theme.org"),
+            create=True,
+        ):
+            current_site = get_current_site_theme_dir()
+            self.assertEqual(current_site, 'red-theme')
diff --git a/openedx/core/djangoapps/theming/tests/test_storage.py b/openedx/core/djangoapps/theming/tests/test_storage.py
new file mode 100644
index 00000000000..d46b07c3c31
--- /dev/null
+++ b/openedx/core/djangoapps/theming/tests/test_storage.py
@@ -0,0 +1,82 @@
+"""
+Tests for comprehensive theme static files storage classes.
+"""
+import ddt
+import unittest
+import re
+
+from mock import patch
+
+from django.test import TestCase, override_settings
+from django.conf import settings
+
+from openedx.core.djangoapps.theming.helpers import get_base_theme_dir
+from openedx.core.djangoapps.theming.storage import ThemeStorage
+
+
+@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+@ddt.ddt
+class TestStorageLMS(TestCase):
+    """
+    Test comprehensive theming static files storage.
+    """
+
+    def setUp(self):
+        super(TestStorageLMS, self).setUp()
+        self.themes_dir = get_base_theme_dir()
+        self.enabled_theme = "red-theme"
+        self.system_dir = settings.REPO_ROOT / "lms"
+        self.storage = ThemeStorage(location=self.themes_dir / self.enabled_theme / 'lms' / 'static')
+
+    @override_settings(DEBUG=True)
+    @ddt.data(
+        (True, "images/logo.png"),
+        (True, "images/favicon.ico"),
+        (False, "images/spinning.gif"),
+    )
+    @ddt.unpack
+    def test_themed(self, is_themed, asset):
+        """
+        Verify storage returns True on themed assets
+        """
+        self.assertEqual(is_themed, self.storage.themed(asset, self.enabled_theme))
+
+    @override_settings(DEBUG=True)
+    @ddt.data(
+        ("images/logo.png", ),
+        ("images/favicon.ico", ),
+    )
+    @ddt.unpack
+    def test_url(self, asset):
+        """
+        Verify storage returns correct url depending upon the enabled theme
+        """
+        with patch(
+            "openedx.core.djangoapps.theming.storage.get_current_site_theme_dir",
+            return_value=self.enabled_theme,
+        ):
+            asset_url = self.storage.url(asset)
+            # remove hash key from file url
+            asset_url = re.sub(r"(\.\w+)(\.png|\.ico)$", r"\g<2>", asset_url)
+            expected_url = self.storage.base_url + self.enabled_theme + "/" + asset
+
+            self.assertEqual(asset_url, expected_url)
+
+    @override_settings(DEBUG=True)
+    @ddt.data(
+        ("images/logo.png", ),
+        ("images/favicon.ico", ),
+    )
+    @ddt.unpack
+    def test_path(self, asset):
+        """
+        Verify storage returns correct file path depending upon the enabled theme
+        """
+        with patch(
+            "openedx.core.djangoapps.theming.storage.get_current_site_theme_dir",
+            return_value=self.enabled_theme,
+        ):
+            returned_path = self.storage.path(asset)
+            expected_path = self.themes_dir / self.enabled_theme / "lms/static/" / asset
+
+            self.assertEqual(expected_path, returned_path)
diff --git a/openedx/core/djangoapps/theming/tests/test_theme_style_overrides.py b/openedx/core/djangoapps/theming/tests/test_theme_style_overrides.py
new file mode 100644
index 00000000000..b7852249d96
--- /dev/null
+++ b/openedx/core/djangoapps/theming/tests/test_theme_style_overrides.py
@@ -0,0 +1,235 @@
+"""
+    Tests for comprehensive themes.
+"""
+import unittest
+
+from django.conf import settings
+from django.test import TestCase, override_settings
+from django.contrib import staticfiles
+
+from paver.easy import call_task
+
+from openedx.core.djangoapps.theming.test_util import with_comprehensive_theme
+
+
+@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+class TestComprehensiveThemeLMS(TestCase):
+    """
+    Test html, sass and static file overrides for comprehensive themes.
+    """
+
+    def setUp(self):
+        """
+        Clear static file finders cache and register cleanup methods.
+        """
+        super(TestComprehensiveThemeLMS, self).setUp()
+
+        # Clear the internal staticfiles caches, to get test isolation.
+        staticfiles.finders.get_finder.cache_clear()
+
+    @classmethod
+    def setUpClass(cls):
+        """
+        Enable Comprehensive theme and compile sass files.
+        """
+        # Apply Comprehensive theme and compile sass assets.
+        compile_sass('lms')
+
+        super(TestComprehensiveThemeLMS, cls).setUpClass()
+
+    @override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
+    @with_comprehensive_theme(settings.TEST_THEME.basename())
+    def test_footer(self):
+        """
+        Test that theme footer is used instead of default footer.
+        """
+        resp = self.client.get('/')
+        self.assertEqual(resp.status_code, 200)
+        # This string comes from header.html of test-theme
+        self.assertContains(resp, "This is a footer for test-theme.")
+
+    @override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
+    @with_comprehensive_theme(settings.TEST_THEME.basename())
+    def test_logo_image(self):
+        """
+        Test that theme logo is used instead of default logo.
+        """
+        result = staticfiles.finders.find('test-theme/images/logo.png')
+        self.assertEqual(result, settings.TEST_THEME / 'lms/static/images/logo.png')
+
+    @override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
+    @with_comprehensive_theme(settings.TEST_THEME.basename())
+    def test_css_files(self):
+        """
+        Test that theme sass files are used instead of default sass files.
+        """
+        result = staticfiles.finders.find('test-theme/css/lms-main.css')
+        self.assertEqual(result, settings.TEST_THEME / "lms/static/css/lms-main.css")
+
+        lms_main_css = ""
+        with open(result) as css_file:
+            lms_main_css += css_file.read()
+
+        self.assertIn("background:#00fa00", lms_main_css)
+
+
+@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
+class TestComprehensiveThemeCMS(TestCase):
+    """
+    Test html, sass and static file overrides for comprehensive themes.
+    """
+
+    def setUp(self):
+        """
+        Clear static file finders cache and register cleanup methods.
+        """
+        super(TestComprehensiveThemeCMS, self).setUp()
+
+        # Clear the internal staticfiles caches, to get test isolation.
+        staticfiles.finders.get_finder.cache_clear()
+
+    @classmethod
+    def setUpClass(cls):
+        """
+        Enable Comprehensive theme and compile sass files.
+        """
+        # Apply Comprehensive theme and compile sass assets.
+        compile_sass('cms')
+
+        super(TestComprehensiveThemeCMS, cls).setUpClass()
+
+    @override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
+    @with_comprehensive_theme(settings.TEST_THEME.basename())
+    def test_template_override(self):
+        """
+        Test that theme templates are used instead of default templates.
+        """
+        resp = self.client.get('/signin')
+        self.assertEqual(resp.status_code, 200)
+        # This string comes from login.html of test-theme
+        self.assertContains(resp, "Login Page override for test-theme.")
+
+    @override_settings(COMPREHENSIVE_THEME_DIR=settings.TEST_THEME.dirname())
+    @with_comprehensive_theme(settings.TEST_THEME.basename())
+    def test_css_files(self):
+        """
+        Test that theme sass files are used instead of default sass files.
+        """
+        result = staticfiles.finders.find('test-theme/css/studio-main.css')
+        self.assertEqual(result, settings.TEST_THEME / "cms/static/css/studio-main.css")
+
+        cms_main_css = ""
+        with open(result) as css_file:
+            cms_main_css += css_file.read()
+
+        self.assertIn("background:#00fa00", cms_main_css)
+
+
+@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+class TestComprehensiveThemeDisabledLMS(TestCase):
+    """
+        Test Sass compilation order and sass overrides for comprehensive themes.
+    """
+
+    def setUp(self):
+        """
+        Clear static file finders cache.
+        """
+        super(TestComprehensiveThemeDisabledLMS, self).setUp()
+
+        # Clear the internal staticfiles caches, to get test isolation.
+        staticfiles.finders.get_finder.cache_clear()
+
+    @classmethod
+    def setUpClass(cls):
+        """
+        Compile sass files.
+        """
+        # compile LMS SASS
+        compile_sass('lms')
+
+        super(TestComprehensiveThemeDisabledLMS, cls).setUpClass()
+
+    def test_logo(self):
+        """
+        Test that default logo is picked in case of no comprehensive theme.
+        """
+        result = staticfiles.finders.find('images/logo.png')
+        self.assertEqual(result, settings.REPO_ROOT / 'lms/static/images/logo.png')
+
+    def test_css(self):
+        """
+        Test that default css files served without comprehensive themes applied.
+        """
+        result = staticfiles.finders.find('css/lms-main.css')
+        self.assertEqual(result, settings.REPO_ROOT / "lms/static/css/lms-main.css")
+
+        lms_main_css = ""
+        with open(result) as css_file:
+            lms_main_css += css_file.read()
+
+        self.assertNotIn("background:#00fa00", lms_main_css)
+
+
+@unittest.skipUnless(settings.ROOT_URLCONF == 'cms.urls', 'Test only valid in cms')
+class TestComprehensiveThemeDisabledCMS(TestCase):
+    """
+    Test default html, sass and static file when no theme is applied.
+    """
+
+    def setUp(self):
+        """
+        Clear static file finders cache and register cleanup methods.
+        """
+        super(TestComprehensiveThemeDisabledCMS, self).setUp()
+
+        # Clear the internal staticfiles caches, to get test isolation.
+        staticfiles.finders.get_finder.cache_clear()
+
+    @classmethod
+    def setUpClass(cls):
+        """
+        Enable Comprehensive theme and compile sass files.
+        """
+        # Apply Comprehensive theme and compile sass assets.
+        compile_sass('cms')
+
+        super(TestComprehensiveThemeDisabledCMS, cls).setUpClass()
+
+    def test_template_override(self):
+        """
+        Test that defaults templates are used when no theme is applied.
+        """
+        resp = self.client.get('/signin')
+        self.assertEqual(resp.status_code, 200)
+        self.assertNotContains(resp, "Login Page override for test-theme.")
+
+    def test_css_files(self):
+        """
+        Test that default css files served without comprehensive themes applied..
+        """
+        result = staticfiles.finders.find('css/studio-main.css')
+        self.assertEqual(result, settings.REPO_ROOT / "cms/static/css/studio-main.css")
+
+        cms_main_css = ""
+        with open(result) as css_file:
+            cms_main_css += css_file.read()
+
+        self.assertNotIn("background:#00fa00", cms_main_css)
+
+
+def compile_sass(system):
+    """
+    Process xmodule assets and compile sass files for the given system.
+
+    :param system - 'lms' or 'cms', specified the system to compile sass for.
+    """
+    # Compile system sass files
+    call_task(
+        'pavelib.assets.update_assets',
+        args=(
+            system,
+            "--themes_dir={}".format(settings.TEST_THEME.dirname()),
+            "--themes={}".format(settings.TEST_THEME.basename()),
+            "--settings=test"),
+    )
diff --git a/openedx/core/lib/tempdir.py b/openedx/core/lib/tempdir.py
index 8d440ad14c3..4932f529564 100644
--- a/openedx/core/lib/tempdir.py
+++ b/openedx/core/lib/tempdir.py
@@ -17,3 +17,22 @@ def cleanup_tempdir(the_dir):
     """Called on process exit to remove a temp directory."""
     if os.path.exists(the_dir):
         shutil.rmtree(the_dir)
+
+
+def create_symlink(src, dest):
+    """
+    Creates a symbolic link which will be deleted when the process ends.
+    :param src: path to source
+    :param dest: path to destination
+    """
+    os.symlink(src, dest)
+    atexit.register(delete_symlink, dest)
+
+
+def delete_symlink(link_path):
+    """
+    Removes symbolic link for
+    :param link_path:
+    """
+    if os.path.exists(link_path):
+        os.remove(link_path)
diff --git a/openedx/core/storage.py b/openedx/core/storage.py
index ff176947075..7a2338ec37c 100644
--- a/openedx/core/storage.py
+++ b/openedx/core/storage.py
@@ -1,15 +1,18 @@
 """
 Django storage backends for Open edX.
 """
-from django.contrib.staticfiles.storage import StaticFilesStorage, CachedFilesMixin
-from pipeline.storage import PipelineMixin, NonPackagingMixin
+from django.contrib.staticfiles.storage import StaticFilesStorage
+from pipeline.storage import NonPackagingMixin
 from require.storage import OptimizedFilesMixin
+from openedx.core.djangoapps.theming.storage import ThemeStorage, ComprehensiveThemingCachedFilesMixin, \
+    ThemePipelineMixin
 
 
 class ProductionStorage(
         OptimizedFilesMixin,
-        PipelineMixin,
-        CachedFilesMixin,
+        ThemePipelineMixin,
+        ComprehensiveThemingCachedFilesMixin,
+        ThemeStorage,
         StaticFilesStorage
 ):
     """
@@ -21,7 +24,8 @@ class ProductionStorage(
 
 class DevelopmentStorage(
         NonPackagingMixin,
-        PipelineMixin,
+        ThemePipelineMixin,
+        ThemeStorage,
         StaticFilesStorage
 ):
     """
diff --git a/pavelib/assets.py b/pavelib/assets.py
index 208292be022..4670a196fe6 100644
--- a/pavelib/assets.py
+++ b/pavelib/assets.py
@@ -20,20 +20,21 @@ from .utils.cmd import cmd, django_cmd
 
 ALL_SYSTEMS = ['lms', 'studio']
 COFFEE_DIRS = ['lms', 'cms', 'common']
-# A list of directories.  Each will be paired with a sibling /css directory.
-COMMON_SASS_DIRECTORIES = [
+
+LMS = 'lms'
+CMS = 'cms'
+
+SYSTEMS = {
+    'lms': LMS,
+    'cms': CMS,
+    'studio': CMS
+}
+
+# Common lookup paths that are added to the lookup paths for all sass compilations
+COMMON_LOOKUP_DIRS = [
+    path("common/static"),
     path("common/static/sass"),
 ]
-LMS_SASS_DIRECTORIES = [
-    path("lms/static/sass"),
-    path("lms/static/themed_sass"),
-    path("lms/static/certificates/sass"),
-]
-CMS_SASS_DIRECTORIES = [
-    path("cms/static/sass"),
-]
-THEME_SASS_DIRECTORIES = []
-SASS_LOAD_PATHS = ['common/static', 'common/static/sass']
 
 # A list of NPM installed libraries that should be copied into the common
 # static directory.
@@ -44,60 +45,199 @@ NPM_INSTALLED_LIBRARIES = [
 # Directory to install static vendor files
 NPM_VENDOR_DIRECTORY = path("common/static/common/js/vendor")
 
+# system specific lookup path additions, add sass dirs if one system depends on the sass files for other systems
+SASS_LOOKUP_DEPENDENCIES = {
+    'cms': [path('lms') / 'static' / 'sass' / 'partials', ],
+}
 
-def configure_paths():
-    """Configure our paths based on settings.  Called immediately."""
-    edxapp_env = Env()
-    if edxapp_env.feature_flags.get('USE_CUSTOM_THEME', False):
-        theme_name = edxapp_env.env_tokens.get('THEME_NAME', '')
-        parent_dir = path(edxapp_env.REPO_ROOT).abspath().parent
-        theme_root = parent_dir / "themes" / theme_name
-        COFFEE_DIRS.append(theme_root)
-        sass_dir = theme_root / "static" / "sass"
-        css_dir = theme_root / "static" / "css"
-        if sass_dir.isdir():
-            css_dir.mkdir_p()
-            THEME_SASS_DIRECTORIES.append(sass_dir)
-
-    if edxapp_env.env_tokens.get("COMPREHENSIVE_THEME_DIR", ""):
-        theme_dir = path(edxapp_env.env_tokens["COMPREHENSIVE_THEME_DIR"])
-        lms_sass = theme_dir / "lms" / "static" / "sass"
-        lms_css = theme_dir / "lms" / "static" / "css"
-        if lms_sass.isdir():
-            lms_css.mkdir_p()
-            THEME_SASS_DIRECTORIES.append(lms_sass)
-        cms_sass = theme_dir / "cms" / "static" / "sass"
-        cms_css = theme_dir / "cms" / "static" / "css"
-        if cms_sass.isdir():
-            cms_css.mkdir_p()
-            THEME_SASS_DIRECTORIES.append(cms_sass)
-
-configure_paths()
-
-
-def applicable_sass_directories(systems=None):
-    """
-    Determine the applicable set of SASS directories to be
-    compiled for the specified list of systems.
-
-    Args:
-        systems: A list of systems (defaults to all)
 
-    Returns:
-        A list of SASS directories to be compiled.
-    """
-    if not systems:
-        systems = ALL_SYSTEMS
-    applicable_directories = []
-    applicable_directories.extend(COMMON_SASS_DIRECTORIES)
-    if "lms" in systems:
-        applicable_directories.extend(LMS_SASS_DIRECTORIES)
-    if "studio" in systems or "cms" in systems:
-        applicable_directories.extend(CMS_SASS_DIRECTORIES)
-    applicable_directories.extend(THEME_SASS_DIRECTORIES)
+def get_sass_directories(system, theme_dir=None):
+    """
+    Determine the set of SASS directories to be compiled for the specified list of system and theme
+    and return a list of those directories.
+
+    Each item in the list is dict object containing the following key-value pairs.
+    {
+        "sass_source_dir": "",  # directory where source sass files are present
+        "css_destination_dir": "",  # destination where css files would be placed
+        "lookup_paths": [],  # list of directories to be passed as lookup paths for @import resolution.
+    }
+
+    if theme_dir is empty or None then return sass directories for the given system only. (i.e. lms or cms)
+
+    :param system: name if the system for which to compile sass e.g. 'lms', 'cms'
+    :param theme_dir: absolute path of theme for which to compile sass files.
+    """
+    if system not in SYSTEMS:
+        raise ValueError("'system' must be one of ({allowed_values})".format(allowed_values=', '.join(SYSTEMS.keys())))
+    system = SYSTEMS[system]
+
+    applicable_directories = list()
+
+    if theme_dir:
+        # Add theme sass directories
+        applicable_directories.extend(
+            get_theme_sass_dirs(system, theme_dir)
+        )
+    else:
+        # add system sass directories
+        applicable_directories.extend(
+            get_system_sass_dirs(system)
+        )
+
     return applicable_directories
 
 
+def get_common_sass_directories():
+    """
+    Determine the set of common SASS directories to be compiled for all the systems and themes.
+
+    Each item in the returned list is dict object containing the following key-value pairs.
+    {
+        "sass_source_dir": "",  # directory where source sass files are present
+        "css_destination_dir": "",  # destination where css files would be placed
+        "lookup_paths": [],  # list of directories to be passed as lookup paths for @import resolution.
+    }
+    """
+    applicable_directories = list()
+
+    # add common sass directories
+    applicable_directories.append({
+        "sass_source_dir": path("common/static/sass"),
+        "css_destination_dir": path("common/static/css"),
+        "lookup_paths": [
+            path("common/static"),
+            path("common/static/sass"),
+        ],
+    })
+
+    return applicable_directories
+
+
+def get_theme_sass_dirs(system, theme_dir):
+    """
+    Return list of sass dirs that need to be compiled for the given theme.
+
+    :param system: name if the system for which to compile sass e.g. 'lms', 'cms'
+    :param theme_dir: absolute path of theme for which to compile sass files.
+    """
+    if system not in ('lms', 'cms'):
+        raise ValueError('"system" must either be "lms" or "cms"')
+
+    dirs = []
+
+    system_sass_dir = path(system) / "static" / "sass"
+    sass_dir = theme_dir / system / "static" / "sass"
+    css_dir = theme_dir / system / "static" / "css"
+
+    dependencies = SASS_LOOKUP_DEPENDENCIES.get(system, [])
+    if sass_dir.isdir():
+        css_dir.mkdir_p()
+
+        # first compile lms sass files and place css in theme dir
+        dirs.append({
+            "sass_source_dir": system_sass_dir,
+            "css_destination_dir": css_dir,
+            "lookup_paths": dependencies + [
+                sass_dir / "partials",
+                system_sass_dir / "partials",
+                system_sass_dir,
+            ],
+        })
+
+        # now compile theme sass files and override css files generated from lms
+        dirs.append({
+            "sass_source_dir": sass_dir,
+            "css_destination_dir": css_dir,
+            "lookup_paths": dependencies + [
+                sass_dir / "partials",
+                system_sass_dir / "partials",
+                system_sass_dir,
+            ],
+        })
+
+    return dirs
+
+
+def get_system_sass_dirs(system):
+    """
+    Return list of sass dirs that need to be compiled for the given system.
+
+    :param system: name if the system for which to compile sass e.g. 'lms', 'cms'
+    """
+    if system not in ('lms', 'cms'):
+        raise ValueError('"system" must either be "lms" or "cms"')
+
+    dirs = []
+    sass_dir = path(system) / "static" / "sass"
+    css_dir = path(system) / "static" / "css"
+
+    dependencies = SASS_LOOKUP_DEPENDENCIES.get(system, [])
+    dirs.append({
+        "sass_source_dir": sass_dir,
+        "css_destination_dir": css_dir,
+        "lookup_paths": dependencies + [
+            sass_dir / "partials",
+            sass_dir,
+        ],
+    })
+
+    if system == 'lms':
+        dirs.append({
+            "sass_source_dir": path(system) / "static" / "certificates" / "sass",
+            "css_destination_dir": path(system) / "static" / "certificates" / "css",
+            "lookup_paths": [
+                sass_dir / "partials",
+                sass_dir
+            ],
+        })
+
+    return dirs
+
+
+def get_watcher_dirs(themes_base_dir=None, themes=None):
+    """
+    Return sass directories that need to be added to sass watcher.
+
+    Example:
+        >> get_watcher_dirs('/edx/app/edx-platform/themes', ['red-theme'])
+        [
+            'common/static',
+            'common/static/sass',
+            'lms/static/sass',
+            'lms/static/sass/partials',
+            '/edx/app/edxapp/edx-platform/themes/red-theme/lms/static/sass',
+            '/edx/app/edxapp/edx-platform/themes/red-theme/lms/static/sass/partials',
+            'cms/static/sass',
+            'cms/static/sass/partials',
+            '/edx/app/edxapp/edx-platform/themes/red-theme/cms/static/sass/partials',
+        ]
+
+    Parameters:
+        themes_base_dir (str): base directory that contains all the themes.
+        themes (list): list containing names of themes
+    Returns:
+        (list): dirs that need to be added to sass watchers.
+    """
+    dirs = []
+    dirs.extend(COMMON_LOOKUP_DIRS)
+    if themes_base_dir and themes:
+        # Register sass watchers for all the given themes
+        theme_dirs = [(path(themes_base_dir) / theme) for theme in themes if theme]
+        for theme_dir in theme_dirs:
+            for _dir in get_sass_directories('lms', theme_dir) + get_sass_directories('cms', theme_dir):
+                dirs.append(_dir['sass_source_dir'])
+                dirs.extend(_dir['lookup_paths'])
+    # Register sass watchers for lms and cms
+    for _dir in get_sass_directories('lms') + get_sass_directories('cms') + get_common_sass_directories():
+        dirs.append(_dir['sass_source_dir'])
+        dirs.extend(_dir['lookup_paths'])
+
+    # remove duplicates
+    dirs = list(set(dirs))
+    return dirs
+
+
 class CoffeeScriptWatcher(PatternMatchingEventHandler):
     """
     Watches for coffeescript changes
@@ -131,11 +271,15 @@ class SassWatcher(PatternMatchingEventHandler):
     patterns = ['*.scss']
     ignore_patterns = ['common/static/xmodule/*']
 
-    def register(self, observer):
+    def register(self, observer, directories):
         """
         register files with observer
+
+        Arguments:
+            observer (watchdog.observers.Observer): sass file observer
+            directories (list): list of directories to be register for sass watcher.
         """
-        for dirname in SASS_LOAD_PATHS + applicable_sass_directories():
+        for dirname in directories:
             paths = []
             if '*' in dirname:
                 paths.extend(glob.glob(dirname))
@@ -159,12 +303,6 @@ class XModuleSassWatcher(SassWatcher):
     ignore_directories = True
     ignore_patterns = []
 
-    def register(self, observer):
-        """
-        register files with observer
-        """
-        observer.schedule(self, 'common/lib/xmodule/', recursive=True)
-
     def on_modified(self, event):
         print('\tCHANGED:', event.src_path)
         try:
@@ -222,12 +360,125 @@ def compile_coffeescript(*files):
 @no_help
 @cmdopts([
     ('system=', 's', 'The system to compile sass for (defaults to all)'),
+    ('themes_dir=', '-td', 'The themes dir containing all themes (defaults to None)'),
+    ('themes=', '-t', 'The theme to compile sass for (defaults to None)'),
     ('debug', 'd', 'Debug mode'),
     ('force', '', 'Force full compilation'),
 ])
 def compile_sass(options):
     """
-    Compile Sass to CSS.
+    Compile Sass to CSS. If command is called without any arguments, it will
+    only compile lms, cms sass for the open source theme. And none of the comprehensive theme's sass would be compiled.
+
+    If you want to compile sass for all comprehensive themes you will have to run compile_sass
+    specifying all the themes that need to be compiled..
+
+    The following is a list of some possible ways to use this command.
+
+    Command:
+        paver compile_sass
+    Description:
+        compile sass files for both lms and cms. If command is called like above (i.e. without any arguments) it will
+        only compile lms, cms sass for the open source theme. None of the theme's sass will be compiled.
+
+    Command:
+        paver compile_sass --themes_dir=/edx/app/edxapp/edx-platform/themes --themes=red-theme
+    Description:
+        compile sass files for both lms and cms for 'red-theme' present in '/edx/app/edxapp/edx-platform/themes'
+
+    Command:
+        paver compile_sass --themes_dir=/edx/app/edxapp/edx-platform/themes --themes=red-theme,stanford-style
+    Description:
+        compile sass files for both lms and cms for 'red-theme' and 'stanford-style' present in
+        '/edx/app/edxapp/edx-platform/themes'.
+
+    Command:
+        paver compile_sass --system=cms --themes_dir=/edx/app/edxapp/edx-platform/themes
+            --themes=red-theme,stanford-style
+    Description:
+        compile sass files for cms only for 'red-theme' and 'stanford-style' present in
+        '/edx/app/edxapp/edx-platform/themes'.
+
+    """
+    debug = options.get('debug')
+    force = options.get('force')
+    systems = getattr(options, 'system', ALL_SYSTEMS)
+    themes = getattr(options, 'themes', None)
+    themes_dir = getattr(options, 'themes_dir', None)
+
+    if not themes_dir and themes:
+        # We can not compile a theme sass without knowing the directory that contains the theme.
+        raise ValueError('themes_dir must be provided for compiling theme sass.')
+    else:
+        theme_base_dir = path(themes_dir)
+
+    if isinstance(systems, basestring):
+        systems = systems.split(',')
+    else:
+        systems = systems if isinstance(systems, list) else [systems]
+    if isinstance(themes, basestring):
+        themes = themes.split(',')
+    else:
+        themes = themes if isinstance(themes, list) else [themes]
+
+    # Compile sass for OpenEdx theme after comprehensive themes
+    if None not in themes:
+        themes.append(None)
+
+    timing_info = []
+    dry_run = tasks.environment.dry_run
+    compilation_results = {'success': [], 'failure': []}
+
+    print("\t\tStarted compiling Sass:")
+
+    # compile common sass files
+    is_successful = _compile_sass('common', None, debug, force, timing_info)
+    if is_successful:
+        print("Finished compiling 'common' sass.")
+    compilation_results['success' if is_successful else 'failure'].append('"common" sass files.')
+
+    for system in systems:
+        for theme in themes:
+            print("Started compiling '{system}' Sass for '{theme}'.".format(system=system, theme=theme or 'system'))
+
+            # Compile sass files
+            is_successful = _compile_sass(
+                system=system,
+                theme=theme_base_dir / theme if theme_base_dir and theme else None,
+                debug=debug,
+                force=force,
+                timing_info=timing_info
+            )
+
+            if is_successful:
+                print("Finished compiling '{system}' Sass for '{theme}'.".format(
+                    system=system, theme=theme or 'system'
+                ))
+
+            compilation_results['success' if is_successful else 'failure'].append('{system} sass for {theme}.'.format(
+                system=system, theme=theme or 'system',
+            ))
+
+    print("\t\tFinished compiling Sass:")
+    if not dry_run:
+        for sass_dir, css_dir, duration in timing_info:
+            print(">> {} -> {} in {}s".format(sass_dir, css_dir, duration))
+
+    if compilation_results['success']:
+        print("\033[92m\n\nSuccessful compilations:\n--- " + "\n--- ".join(compilation_results['success']) + "\033[00m")
+    if compilation_results['failure']:
+        print("\033[91m\n\nFailed compilations:\n--- " + "\n--- ".join(compilation_results['failure']) + "\033[00m")
+
+
+def _compile_sass(system, theme, debug, force, timing_info):
+    """
+    Compile sass files for the given system and theme.
+
+    :param system: system to compile sass for e.g. 'lms', 'cms', 'common'
+    :param theme: absolute path of the theme to compile sass for.
+    :param debug: boolean showing whether to display source comments in resulted css
+    :param force: boolean showing whether to remove existing css files before generating new files
+    :param timing_info: list variable to keep track of timing for sass compilation
     """
 
     # Note: import sass only when it is needed and not at the top of the file.
@@ -235,12 +486,14 @@ def compile_sass(options):
     # installed. In particular, this allows the install_prereqs command to be
     # used to install the dependency.
     import sass
+    if system == "common":
+        sass_dirs = get_common_sass_directories()
+    else:
+        sass_dirs = get_sass_directories(system, theme)
 
-    debug = options.get('debug')
-    force = options.get('force')
-    systems = getattr(options, 'system', ALL_SYSTEMS)
-    if isinstance(systems, basestring):
-        systems = systems.split(',')
+    dry_run = tasks.environment.dry_run
+
+    # determine css out put style and source comments enabling
     if debug:
         source_comments = True
         output_style = 'nested'
@@ -248,13 +501,18 @@ def compile_sass(options):
         source_comments = False
         output_style = 'compressed'
 
-    timing_info = []
-    system_sass_directories = applicable_sass_directories(systems)
-    all_sass_directories = applicable_sass_directories()
-    dry_run = tasks.environment.dry_run
-    for sass_dir in system_sass_directories:
+    for dirs in sass_dirs:
         start = datetime.now()
-        css_dir = sass_dir.parent / "css"
+        css_dir = dirs['css_destination_dir']
+        sass_source_dir = dirs['sass_source_dir']
+        lookup_paths = dirs['lookup_paths']
+
+        if not sass_source_dir.isdir():
+            print("\033[91m Sass dir '{dir}' does not exists, skipping sass compilation for '{theme}' \033[00m".format(
+                dir=sass_dirs, theme=theme or system,
+            ))
+            # theme doesn't override sass directory, so skip it
+            continue
 
         if force:
             if dry_run:
@@ -266,22 +524,18 @@ def compile_sass(options):
 
         if dry_run:
             tasks.environment.info("libsass {sass_dir}".format(
-                sass_dir=sass_dir,
+                sass_dir=sass_source_dir,
             ))
         else:
             sass.compile(
-                dirname=(sass_dir, css_dir),
-                include_paths=SASS_LOAD_PATHS + all_sass_directories,
+                dirname=(sass_source_dir, css_dir),
+                include_paths=COMMON_LOOKUP_DIRS + lookup_paths,
                 source_comments=source_comments,
                 output_style=output_style,
             )
             duration = datetime.now() - start
-            timing_info.append((sass_dir, css_dir, duration))
-
-    print("\t\tFinished compiling Sass:")
-    if not dry_run:
-        for sass_dir, css_dir, duration in timing_info:
-            print(">> {} -> {} in {}s".format(sass_dir, css_dir, duration))
+            timing_info.append((sass_source_dir, css_dir, duration))
+    return True
 
 
 def compile_templated_sass(systems, settings):
@@ -353,7 +607,11 @@ def collect_assets(systems, settings):
 
 
 @task
-@cmdopts([('background', 'b', 'Background mode')])
+@cmdopts([
+    ('background', 'b', 'Background mode'),
+    ('themes_dir=', '-td', 'The themes dir containing all themes (defaults to None)'),
+    ('themes=', '-t', 'The themes to add sass watchers for (defaults to None)'),
+])
 def watch_assets(options):
     """
     Watch for changes to asset files, and regenerate js/css
@@ -362,11 +620,25 @@ def watch_assets(options):
     if tasks.environment.dry_run:
         return
 
+    themes = getattr(options, 'themes', None)
+    themes_dir = getattr(options, 'themes_dir', None)
+    if not themes_dir and themes:
+        # We can not add theme sass watchers without knowing the directory that contains the themes.
+        raise ValueError('themes_dir must be provided for compiling theme sass.')
+    else:
+        theme_base_dir = path(themes_dir)
+
+    if isinstance(themes, basestring):
+        themes = themes.split(',')
+    else:
+        themes = themes if isinstance(themes, list) else [themes]
+
+    sass_directories = get_watcher_dirs(theme_base_dir, themes)
     observer = Observer()
 
     CoffeeScriptWatcher().register(observer)
-    SassWatcher().register(observer)
-    XModuleSassWatcher().register(observer)
+    SassWatcher().register(observer, sass_directories)
+    XModuleSassWatcher().register(observer, ['common/lib/xmodule/'])
     XModuleAssetsWatcher().register(observer)
 
     print("Starting asset watcher...")
@@ -412,16 +684,31 @@ def update_assets(args):
         '--watch', action='store_true', default=False,
         help="Watch files for changes",
     )
+    parser.add_argument(
+        '--themes_dir', type=str, default=None,
+        help="base directory where themes are placed",
+    )
+    parser.add_argument(
+        '--themes', type=str, nargs='*', default=None,
+        help="list of themes to compile sass for",
+    )
     args = parser.parse_args(args)
 
     compile_templated_sass(args.system, args.settings)
     process_xmodule_assets()
     process_npm_assets()
     compile_coffeescript()
-    call_task('pavelib.assets.compile_sass', options={'system': args.system, 'debug': args.debug})
+
+    call_task(
+        'pavelib.assets.compile_sass',
+        options={'system': args.system, 'debug': args.debug, 'themes_dir': args.themes_dir, 'themes': args.themes},
+    )
 
     if args.collect:
         collect_assets(args.system, args.settings)
 
     if args.watch:
-        call_task('pavelib.assets.watch_assets', options={'background': not args.debug})
+        call_task(
+            'pavelib.assets.watch_assets',
+            options={'background': not args.debug, 'themes_dir': args.themes_dir, 'themes': args.themes},
+        )
diff --git a/pavelib/paver_tests/test_assets.py b/pavelib/paver_tests/test_assets.py
index b690b6d0fe8..d94bf75e45b 100644
--- a/pavelib/paver_tests/test_assets.py
+++ b/pavelib/paver_tests/test_assets.py
@@ -1,10 +1,17 @@
 """Unit tests for the Paver asset tasks."""
 
 import ddt
+import os
+from unittest import TestCase
 from paver.easy import call_task
-
+from paver.easy import path
+from mock import patch
+from watchdog.observers import Observer
 from .utils import PaverTestCase
 
+ROOT_PATH = path(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
+TEST_THEME = ROOT_PATH / "common/test/test-theme"  # pylint: disable=invalid-name
+
 
 @ddt.ddt
 class TestPaverAssetTasks(PaverTestCase):
@@ -41,18 +48,157 @@ class TestPaverAssetTasks(PaverTestCase):
         if force:
             expected_messages.append("rm -rf common/static/css/*.css")
         expected_messages.append("libsass common/static/sass")
+
         if "lms" in system:
             if force:
                 expected_messages.append("rm -rf lms/static/css/*.css")
             expected_messages.append("libsass lms/static/sass")
+            if force:
+                expected_messages.append("rm -rf lms/static/certificates/css/*.css")
+            expected_messages.append("libsass lms/static/certificates/sass")
+        if "studio" in system:
+            if force:
+                expected_messages.append("rm -rf cms/static/css/*.css")
+            expected_messages.append("libsass cms/static/sass")
+
+        self.assertEquals(self.task_messages, expected_messages)
+
+
+@ddt.ddt
+class TestPaverThemeAssetTasks(PaverTestCase):
+    """
+    Test the Paver asset tasks.
+    """
+
+    @ddt.data(
+        [""],
+        ["--force"],
+        ["--debug"],
+        ["--system=lms"],
+        ["--system=lms --force"],
+        ["--system=studio"],
+        ["--system=studio --force"],
+        ["--system=lms,studio"],
+        ["--system=lms,studio --force"],
+    )
+    @ddt.unpack
+    def test_compile_theme_sass(self, options):
+        """
+        Test the "compile_sass" task.
+        """
+        parameters = options.split(" ")
+        system = []
+
+        if "--system=studio" not in parameters:
+            system += ["lms"]
+        if "--system=lms" not in parameters:
+            system += ["studio"]
+        debug = "--debug" in parameters
+        force = "--force" in parameters
+
+        self.reset_task_messages()
+        call_task(
+            'pavelib.assets.compile_sass',
+            options={"system": system, "debug": debug, "force": force, "themes_dir": TEST_THEME.dirname(),
+                     "themes": [TEST_THEME.basename()]},
+        )
+        expected_messages = []
+        if force:
+            expected_messages.append("rm -rf common/static/css/*.css")
+        expected_messages.append("libsass common/static/sass")
+
+        if "lms" in system:
+            expected_messages.append("mkdir_p " + repr(TEST_THEME / "lms/static/css"))
+
+            if force:
+                expected_messages.append("rm -rf " + str(TEST_THEME) + "/lms/static/css/*.css")
+            expected_messages.append("libsass lms/static/sass")
+            if force:
+                expected_messages.append("rm -rf " + str(TEST_THEME) + "/lms/static/css/*.css")
+            expected_messages.append("libsass " + str(TEST_THEME) + "/lms/static/sass")
             if force:
                 expected_messages.append("rm -rf lms/static/css/*.css")
-            expected_messages.append("libsass lms/static/themed_sass")
+            expected_messages.append("libsass lms/static/sass")
             if force:
                 expected_messages.append("rm -rf lms/static/certificates/css/*.css")
             expected_messages.append("libsass lms/static/certificates/sass")
+
         if "studio" in system:
+            expected_messages.append("mkdir_p " + repr(TEST_THEME / "cms/static/css"))
+            if force:
+                expected_messages.append("rm -rf " + str(TEST_THEME) + "/cms/static/css/*.css")
+            expected_messages.append("libsass cms/static/sass")
+            if force:
+                expected_messages.append("rm -rf " + str(TEST_THEME) + "/cms/static/css/*.css")
+            expected_messages.append("libsass " + str(TEST_THEME) + "/cms/static/sass")
+
             if force:
                 expected_messages.append("rm -rf cms/static/css/*.css")
             expected_messages.append("libsass cms/static/sass")
+
         self.assertEquals(self.task_messages, expected_messages)
+
+
+class TestPaverWatchAssetTasks(TestCase):
+    """
+    Test the Paver watch asset tasks.
+    """
+
+    def setUp(self):
+        self.expected_sass_directories = [
+            path('common/static/sass'),
+            path('common/static'),
+            path('lms/static/sass/partials'),
+            path('lms/static/sass'),
+            path('lms/static/certificates/sass'),
+            path('cms/static/sass'),
+            path('cms/static/sass/partials'),
+        ]
+        super(TestPaverWatchAssetTasks, self).setUp()
+
+    def test_watch_assets(self):
+        """
+        Test the "compile_sass" task.
+        """
+        with patch('pavelib.assets.SassWatcher.register') as mock_register:
+            with patch('pavelib.assets.Observer.start'):
+                call_task(
+                    'pavelib.assets.watch_assets',
+                    options={"background": True},
+                )
+                self.assertEqual(mock_register.call_count, 2)
+
+                sass_watcher_args = mock_register.call_args_list[0][0]
+
+                self.assertIsInstance(sass_watcher_args[0], Observer)
+                self.assertIsInstance(sass_watcher_args[1], list)
+                self.assertItemsEqual(sass_watcher_args[1], self.expected_sass_directories)
+
+    def test_watch_theme_assets(self):
+        """
+        Test the Paver watch asset tasks with theming enabled.
+        """
+        self.expected_sass_directories.extend([
+            path(TEST_THEME) / 'lms/static/sass',
+            path(TEST_THEME) / 'lms/static/sass/partials',
+            path(TEST_THEME) / 'cms/static/sass',
+            path(TEST_THEME) / 'cms/static/sass/partials',
+        ])
+
+        with patch('pavelib.assets.SassWatcher.register') as mock_register:
+            with patch('pavelib.assets.Observer.start'):
+                call_task(
+                    'pavelib.assets.watch_assets',
+                    options={"background": True, "themes_dir": TEST_THEME.dirname(),
+                             "themes": [TEST_THEME.basename()]},
+                )
+                self.assertEqual(mock_register.call_count, 2)
+
+                sass_watcher_args = mock_register.call_args_list[0][0]
+                self.assertIsInstance(sass_watcher_args[0], Observer)
+                self.assertIsInstance(sass_watcher_args[1], list)
+                self.assertItemsEqual(sass_watcher_args[1], self.expected_sass_directories)
+
+    def tearDown(self):
+        self.expected_sass_directories = []
+        super(TestPaverWatchAssetTasks, self).tearDown()
diff --git a/pavelib/paver_tests/test_servers.py b/pavelib/paver_tests/test_servers.py
index 126f016670d..812dd58d7ac 100644
--- a/pavelib/paver_tests/test_servers.py
+++ b/pavelib/paver_tests/test_servers.py
@@ -17,7 +17,6 @@ EXPECTED_COMMON_SASS_DIRECTORIES = [
 ]
 EXPECTED_LMS_SASS_DIRECTORIES = [
     u"lms/static/sass",
-    u"lms/static/themed_sass",
     u"lms/static/certificates/sass",
 ]
 EXPECTED_CMS_SASS_DIRECTORIES = [
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index eb3d248b0a3..2a76049177c 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -173,3 +173,6 @@ jsonfield==1.0.3
 
 # Inlines CSS styles into HTML for email notifications.
 pynliner==0.5.2
+
+# django current site middleware with default site
+edx-django-sites-extensions==1.0.0
diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt
index 5f0ae78bcae..4594eee30a3 100644
--- a/requirements/edx/github.txt
+++ b/requirements/edx/github.txt
@@ -45,7 +45,7 @@
 
 # Third-party:
 git+https://github.com/cyberdelia/django-pipeline.git@1.5.3#egg=django-pipeline==1.5.3
-git+https://github.com/edx/django-wiki.git@v0.0.5#egg=django-wiki==0.0.5
+git+https://github.com/edx/django-wiki.git@v0.0.7#egg=django-wiki==0.0.7
 git+https://github.com/edx/django-openid-auth.git@0.8#egg=django-openid-auth==0.8
 git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7fed0#egg=MongoDBProxy==0.1.0
 git+https://github.com/edx/nltk.git@2.0.6#egg=nltk==2.0.6
diff --git a/themes/red-theme/cms/templates/login.html b/themes/red-theme/cms/templates/login.html
new file mode 100644
index 00000000000..70db0d13b33
--- /dev/null
+++ b/themes/red-theme/cms/templates/login.html
@@ -0,0 +1,55 @@
+<%inherit file="base.html" />
+<%def name="online_help_token()"><% return "login" %></%def>
+<%!
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext as _
+%>
+<%block name="title">${_("Sign In")}</%block>
+<%block name="bodyclass">not-signedin view-signin</%block>
+
+<%block name="content">
+<div class="wrapper-content wrapper">
+  <section class="content">
+    <header>
+      <h1 class="title title-1">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</h1>
+      <a href="${reverse('signup')}" class="action action-signin">${_("Don't have a {studio_name} Account? Sign up!").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
+    </header>
+    <!-- Login Page override for red-theme. -->
+    <article class="content-primary" role="main">
+      <form id="login_form" method="post" action="login_post" onsubmit="return false;">
+
+        <fieldset>
+          <legend class="sr">${_("Required Information to Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</legend>
+          <input type="hidden" name="csrfmiddlewaretoken" value="${ csrf }" />
+
+          <ol class="list-input">
+            <li class="field text required" id="field-email">
+              <label for="email">${_("E-mail")}</label>
+              <input id="email" type="email" name="email" placeholder="${_('example: username@domain.com')}"/>
+            </li>
+
+            <li class="field text required" id="field-password">
+              <label for="password">${_("Password")}</label>
+              <input id="password" type="password" name="password" />
+              <a href="${forgot_password_link}" class="action action-forgotpassword">${_("Forgot password?")}</a>
+            </li>
+          </ol>
+        </fieldset>
+
+        <div class="form-actions">
+          <button type="submit" id="submit" name="submit" class="action action-primary">${_("Sign In to {studio_name}").format(studio_name=settings.STUDIO_NAME)}</button>
+        </div>
+
+        <!-- no honor code for CMS, but need it because we're using the lms student object -->
+        <input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
+      </form>
+    </article>
+  </section>
+</div>
+</%block>
+
+<%block name="requirejs">
+  require(["js/factories/login"], function(LoginFactory) {
+      LoginFactory("${reverse('homepage')}");
+  });
+</%block>
diff --git a/themes/red-theme/lms/static/sass/_overrides.scss b/themes/red-theme/lms/static/sass/_overrides.scss
deleted file mode 100755
index 4e5e1f2b6e3..00000000000
--- a/themes/red-theme/lms/static/sass/_overrides.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-// Theming overrides for sample theme
-$header-bg: rgb(250,0,0);
-$footer-bg: rgb(250,0,0);
-$container-bg: rgb(250,0,0);
-$content-wrapper-bg: rgb(250,0,0);
-$serif: 'Comic Sans', 'Comic Sans MS';
-$sans-serif: 'Comic Sans', 'Comic Sans MS';
diff --git a/themes/red-theme/lms/static/sass/lms-main-rtl.scss b/themes/red-theme/lms/static/sass/lms-main-rtl.scss
deleted file mode 100755
index 3eaad226a2b..00000000000
--- a/themes/red-theme/lms/static/sass/lms-main-rtl.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-// Theming overrides for sample theme
-@import 'overrides';
-
-// import the rest of the application
-@import 'lms/static/sass/lms-main-rtl';
diff --git a/themes/red-theme/lms/static/sass/lms-main.scss b/themes/red-theme/lms/static/sass/lms-main.scss
deleted file mode 100755
index d6287e82155..00000000000
--- a/themes/red-theme/lms/static/sass/lms-main.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-// Theming overrides for sample theme
-@import 'overrides';
-
-// import the rest of the application
-@import 'lms/static/sass/lms-main';
diff --git a/themes/red-theme/lms/static/sass/partials/base/_variables.scss b/themes/red-theme/lms/static/sass/partials/base/_variables.scss
new file mode 100755
index 00000000000..c869ff98567
--- /dev/null
+++ b/themes/red-theme/lms/static/sass/partials/base/_variables.scss
@@ -0,0 +1,5 @@
+@import 'lms/static/sass/partials/base/variables';
+
+$header-bg: rgb(250,0,0);
+$footer-bg: rgb(250,0,0);
+$container-bg: rgb(250,0,0);
diff --git a/themes/stanford-style/lms/static/sass/lms-main-rtl.scss b/themes/stanford-style/lms/static/sass/lms-main-rtl.scss
deleted file mode 100755
index 3eaad226a2b..00000000000
--- a/themes/stanford-style/lms/static/sass/lms-main-rtl.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-// Theming overrides for sample theme
-@import 'overrides';
-
-// import the rest of the application
-@import 'lms/static/sass/lms-main-rtl';
diff --git a/themes/stanford-style/lms/static/sass/lms-main.scss b/themes/stanford-style/lms/static/sass/lms-main.scss
deleted file mode 100755
index d6287e82155..00000000000
--- a/themes/stanford-style/lms/static/sass/lms-main.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-// Theming overrides for sample theme
-@import 'overrides';
-
-// import the rest of the application
-@import 'lms/static/sass/lms-main';
diff --git a/themes/stanford-style/lms/static/sass/_overrides.scss b/themes/stanford-style/lms/static/sass/partials/base/_variables.scss
similarity index 82%
rename from themes/stanford-style/lms/static/sass/_overrides.scss
rename to themes/stanford-style/lms/static/sass/partials/base/_variables.scss
index 4bf94c998aa..6453fbb2b96 100755
--- a/themes/stanford-style/lms/static/sass/_overrides.scss
+++ b/themes/stanford-style/lms/static/sass/partials/base/_variables.scss
@@ -1,3 +1,5 @@
+@import 'lms/static/sass/partials/base/variables';
+
 // Theming overrides for sample theme
 $header-bg: rgb(140,21,21);
 $footer-bg: rgb(140,21,21);
-- 
GitLab