├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.txt ├── LICENSE ├── MANIFEST.in ├── README.rst ├── django_libsass.py ├── runtests.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── extra_static │ └── extra.scss ├── settings.py ├── static │ └── css │ │ ├── imported.scss │ │ ├── index.scss │ │ ├── raw1.css │ │ ├── raw2.css │ │ ├── with_extra_include.scss │ │ ├── with_import.scss │ │ ├── with_raw_css_import.scss │ │ └── with_static.scss ├── templates │ └── index.html ├── tests │ ├── __init__.py │ └── test_sass.py ├── urls.py └── views.py └── tox.ini /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please see https://github.com/torchbox/django-libsass#reporting-bugs before reporting. Any bug reports relating to a third-party CSS framework are liable to be closed without further investigation. 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | name: Python ${{ matrix.python-version }}, django ${{ matrix.django-version }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: # https://docs.djangoproject.com/en/5.0/faq/install/#faq-python-version-support 15 | django-version: ["42", "50", "51"] 16 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 17 | 18 | exclude: 19 | - django-version: "42" 20 | python-version: "3.13" 21 | - django-version: "50" 22 | python-version: "3.9" 23 | - django-version: "50" 24 | python-version: "3.13" 25 | - django-version: "51" 26 | python-version: "3.9" 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: setup python 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | - name: Install tox 35 | run: pip install tox 36 | - name: Run Tests 37 | env: 38 | TOXENV: django${{ matrix.django-version }} 39 | run: tox 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | dist/ 4 | django_libsass.egg-info 5 | MANIFEST 6 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Unreleased 5 | ~~~~~~~~~~~~~~~~ 6 | 7 | * Added Django 4.1 & 4.2 support 8 | * Added Python 3.10 & 3.11 support 9 | * Drop support for Python 2.7, 3.4, 3.5, 3.6 & 3.7 (EOL) 10 | * Drop support for Django 1.8, 1.9, 1.10, 1.11, 2.2, 3.0 & 3.1 (EOL) 11 | 12 | 0.9 (08.07.2021) 13 | ~~~~~~~~~~~~~~~~ 14 | * Added Django 3.1 and 3.2 support (Awais Qureshi) 15 | * Added tox and enable Github actions for testing across different Django and Python versions (Awais Qureshi) 16 | 17 | 0.8 (06.01.2020) 18 | ~~~~~~~~~~~~~~~~ 19 | * Added Django 3.0 support (Tobias Kunze, Dan Kingston) 20 | * Included LICENSE file in distribution (Dougal J. Sutherland) 21 | * Added LIBSASS_ADDITIONAL_INCLUDE_PATHS setting (Iwan Trofimtschuk) 22 | * Removed unnecessary quoting from output of `static` function 23 | 24 | 0.7 (02.05.2016) 25 | ~~~~~~~~~~~~~~~~ 26 | * Added LIBSASS_PRECISION setting (Liang-Bo Wang) 27 | 28 | 0.6 (21.11.2015) 29 | ~~~~~~~~~~~~~~~~ 30 | * Fixed call to FilterBase super for django-compressor 1.6 compatibility (Dennis Vermeulen) 31 | 32 | 0.5 (18.11.2015) 33 | ~~~~~~~~~~~~~~~~ 34 | * Added sourcemap support (Saulius Žemaitaitis) 35 | * Updated the `static` function to use the more versatile django.contrib.staticfiles implementation (Carl Johnson) 36 | 37 | 0.4 (24.08.2015) 38 | ~~~~~~~~~~~~~~~~ 39 | * Added support for custom functions (Alexandre Pocquet) 40 | * Added a `static` function to generate paths to assets such as images and fonts (Alexandre Pocquet) 41 | 42 | 0.3 (27.04.2015) 43 | ~~~~~~~~~~~~~~~~ 44 | * Enabled source comments when DEBUG is True; can be overridden with the LIBSASS_SOURCE_COMMENTS setting 45 | * Added LIBSASS_OUTPUT_STYLE setting 46 | 47 | 0.2 (22.05.2014) 48 | ~~~~~~~~~~~~~~~~ 49 | * Made compatible with django-compressor 1.4 and Python 3. 50 | 51 | 0.1 (05.03.2014) 52 | ~~~~~~~~~~~~~~~~ 53 | * Initial release. 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Torchbox Ltd and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Torchbox nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-libsass 2 | ============== 3 | 4 | A django-compressor filter to compile Sass files using libsass. 5 | 6 | Installation 7 | ~~~~~~~~~~~~ 8 | 9 | Starting from a Django project with `django-compressor `_ set up:: 10 | 11 | pip install django-libsass 12 | 13 | and add django_libsass.SassCompiler to your COMPRESS_PRECOMPILERS setting:: 14 | 15 | COMPRESS_PRECOMPILERS = ( 16 | ('text/x-scss', 'django_libsass.SassCompiler'), 17 | ) 18 | 19 | You can now use the content type text/x-scss on your stylesheets, and have them 20 | compiled seamlessly into CSS:: 21 | 22 | {% load compress %} 23 | 24 | {% compress css %} 25 | 26 | {% endcompress %} 27 | 28 | 29 | Imports 30 | ~~~~~~~ 31 | 32 | Relative paths in @import lines are followed as you would expect:: 33 | 34 | @import "../variables.scss"; 35 | 36 | Additionally, Django's STATICFILES_FINDERS setting is consulted, and all possible locations 37 | for static files *on the local filesystem* are included on the search path. This makes it 38 | possible to import files across different apps:: 39 | 40 | @import "myotherapp/css/widget.scss" 41 | 42 | 43 | Settings 44 | ~~~~~~~~ 45 | 46 | The following settings can be used to control django-libsass's behaviour: 47 | 48 | * ``LIBSASS_SOURCE_COMMENTS`` - whether to enable SASS source comments (adds comments about source lines). Defaults to ``True`` when Django's ``DEBUG`` is ``True``, ``False`` otherwise. 49 | * ``LIBSASS_OUTPUT_STYLE`` - SASS output style. Options are ``'nested'``, ``'expanded'``, ``'compact'`` and ``'compressed'``, although as of libsass 3.0.2 only ``'nested'`` and ``'compressed'`` are implemented. Default is 'nested'. See `SASS documentation for output styles `_. Note that `django-compressor's settings `_ may also affect the formatting of the resulting CSS. 50 | * ``LIBSASS_CUSTOM_FUNCTIONS`` - A mapping of custom functions to be made available within the SASS compiler. By default, a ``static`` function is provided, analogous to Django's ``static`` template tag. 51 | * ``LIBSASS_SOURCEMAPS`` - Enable embedding sourcemaps into file output (default: False) 52 | * ``LIBSASS_PRECISION`` - Number of digits of numerical precision (default: 5) 53 | * ``LIBSASS_ADDITIONAL_INCLUDE_PATHS`` - a list of base paths to be recognised in @import lines, in addition to Django's recognised static file locations 54 | 55 | 56 | Custom functions 57 | ~~~~~~~~~~~~~~~~ 58 | 59 | The SASS compiler can be extended with custom Python functions defined in the ``LIBSASS_CUSTOM_FUNCTIONS`` setting. By default, a ``static`` function is provided, for generating static paths to resources such as images and fonts:: 60 | 61 | .foo { 62 | background: url(static("myapp/image/bar.png")); 63 | } 64 | 65 | If your ``STATIC_URL`` is '/static/', this will be rendered as:: 66 | 67 | .foo { 68 | background: url("/static/myapp/image/bar.png")); 69 | } 70 | 71 | Why django-libsass? 72 | ~~~~~~~~~~~~~~~~~~~ 73 | 74 | We wanted to use Sass in a Django project without introducing any external (non pip-installable) 75 | dependencies. (Actually, we wanted to use Less, but the same arguments apply...) There are a few 76 | pure Python implementations of Sass and Less, but we found that they invariably didn't match the 77 | behaviour of the reference compilers, either in their handling of @imports or lesser-used CSS 78 | features such as media queries. 79 | 80 | `libsass `_ is a mature C/C++ port of the Sass engine, co-developed by the 81 | original creator of Sass, and we can reasonably rely on it to stay in sync with the reference 82 | Sass compiler - and, being C/C++, it's fast. Thanks to Hong Minhee's 83 | `libsass-python `_ project, it has Python bindings and 84 | installs straight from pip. 85 | 86 | django-libsass builds on libsass-python to make @import paths aware of Django's staticfiles 87 | mechanism, and provides a filter module for django-compressor which uses the libsass-python API 88 | directly, avoiding the overheads of calling an external executable to do the compilation. 89 | 90 | 91 | Reporting bugs 92 | ~~~~~~~~~~~~~~ 93 | 94 | Please see the `troubleshooting `_ page for help with some common setup issues. 95 | 96 | I do not provide support for getting django-libsass working with your CSS framework of choice. If you believe you've found a bug, please try to isolate it as a minimal reproducible test case before reporting it - ideally this will consist of a few edits / additions to the `hello-django-libsass `_ example project. If you cannot demonstrate the problem in a few standalone SCSS files, it is almost certainly not a django-libsass bug - any bug reports that relate to a third-party CSS framework are likely to be closed without further investigation. 97 | 98 | 99 | Author 100 | ~~~~~~ 101 | 102 | Matt Westcott matthew.westcott@torchbox.com 103 | -------------------------------------------------------------------------------- /django_libsass.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import os 4 | import re 5 | 6 | import sass 7 | from compressor.filters.base import FilterBase 8 | from django.conf import settings 9 | from django.contrib.staticfiles.finders import get_finders 10 | from django.templatetags.static import static as django_static 11 | 12 | 13 | def static(path): 14 | """ 15 | Use the Django builtin static file resolver to return an absolute path 16 | usable as CSS url() argument. Sass equivalent of the 'static' template 17 | tag. 18 | """ 19 | return django_static(path) 20 | 21 | 22 | OUTPUT_STYLE = getattr(settings, 'LIBSASS_OUTPUT_STYLE', 'nested') 23 | SOURCE_COMMENTS = getattr(settings, 'LIBSASS_SOURCE_COMMENTS', settings.DEBUG) 24 | CUSTOM_FUNCTIONS = getattr(settings, 'LIBSASS_CUSTOM_FUNCTIONS', {'static': static}) 25 | SOURCEMAPS = getattr(settings, 'LIBSASS_SOURCEMAPS', False) 26 | PRECISION = getattr(settings, 'LIBSASS_PRECISION', None) # None use libsass default 27 | ADDITIONAL_INCLUDE_PATHS = getattr(settings, 'LIBSASS_ADDITIONAL_INCLUDE_PATHS', None) 28 | 29 | 30 | INCLUDE_PATHS = None # populate this on first call to 'get_include_paths' 31 | 32 | 33 | def get_include_paths(): 34 | """ 35 | Generate a list of include paths that libsass should use to find files 36 | mentioned in @import lines. 37 | """ 38 | global INCLUDE_PATHS 39 | if INCLUDE_PATHS is not None: 40 | return INCLUDE_PATHS 41 | 42 | include_paths = [] 43 | 44 | # Look for staticfile finders that define 'storages' 45 | for finder in get_finders(): 46 | try: 47 | storages = finder.storages 48 | except AttributeError: 49 | continue 50 | 51 | for storage in storages.values(): 52 | try: 53 | include_paths.append(storage.path('.')) 54 | except NotImplementedError: 55 | # storages that do not implement 'path' do not store files locally, 56 | # and thus cannot provide an include path 57 | pass 58 | 59 | global ADDITIONAL_INCLUDE_PATHS 60 | if ADDITIONAL_INCLUDE_PATHS: 61 | include_paths.extend(ADDITIONAL_INCLUDE_PATHS) 62 | 63 | INCLUDE_PATHS = include_paths 64 | return include_paths 65 | 66 | 67 | def prefix_sourcemap(sourcemap, base_path): 68 | decoded_sourcemap = json.loads(sourcemap) 69 | source_urls = [] 70 | include_paths = get_include_paths() 71 | 72 | for source_filename in decoded_sourcemap['sources']: 73 | # expand source_filename into an absolute file path 74 | full_source_path = os.path.normpath(os.path.join(base_path, source_filename)) 75 | 76 | # look for a path in include_paths that is a prefix of full_source_path 77 | for path in include_paths: 78 | if full_source_path.startswith(path): 79 | # A matching path has been found; take the remainder as a relative path. 80 | # include_paths entries do not include a trailing slash; 81 | # [len(path) + 1:] ensures that we trim the path plus trailing slash 82 | remainder = full_source_path[len(path) + 1:] 83 | 84 | # Invoke the 'static' template tag to turn the relative path into a URL 85 | source_urls.append(django_static(remainder)) 86 | break 87 | else: 88 | # no matching path was found in include_paths; return the original source filename 89 | # as a fallback 90 | source_urls.append(source_filename) 91 | 92 | decoded_sourcemap['sources'] = source_urls 93 | return json.dumps(decoded_sourcemap) 94 | 95 | 96 | def embed_sourcemap(output, sourcemap): 97 | encoded_sourcemap = base64.standard_b64encode( 98 | sourcemap.encode('utf-8') 99 | ) 100 | sourcemap_fragment = 'sourceMappingURL=data:application/json;base64,{} '\ 101 | .format(encoded_sourcemap.decode('utf-8')) 102 | url_re = re.compile(r'sourceMappingURL=[^\s]+', re.M) 103 | output = url_re.sub(sourcemap_fragment, output) 104 | 105 | return output 106 | 107 | 108 | def compile(**kwargs): 109 | """Perform sass.compile, but with the appropriate include_paths for Django added""" 110 | kwargs = kwargs.copy() 111 | if PRECISION is not None: 112 | kwargs['precision'] = PRECISION 113 | kwargs['include_paths'] = (kwargs.get('include_paths') or []) + get_include_paths() 114 | 115 | custom_functions = CUSTOM_FUNCTIONS.copy() 116 | custom_functions.update(kwargs.get('custom_functions', {})) 117 | kwargs['custom_functions'] = custom_functions 118 | 119 | if SOURCEMAPS and kwargs.get('filename', None): 120 | # We need to pass source_map_file to libsass so it generates 121 | # correct paths to source files. 122 | base_path = os.path.dirname(kwargs['filename']) 123 | sourcemap_filename = os.path.join(base_path, 'sourcemap.map') 124 | kwargs['source_map_filename'] = sourcemap_filename 125 | 126 | libsass_output, sourcemap = sass.compile(**kwargs) 127 | sourcemap = prefix_sourcemap(sourcemap, base_path) 128 | output = embed_sourcemap(libsass_output, sourcemap) 129 | else: 130 | output = sass.compile(**kwargs) 131 | return output 132 | 133 | 134 | class SassCompiler(FilterBase): 135 | def __init__(self, content, attrs=None, filter_type=None, charset=None, filename=None): 136 | # FilterBase doesn't handle being passed attrs, so fiddle the signature 137 | super(SassCompiler, self).__init__(content=content, 138 | filter_type=filter_type, 139 | filename=filename) 140 | 141 | def input(self, **kwargs): 142 | if self.filename: 143 | return compile(filename=self.filename, 144 | output_style=OUTPUT_STYLE, 145 | source_comments=SOURCE_COMMENTS) 146 | else: 147 | return compile(string=self.content, 148 | output_style=OUTPUT_STYLE) 149 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import os 5 | import shutil 6 | import sys 7 | import warnings 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' 12 | 13 | 14 | def make_parser(): 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument('--deprecation', choices=['pending', 'imminent', 'none'], default='imminent') 17 | return parser 18 | 19 | 20 | def parse_args(args=None): 21 | return make_parser().parse_known_args(args) 22 | 23 | 24 | def runtests(): 25 | args, rest = parse_args() 26 | 27 | if args.deprecation == 'pending': 28 | # Show all deprecation warnings 29 | warnings.simplefilter('default', DeprecationWarning) 30 | warnings.simplefilter('default', PendingDeprecationWarning) 31 | elif args.deprecation == 'imminent': 32 | # Show only imminent deprecation warnings 33 | warnings.simplefilter('default', DeprecationWarning) 34 | elif args.deprecation == 'none': 35 | # Deprecation warnings are ignored by default 36 | pass 37 | 38 | argv = [sys.argv[0], 'test'] + rest 39 | 40 | try: 41 | execute_from_command_line(argv) 42 | finally: 43 | from tests.settings import STATIC_ROOT 44 | shutil.rmtree(STATIC_ROOT, ignore_errors=True) 45 | 46 | 47 | if __name__ == '__main__': 48 | runtests() 49 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license-file = LICENSE 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | setup( 9 | name='django-libsass', 10 | version='0.9', 11 | description="A django-compressor filter to compile SASS files using libsass", 12 | author='Matt Westcott', 13 | author_email='matthew.westcott@torchbox.com', 14 | url='https://github.com/torchbox/django-libsass', 15 | py_modules=['django_libsass'], 16 | license='BSD', 17 | long_description=open('README.rst').read(), 18 | classifiers=[ 19 | 'Development Status :: 5 - Production/Stable', 20 | 'Environment :: Web Environment', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: BSD License', 23 | 'Operating System :: OS Independent', 24 | 'Programming Language :: Python', 25 | 'Programming Language :: Python :: 3', 26 | 'Programming Language :: Python :: 3.9', 27 | 'Programming Language :: Python :: 3.10', 28 | 'Programming Language :: Python :: 3.11', 29 | 'Programming Language :: Python :: 3.12', 30 | 'Programming Language :: Python :: 3.13', 31 | 'Framework :: Django', 32 | 'Framework :: Django :: 4.2', 33 | 'Framework :: Django :: 5.0', 34 | 'Framework :: Django :: 5.1', 35 | ], 36 | python_requires='>=3.9', 37 | install_requires=[ 38 | "django-compressor>=1.3", 39 | "libsass>=0.7.0,<1", 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torchbox/django-libsass/8fb6d21cf975645f84d893a5c7ace7311a97a9bc/tests/__init__.py -------------------------------------------------------------------------------- /tests/extra_static/extra.scss: -------------------------------------------------------------------------------- 1 | .extra-style { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_libsass tests. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.13. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'snbicjfchnkjnyhcojinbcojnghjn=*uv2e$gr5-7w2^j2y9y!' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'tests', 35 | 36 | 'compressor', 37 | 38 | 'django.contrib.admin', 39 | 'django.contrib.auth', 40 | 'django.contrib.contenttypes', 41 | 'django.contrib.sessions', 42 | 'django.contrib.messages', 43 | 'django.contrib.staticfiles', 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | 'django.middleware.security.SecurityMiddleware', 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 54 | ] 55 | 56 | ROOT_URLCONF = 'tests.urls' 57 | 58 | TEMPLATES = [ 59 | { 60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 61 | 'DIRS': [], 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.contrib.messages.context_processors.messages', 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | STATIC_ROOT = os.path.join(BASE_DIR, 'test-static') 123 | 124 | # List of finder classes that know how to find static files in 125 | # various locations. 126 | STATICFILES_FINDERS = ( 127 | 'django.contrib.staticfiles.finders.FileSystemFinder', 128 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 129 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 130 | 'compressor.finders.CompressorFinder', 131 | ) 132 | 133 | COMPRESS_PRECOMPILERS = ( 134 | ('text/x-scss', 'django_libsass.SassCompiler'), 135 | ) 136 | 137 | LIBSASS_ADDITIONAL_INCLUDE_PATHS = [ 138 | os.path.join(BASE_DIR, 'tests', 'extra_static'), 139 | ] 140 | LIBSASS_SOURCEMAPS = True 141 | -------------------------------------------------------------------------------- /tests/static/css/imported.scss: -------------------------------------------------------------------------------- 1 | .imported-style { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/static/css/index.scss: -------------------------------------------------------------------------------- 1 | $background-color: #ddffdd; 2 | $foreground-color: #008800; 3 | 4 | body { 5 | background-color: $background-color; 6 | 7 | h1 { 8 | color: $foreground-color; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/static/css/raw1.css: -------------------------------------------------------------------------------- 1 | .raw-style-1 { 2 | color: yellow; 3 | } 4 | -------------------------------------------------------------------------------- /tests/static/css/raw2.css: -------------------------------------------------------------------------------- 1 | .raw-style-2 { 2 | color: green; 3 | } 4 | -------------------------------------------------------------------------------- /tests/static/css/with_extra_include.scss: -------------------------------------------------------------------------------- 1 | @import 'extra'; 2 | -------------------------------------------------------------------------------- /tests/static/css/with_import.scss: -------------------------------------------------------------------------------- 1 | @import 'imported'; 2 | -------------------------------------------------------------------------------- /tests/static/css/with_raw_css_import.scss: -------------------------------------------------------------------------------- 1 | @import 'raw1.css'; 2 | @import 'raw2'; 3 | -------------------------------------------------------------------------------- /tests/static/css/with_static.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-image: url(static('images/my image.jpg')); 3 | } 4 | -------------------------------------------------------------------------------- /tests/templates/index.html: -------------------------------------------------------------------------------- 1 | {% load compress static %} 2 | 3 | 4 | 5 | Hello django-libsass 6 | {% compress css %} 7 | 8 | {% endcompress %} 9 | 10 | 11 |

