diff --git a/Gemfile b/Gemfile index cef97fbc1ba690311d659aaf17bfb108e124b31c..b438bc89e3ec66fd29103053b7355214857c9051 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,3 @@ source 'https://rubygems.org' -gem 'sass', '3.3.5' gem 'bourbon', '~> 4.0.2' gem 'neat', '~> 1.6.0' diff --git a/Gemfile.lock b/Gemfile.lock index 551990d98f6664ad65c2b1072316f08116d87c7f..fb1a1e69f7ee72ecdfc6e58863d53968bf20f149 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,7 +7,7 @@ GEM neat (1.6.0) bourbon (>= 3.1) sass (>= 3.3) - sass (3.3.5) + sass (3.4.21) thor (0.19.1) PLATFORMS @@ -16,4 +16,3 @@ PLATFORMS DEPENDENCIES bourbon (~> 4.0.2) neat (~> 1.6.0) - sass (= 3.3.5) diff --git a/circle.yml b/circle.yml index c37428a7394c00ec18845d0dfd14567d3b612ea3..95549be641a11973554399b2a6b6a9e2f002a37d 100644 --- a/circle.yml +++ b/circle.yml @@ -29,6 +29,7 @@ dependencies: # Install a version which falls within that range. - pip install --exists-action w pbr==0.9.0 - pip install --exists-action w -r requirements/edx/base.txt + - pip install --exists-action w -r requirements/edx/paver.txt - if [ -e requirements/edx/post.txt ]; then pip install --exists-action w -r requirements/edx/post.txt ; fi - pip install coveralls==1.0 diff --git a/pavelib/acceptance_test.py b/pavelib/acceptance_test.py index 373db696edc97c3d16e8d531df40444f2c9b31d4..254af313bdd66f386b5096bc7ce09655d7bb59b6 100644 --- a/pavelib/acceptance_test.py +++ b/pavelib/acceptance_test.py @@ -30,7 +30,7 @@ __test__ = False # do not collect ]) def test_acceptance(options): """ - Run the acceptance tests for the either lms or cms + Run the acceptance tests for either lms or cms """ opts = { 'fasttest': getattr(options, 'fasttest', False), diff --git a/pavelib/assets.py b/pavelib/assets.py index e1d3cd15d7591173e575335ecc193c9cdcdc5080..d7c8c13c0da30b3aecf3886facb16bb13aeb006f 100644 --- a/pavelib/assets.py +++ b/pavelib/assets.py @@ -12,24 +12,28 @@ from paver import tasks from paver.easy import sh, path, task, cmdopts, needs, consume_args, call_task, no_help from watchdog.observers import Observer from watchdog.events import PatternMatchingEventHandler -import sass from .utils.envs import Env from .utils.cmd import cmd, django_cmd # setup baseline paths +ALL_SYSTEMS = ['lms', 'studio'] COFFEE_DIRS = ['lms', 'cms', 'common'] # A list of directories. Each will be paired with a sibling /css directory. -SASS_DIRS = [ +COMMON_SASS_DIRECTORIES = [ + path("common/static/sass"), +] +LMS_SASS_DIRECTORIES = [ path("lms/static/sass"), path("lms/static/themed_sass"), - path("cms/static/sass"), - path("common/static/sass"), path("lms/static/certificates/sass"), ] +CMS_SASS_DIRECTORIES = [ + path("cms/static/sass"), +] +THEME_SASS_DIRECTORIES = [] SASS_LOAD_PATHS = ['common/static', 'common/static/sass'] -SASS_CACHE_PATH = '/tmp/sass-cache' def configure_paths(): @@ -44,7 +48,7 @@ def configure_paths(): css_dir = theme_root / "static" / "css" if sass_dir.isdir(): css_dir.mkdir_p() - SASS_DIRS.append(sass_dir) + 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"]) @@ -52,16 +56,39 @@ def configure_paths(): lms_css = theme_dir / "lms" / "static" / "css" if lms_sass.isdir(): lms_css.mkdir_p() - SASS_DIRS.append(lms_sass) + 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() - SASS_DIRS.append(cms_sass) + 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) + return applicable_directories + + class CoffeeScriptWatcher(PatternMatchingEventHandler): """ Watches for coffeescript changes @@ -99,7 +126,7 @@ class SassWatcher(PatternMatchingEventHandler): """ register files with observer """ - for dirname in SASS_LOAD_PATHS + SASS_DIRS: + for dirname in SASS_LOAD_PATHS + applicable_sass_directories(): paths = [] if '*' in dirname: paths.extend(glob.glob(dirname)) @@ -185,6 +212,7 @@ def compile_coffeescript(*files): @task @no_help @cmdopts([ + ('system=', 's', 'The system to compile sass for (defaults to all)'), ('debug', 'd', 'Debug mode'), ('force', '', 'Force full compilation'), ]) @@ -192,8 +220,18 @@ def compile_sass(options): """ Compile Sass to CSS. """ - debug = options.get('debug') + # Note: import sass only when it is needed and not at the top of the file. + # This allows other paver commands to operate even without libsass being + # installed. In particular, this allows the install_prereqs command to be + # used to install the dependency. + import sass + + debug = options.get('debug') + force = options.get('force') + systems = getattr(options, 'system', ALL_SYSTEMS) + if isinstance(systems, basestring): + systems = systems.split(',') if debug: source_comments = True output_style = 'nested' @@ -202,22 +240,39 @@ def compile_sass(options): output_style = 'compressed' timing_info = [] - - for sass_dir in SASS_DIRS: + 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: start = datetime.now() css_dir = sass_dir.parent / "css" - sass.compile( - dirname=(sass_dir, css_dir), - include_paths=SASS_LOAD_PATHS + SASS_DIRS, - source_comments=source_comments, - output_style=output_style, - ) - duration = datetime.now() - start - timing_info.append((sass_dir, css_dir, duration)) + + if force: + if dry_run: + tasks.environment.info("rm -rf {css_dir}/*.css".format( + css_dir=css_dir, + )) + else: + sh("rm -rf {css_dir}/*.css".format(css_dir=css_dir)) + + if dry_run: + tasks.environment.info("libsass {sass_dir}".format( + sass_dir=sass_dir, + )) + else: + sass.compile( + dirname=(sass_dir, css_dir), + include_paths=SASS_LOAD_PATHS + all_sass_directories, + 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:") - for sass_dir, css_dir, duration in timing_info: - print(">> {} -> {} in {}s".format(sass_dir, css_dir, duration)) + if not dry_run: + for sass_dir, css_dir, duration in timing_info: + print(">> {} -> {} in {}s".format(sass_dir, css_dir, duration)) def compile_templated_sass(systems, settings): @@ -226,15 +281,15 @@ def compile_templated_sass(systems, settings): `systems` is a list of systems (e.g. 'lms' or 'studio' or both) `settings` is the Django settings module to use. """ - for sys in systems: - if sys == "studio": - sys = "cms" + for system in systems: + if system == "studio": + system = "cms" sh(django_cmd( - sys, settings, 'preprocess_assets', - '{sys}/static/sass/*.scss'.format(sys=sys), - '{sys}/static/themed_sass'.format(sys=sys) + system, settings, 'preprocess_assets', + '{system}/static/sass/*.scss'.format(system=system), + '{system}/static/themed_sass'.format(system=system) )) - print("\t\tFinished preprocessing {} assets.".format(sys)) + print("\t\tFinished preprocessing {} assets.".format(system)) def process_xmodule_assets(): @@ -310,7 +365,7 @@ def update_assets(args): """ parser = argparse.ArgumentParser(prog='paver update_assets') parser.add_argument( - 'system', type=str, nargs='*', default=['lms', 'studio'], + 'system', type=str, nargs='*', default=ALL_SYSTEMS, help="lms or studio", ) parser.add_argument( @@ -334,7 +389,7 @@ def update_assets(args): compile_templated_sass(args.system, args.settings) process_xmodule_assets() compile_coffeescript() - call_task('pavelib.assets.compile_sass', options={'debug': args.debug}) + call_task('pavelib.assets.compile_sass', options={'system': args.system, 'debug': args.debug}) if args.collect: collect_assets(args.system, args.settings) diff --git a/pavelib/paver_tests/test_assets.py b/pavelib/paver_tests/test_assets.py new file mode 100644 index 0000000000000000000000000000000000000000..b690b6d0fe85bd05710a803c75f6e2c93f4cf276 --- /dev/null +++ b/pavelib/paver_tests/test_assets.py @@ -0,0 +1,58 @@ +"""Unit tests for the Paver asset tasks.""" + +import ddt +from paver.easy import call_task + +from .utils import PaverTestCase + + +@ddt.ddt +class TestPaverAssetTasks(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_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}) + expected_messages = [] + 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/css/*.css") + expected_messages.append("libsass lms/static/themed_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) diff --git a/pavelib/paver_tests/test_servers.py b/pavelib/paver_tests/test_servers.py index 9d75e57ce275f4389d189bf83cc817b8c3749961..d2169b7ac07cfdcb3f5ad106a6ff33959fa5fd99 100644 --- a/pavelib/paver_tests/test_servers.py +++ b/pavelib/paver_tests/test_servers.py @@ -11,21 +11,19 @@ EXPECTED_COFFEE_COMMAND = ( "{platform_root}/cms {platform_root}/common -type f -name \"*.coffee\"`" ) EXPECTED_SASS_COMMAND = ( - "sass --update --cache-location /tmp/sass-cache --default-encoding utf-8 --style compressed" - " --quiet" - " --load-path ." - " --load-path common/static" - " --load-path common/static/sass" - " --load-path lms/static/sass" - " --load-path lms/static/themed_sass" - " --load-path cms/static/sass --load-path common/static/sass" - " --load-path lms/static/certificates/sass" - " lms/static/sass:lms/static/css" - " lms/static/themed_sass:lms/static/css" - " cms/static/sass:cms/static/css" - " common/static/sass:common/static/css" - " lms/static/certificates/sass:lms/static/certificates/css" + "libsass {sass_directory}" ) +EXPECTED_COMMON_SASS_DIRECTORIES = [ + "common/static/sass", +] +EXPECTED_LMS_SASS_DIRECTORIES = [ + "lms/static/sass", + "lms/static/themed_sass", + "lms/static/certificates/sass", +] +EXPECTED_CMS_SASS_DIRECTORIES = [ + "cms/static/sass", +] EXPECTED_PREPROCESS_ASSETS_COMMAND = ( "python manage.py {system} --settings={asset_settings} preprocess_assets" " {system}/static/sass/*.scss {system}/static/themed_sass" @@ -236,7 +234,7 @@ class TestPaverServerTasks(PaverTestCase): )) expected_messages.append("xmodule_assets common/static/xmodule") expected_messages.append(EXPECTED_COFFEE_COMMAND.format(platform_root=platform_root)) - expected_messages.append(EXPECTED_SASS_COMMAND) + expected_messages.extend(self.expected_sass_commands(system=system)) if expected_collect_static: expected_messages.append(EXPECTED_COLLECT_STATIC_COMMAND.format( system=system, asset_settings=expected_asset_settings @@ -278,7 +276,7 @@ class TestPaverServerTasks(PaverTestCase): )) expected_messages.append("xmodule_assets common/static/xmodule") expected_messages.append(EXPECTED_COFFEE_COMMAND.format(platform_root=platform_root)) - expected_messages.append(EXPECTED_SASS_COMMAND) + expected_messages.extend(self.expected_sass_commands()) if expected_collect_static: expected_messages.append(EXPECTED_COLLECT_STATIC_COMMAND.format( system="lms", asset_settings=expected_asset_settings @@ -302,3 +300,15 @@ class TestPaverServerTasks(PaverTestCase): ) expected_messages.append(EXPECTED_CELERY_COMMAND.format(settings="dev_with_worker")) self.assertEquals(self.task_messages, expected_messages) + + def expected_sass_commands(self, system=None): + """ + Returns the expected SASS commands for the specified system. + """ + expected_sass_directories = [] + expected_sass_directories.extend(EXPECTED_COMMON_SASS_DIRECTORIES) + if system != 'cms': + expected_sass_directories.extend(EXPECTED_LMS_SASS_DIRECTORIES) + if system != 'lms': + expected_sass_directories.extend(EXPECTED_CMS_SASS_DIRECTORIES) + return [EXPECTED_SASS_COMMAND.format(sass_directory=directory) for directory in expected_sass_directories] diff --git a/pavelib/prereqs.py b/pavelib/prereqs.py index 0646ca307616ba60f41fbe003fbd72fae80d5d5b..1136865381cea9cb3d9e28a02d27f09112d103a4 100644 --- a/pavelib/prereqs.py +++ b/pavelib/prereqs.py @@ -24,6 +24,7 @@ PYTHON_REQ_FILES = [ 'requirements/edx/github.txt', 'requirements/edx/local.txt', 'requirements/edx/base.txt', + 'requirements/edx/paver.txt', 'requirements/edx/post.txt', ]