It worked!

12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torchbox/django-libsass/8fb6d21cf975645f84d893a5c7ace7311a97a9bc/tests/tests/__init__.py -------------------------------------------------------------------------------- /tests/tests/test_sass.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from django.conf import settings 3 | from django.test import TestCase 4 | 5 | from django_libsass import compile 6 | 7 | class TestSass(TestCase): 8 | def test_invocation(self): 9 | response = self.client.get('/') 10 | self.assertEqual(response.status_code, 200) 11 | 12 | def test_import(self): 13 | result = compile(filename=os.path.join(settings.BASE_DIR, 'tests', 'static', 'css', 'with_import.scss')) 14 | self.assertIn('.imported-style', result) 15 | 16 | def test_extra_include_path(self): 17 | result = compile(filename=os.path.join(settings.BASE_DIR, 'tests', 'static', 'css', 'with_extra_include.scss')) 18 | self.assertIn('.extra-style', result) 19 | 20 | def test_raw_css_import(self): 21 | result = compile(filename=os.path.join(settings.BASE_DIR, 'tests', 'static', 'css', 'with_raw_css_import.scss')) 22 | self.assertIn('@import url(raw1.css);', result) 23 | self.assertIn('.raw-style-2', result) 24 | 25 | def test_static_function(self): 26 | result = compile(filename=os.path.join(settings.BASE_DIR, 'tests', 'static', 'css', 'with_static.scss')) 27 | self.assertIn(r'background-image: url(/static/images/my%20image.jpg);', result) 28 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | """hello_django_libsass URL Configuration 4 | 5 | The `urlpatterns` list routes URLs to views. For more information please see: 6 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 7 | Examples: 8 | Function views 9 | 1. Add an import: from my_app import views 10 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 11 | Class-based views 12 | 1. Add an import: from other_app.views import Home 13 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 14 | Including another URLconf 15 | 1. Import the include() function: from django.urls import include, path 16 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 17 | """ 18 | 19 | from django.urls import path 20 | 21 | from tests.views import index 22 | 23 | 24 | urlpatterns = [ 25 | path('', index), 26 | ] 27 | -------------------------------------------------------------------------------- /tests/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def index(request): 5 | return render(request, 'index.html') 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = django{42,50,51} 3 | 4 | [testenv] 5 | commands = 6 | python runtests.py 7 | deps = 8 | django42: Django>=4.2,<5.0 9 | django50: Django>=5.0,<5.1 10 | django51: Django>=5.1,<5.2 11 | --------------------------------------------------------------------------------