├── .gitignore ├── AUTHORS.txt ├── LICENSE.txt ├── README ├── distribute_setup.py ├── sass ├── __init__.py ├── admin.py ├── exceptions.py ├── listeners.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── sassify.py ├── models.py ├── templatetags │ ├── __init__.py │ └── sass_tag.py ├── tests.py ├── utils.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Developers: 2 | Ash Christopher 3 | 4 | Contributors: 5 | Jerome Leclanche (Python 3 support) 6 | Shawn Burant (Namespace clobbering bug.) 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Ash Christopher 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | *** Note: This package is no longer under development. *** 2 | 3 | 4 | ================================ 5 | Sass integration app for Django 6 | ================================ 7 | 8 | Django-sass integrates the power, usefulness and best of 9 | all DRY-ness of Sass, into your Django projects. 10 | 11 | 12 | Getting Started 13 | ------------------------------- 14 | 15 | In your settings.py, define the path to your sass executable. 16 | 17 | SASS_BIN = '/usr/bin/sass' # location of the sass binary 18 | 19 | As well, you will need a dictionary to define the sass input and output files. 20 | 21 | SASS = ( 22 | { 23 | 'name' : 'test', 24 | 'details' : { 25 | 'input' : 'sass/test.sass', 26 | 'output' : 'css/test.css', 27 | } 28 | }, 29 | { 30 | 'name' : 'test2', 31 | 'details' : { 32 | 'input' : 'sass/test2.sass', 33 | 'output' : 'css/test2.css', 34 | } 35 | }, 36 | ) 37 | 38 | Once all of your Sass files have been defined in your settings.py file, you can now reference 39 | them in your templates. 40 | 41 | {% load sass_tag %} 42 | ... 43 | {% sass %} 44 | 45 | eg. 46 | 47 | {% sass 'test' %} 48 | 49 | If the named sass entry is not up to date, django-sass will automatically run the sass command on 50 | the sass file, generating your css. 51 | 52 | 53 | Management Command 54 | ------------------------------- 55 | The 'sassify' command is used to generate the css manually. The css will only be generated if 56 | there are changes made to the sass files. 57 | 58 | python manage.py sassify [OPTIONS] 59 | -- This command runs sass on all files that are not yet up to date. 60 | 61 | 62 | Options: 63 | 64 | '--force' 65 | - Forces sass to regenerate all css files from css files. 66 | 67 | '--list' 68 | - List the status of your named sass entries. 69 | 70 | '--clean' 71 | - Remove all generated files. 72 | 73 | 74 | 75 | Compatability 76 | ------------------------------- 77 | This library is only compatible with Linux/BSD based distros. I don't use Windows, so if you want 78 | Windows support, feel free to submit a patch. 79 | -------------------------------------------------------------------------------- /distribute_setup.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """Bootstrap distribute installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from distribute_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import os 17 | import sys 18 | import time 19 | import fnmatch 20 | import tempfile 21 | import tarfile 22 | from distutils import log 23 | 24 | try: 25 | from site import USER_SITE 26 | except ImportError: 27 | USER_SITE = None 28 | 29 | try: 30 | import subprocess 31 | 32 | def _python_cmd(*args): 33 | args = (sys.executable,) + args 34 | return subprocess.call(args) == 0 35 | 36 | except ImportError: 37 | # will be used for python 2.3 38 | def _python_cmd(*args): 39 | args = (sys.executable,) + args 40 | # quoting arguments if windows 41 | if sys.platform == 'win32': 42 | def quote(arg): 43 | if ' ' in arg: 44 | return '"%s"' % arg 45 | return arg 46 | args = [quote(arg) for arg in args] 47 | return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 48 | 49 | DEFAULT_VERSION = "0.6.10" 50 | DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" 51 | SETUPTOOLS_FAKED_VERSION = "0.6c11" 52 | 53 | SETUPTOOLS_PKG_INFO = """\ 54 | Metadata-Version: 1.0 55 | Name: setuptools 56 | Version: %s 57 | Summary: xxxx 58 | Home-page: xxx 59 | Author: xxx 60 | Author-email: xxx 61 | License: xxx 62 | Description: xxx 63 | """ % SETUPTOOLS_FAKED_VERSION 64 | 65 | 66 | def _install(tarball): 67 | # extracting the tarball 68 | tmpdir = tempfile.mkdtemp() 69 | log.warn('Extracting in %s', tmpdir) 70 | old_wd = os.getcwd() 71 | try: 72 | os.chdir(tmpdir) 73 | tar = tarfile.open(tarball) 74 | _extractall(tar) 75 | tar.close() 76 | 77 | # going in the directory 78 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 79 | os.chdir(subdir) 80 | log.warn('Now working in %s', subdir) 81 | 82 | # installing 83 | log.warn('Installing Distribute') 84 | if not _python_cmd('setup.py', 'install'): 85 | log.warn('Something went wrong during the installation.') 86 | log.warn('See the error message above.') 87 | finally: 88 | os.chdir(old_wd) 89 | 90 | 91 | def _build_egg(egg, tarball, to_dir): 92 | # extracting the tarball 93 | tmpdir = tempfile.mkdtemp() 94 | log.warn('Extracting in %s', tmpdir) 95 | old_wd = os.getcwd() 96 | try: 97 | os.chdir(tmpdir) 98 | tar = tarfile.open(tarball) 99 | _extractall(tar) 100 | tar.close() 101 | 102 | # going in the directory 103 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 104 | os.chdir(subdir) 105 | log.warn('Now working in %s', subdir) 106 | 107 | # building an egg 108 | log.warn('Building a Distribute egg in %s', to_dir) 109 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) 110 | 111 | finally: 112 | os.chdir(old_wd) 113 | # returning the result 114 | log.warn(egg) 115 | if not os.path.exists(egg): 116 | raise IOError('Could not build the egg.') 117 | 118 | 119 | def _do_download(version, download_base, to_dir, download_delay): 120 | egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' 121 | % (version, sys.version_info[0], sys.version_info[1])) 122 | if not os.path.exists(egg): 123 | tarball = download_setuptools(version, download_base, 124 | to_dir, download_delay) 125 | _build_egg(egg, tarball, to_dir) 126 | sys.path.insert(0, egg) 127 | import setuptools 128 | setuptools.bootstrap_install_from = egg 129 | 130 | 131 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 132 | to_dir=os.curdir, download_delay=15, no_fake=True): 133 | # making sure we use the absolute path 134 | to_dir = os.path.abspath(to_dir) 135 | was_imported = 'pkg_resources' in sys.modules or \ 136 | 'setuptools' in sys.modules 137 | try: 138 | try: 139 | import pkg_resources 140 | if not hasattr(pkg_resources, '_distribute'): 141 | if not no_fake: 142 | _fake_setuptools() 143 | raise ImportError 144 | except ImportError: 145 | return _do_download(version, download_base, to_dir, download_delay) 146 | try: 147 | pkg_resources.require("distribute>="+version) 148 | return 149 | except pkg_resources.VersionConflict: 150 | e = sys.exc_info()[1] 151 | if was_imported: 152 | sys.stderr.write( 153 | "The required version of distribute (>=%s) is not available,\n" 154 | "and can't be installed while this script is running. Please\n" 155 | "install a more recent version first, using\n" 156 | "'easy_install -U distribute'." 157 | "\n\n(Currently using %r)\n" % (version, e.args[0])) 158 | sys.exit(2) 159 | else: 160 | del pkg_resources, sys.modules['pkg_resources'] # reload ok 161 | return _do_download(version, download_base, to_dir, 162 | download_delay) 163 | except pkg_resources.DistributionNotFound: 164 | return _do_download(version, download_base, to_dir, 165 | download_delay) 166 | finally: 167 | if not no_fake: 168 | _create_fake_setuptools_pkg_info(to_dir) 169 | 170 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 171 | to_dir=os.curdir, delay=15): 172 | """Download distribute from a specified location and return its filename 173 | 174 | `version` should be a valid distribute version number that is available 175 | as an egg for download under the `download_base` URL (which should end 176 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 177 | `delay` is the number of seconds to pause before an actual download 178 | attempt. 179 | """ 180 | # making sure we use the absolute path 181 | to_dir = os.path.abspath(to_dir) 182 | try: 183 | from urllib.request import urlopen 184 | except ImportError: 185 | from urllib2 import urlopen 186 | tgz_name = "distribute-%s.tar.gz" % version 187 | url = download_base + tgz_name 188 | saveto = os.path.join(to_dir, tgz_name) 189 | src = dst = None 190 | if not os.path.exists(saveto): # Avoid repeated downloads 191 | try: 192 | log.warn("Downloading %s", url) 193 | src = urlopen(url) 194 | # Read/write all in one block, so we don't create a corrupt file 195 | # if the download is interrupted. 196 | data = src.read() 197 | dst = open(saveto, "wb") 198 | dst.write(data) 199 | finally: 200 | if src: 201 | src.close() 202 | if dst: 203 | dst.close() 204 | return os.path.realpath(saveto) 205 | 206 | 207 | def _patch_file(path, content): 208 | """Will backup the file then patch it""" 209 | existing_content = open(path).read() 210 | if existing_content == content: 211 | # already patched 212 | log.warn('Already patched.') 213 | return False 214 | log.warn('Patching...') 215 | _rename_path(path) 216 | f = open(path, 'w') 217 | try: 218 | f.write(content) 219 | finally: 220 | f.close() 221 | return True 222 | 223 | 224 | def _same_content(path, content): 225 | return open(path).read() == content 226 | 227 | def _no_sandbox(function): 228 | def __no_sandbox(*args, **kw): 229 | try: 230 | from setuptools.sandbox import DirectorySandbox 231 | def violation(*args): 232 | pass 233 | DirectorySandbox._old = DirectorySandbox._violation 234 | DirectorySandbox._violation = violation 235 | patched = True 236 | except ImportError: 237 | patched = False 238 | 239 | try: 240 | return function(*args, **kw) 241 | finally: 242 | if patched: 243 | DirectorySandbox._violation = DirectorySandbox._old 244 | del DirectorySandbox._old 245 | 246 | return __no_sandbox 247 | 248 | @_no_sandbox 249 | def _rename_path(path): 250 | new_name = path + '.OLD.%s' % time.time() 251 | log.warn('Renaming %s into %s', path, new_name) 252 | os.rename(path, new_name) 253 | return new_name 254 | 255 | def _remove_flat_installation(placeholder): 256 | if not os.path.isdir(placeholder): 257 | log.warn('Unkown installation at %s', placeholder) 258 | return False 259 | found = False 260 | for file in os.listdir(placeholder): 261 | if fnmatch.fnmatch(file, 'setuptools*.egg-info'): 262 | found = True 263 | break 264 | if not found: 265 | log.warn('Could not locate setuptools*.egg-info') 266 | return 267 | 268 | log.warn('Removing elements out of the way...') 269 | pkg_info = os.path.join(placeholder, file) 270 | if os.path.isdir(pkg_info): 271 | patched = _patch_egg_dir(pkg_info) 272 | else: 273 | patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) 274 | 275 | if not patched: 276 | log.warn('%s already patched.', pkg_info) 277 | return False 278 | # now let's move the files out of the way 279 | for element in ('setuptools', 'pkg_resources.py', 'site.py'): 280 | element = os.path.join(placeholder, element) 281 | if os.path.exists(element): 282 | _rename_path(element) 283 | else: 284 | log.warn('Could not find the %s element of the ' 285 | 'Setuptools distribution', element) 286 | return True 287 | 288 | 289 | def _after_install(dist): 290 | log.warn('After install bootstrap.') 291 | placeholder = dist.get_command_obj('install').install_purelib 292 | _create_fake_setuptools_pkg_info(placeholder) 293 | 294 | @_no_sandbox 295 | def _create_fake_setuptools_pkg_info(placeholder): 296 | if not placeholder or not os.path.exists(placeholder): 297 | log.warn('Could not find the install location') 298 | return 299 | pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) 300 | setuptools_file = 'setuptools-%s-py%s.egg-info' % \ 301 | (SETUPTOOLS_FAKED_VERSION, pyver) 302 | pkg_info = os.path.join(placeholder, setuptools_file) 303 | if os.path.exists(pkg_info): 304 | log.warn('%s already exists', pkg_info) 305 | return 306 | 307 | log.warn('Creating %s', pkg_info) 308 | f = open(pkg_info, 'w') 309 | try: 310 | f.write(SETUPTOOLS_PKG_INFO) 311 | finally: 312 | f.close() 313 | 314 | pth_file = os.path.join(placeholder, 'setuptools.pth') 315 | log.warn('Creating %s', pth_file) 316 | f = open(pth_file, 'w') 317 | try: 318 | f.write(os.path.join(os.curdir, setuptools_file)) 319 | finally: 320 | f.close() 321 | 322 | def _patch_egg_dir(path): 323 | # let's check if it's already patched 324 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 325 | if os.path.exists(pkg_info): 326 | if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): 327 | log.warn('%s already patched.', pkg_info) 328 | return False 329 | _rename_path(path) 330 | os.mkdir(path) 331 | os.mkdir(os.path.join(path, 'EGG-INFO')) 332 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 333 | f = open(pkg_info, 'w') 334 | try: 335 | f.write(SETUPTOOLS_PKG_INFO) 336 | finally: 337 | f.close() 338 | return True 339 | 340 | 341 | def _before_install(): 342 | log.warn('Before install bootstrap.') 343 | _fake_setuptools() 344 | 345 | 346 | def _under_prefix(location): 347 | if 'install' not in sys.argv: 348 | return True 349 | args = sys.argv[sys.argv.index('install')+1:] 350 | for index, arg in enumerate(args): 351 | for option in ('--root', '--prefix'): 352 | if arg.startswith('%s=' % option): 353 | top_dir = arg.split('root=')[-1] 354 | return location.startswith(top_dir) 355 | elif arg == option: 356 | if len(args) > index: 357 | top_dir = args[index+1] 358 | return location.startswith(top_dir) 359 | elif option == '--user' and USER_SITE is not None: 360 | return location.startswith(USER_SITE) 361 | return True 362 | 363 | 364 | def _fake_setuptools(): 365 | log.warn('Scanning installed packages') 366 | try: 367 | import pkg_resources 368 | except ImportError: 369 | # we're cool 370 | log.warn('Setuptools or Distribute does not seem to be installed.') 371 | return 372 | ws = pkg_resources.working_set 373 | try: 374 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', 375 | replacement=False)) 376 | except TypeError: 377 | # old distribute API 378 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) 379 | 380 | if setuptools_dist is None: 381 | log.warn('No setuptools distribution found') 382 | return 383 | # detecting if it was already faked 384 | setuptools_location = setuptools_dist.location 385 | log.warn('Setuptools installation detected at %s', setuptools_location) 386 | 387 | # if --root or --preix was provided, and if 388 | # setuptools is not located in them, we don't patch it 389 | if not _under_prefix(setuptools_location): 390 | log.warn('Not patching, --root or --prefix is installing Distribute' 391 | ' in another location') 392 | return 393 | 394 | # let's see if its an egg 395 | if not setuptools_location.endswith('.egg'): 396 | log.warn('Non-egg installation') 397 | res = _remove_flat_installation(setuptools_location) 398 | if not res: 399 | return 400 | else: 401 | log.warn('Egg installation') 402 | pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') 403 | if (os.path.exists(pkg_info) and 404 | _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): 405 | log.warn('Already patched.') 406 | return 407 | log.warn('Patching...') 408 | # let's create a fake egg replacing setuptools one 409 | res = _patch_egg_dir(setuptools_location) 410 | if not res: 411 | return 412 | log.warn('Patched done.') 413 | _relaunch() 414 | 415 | 416 | def _relaunch(): 417 | log.warn('Relaunching...') 418 | # we have to relaunch the process 419 | args = [sys.executable] + sys.argv 420 | sys.exit(subprocess.call(args)) 421 | 422 | 423 | def _extractall(self, path=".", members=None): 424 | """Extract all members from the archive to the current working 425 | directory and set owner, modification time and permissions on 426 | directories afterwards. `path' specifies a different directory 427 | to extract to. `members' is optional and must be a subset of the 428 | list returned by getmembers(). 429 | """ 430 | import copy 431 | import operator 432 | from tarfile import ExtractError 433 | directories = [] 434 | 435 | if members is None: 436 | members = self 437 | 438 | for tarinfo in members: 439 | if tarinfo.isdir(): 440 | # Extract directories with a safe mode. 441 | directories.append(tarinfo) 442 | tarinfo = copy.copy(tarinfo) 443 | tarinfo.mode = 448 # decimal for oct 0700 444 | self.extract(tarinfo, path) 445 | 446 | # Reverse sort directories. 447 | if sys.version_info < (2, 4): 448 | def sorter(dir1, dir2): 449 | return cmp(dir1.name, dir2.name) 450 | directories.sort(sorter) 451 | directories.reverse() 452 | else: 453 | directories.sort(key=operator.attrgetter('name'), reverse=True) 454 | 455 | # Set correct owner, mtime and filemode on directories. 456 | for tarinfo in directories: 457 | dirpath = os.path.join(path, tarinfo.name) 458 | try: 459 | self.chown(tarinfo, dirpath) 460 | self.utime(tarinfo, dirpath) 461 | self.chmod(tarinfo, dirpath) 462 | except ExtractError: 463 | e = sys.exc_info()[1] 464 | if self.errorlevel > 1: 465 | raise 466 | else: 467 | self._dbg(1, "tarfile: %s" % e) 468 | 469 | 470 | def main(argv, version=DEFAULT_VERSION): 471 | """Install or upgrade setuptools and EasyInstall""" 472 | tarball = download_setuptools() 473 | _install(tarball) 474 | 475 | 476 | if __name__ == '__main__': 477 | main(sys.argv[1:]) 478 | -------------------------------------------------------------------------------- /sass/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashchristopher/django-sass/6a43d2ae2ef699a887f0e0534643fb46c031a59e/sass/__init__.py -------------------------------------------------------------------------------- /sass/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from sass.models import SassModel 4 | 5 | class SassModelAdmin(admin.ModelAdmin): 6 | pass 7 | 8 | admin.site.register(SassModel, SassModelAdmin) -------------------------------------------------------------------------------- /sass/exceptions.py: -------------------------------------------------------------------------------- 1 | class SassConfigurationError(Exception): 2 | """ 3 | This ERROR should be used when there is a problem with the 4 | configuration settings in the settings.py file. 5 | """ 6 | pass 7 | 8 | 9 | class SassCommandArgumentError(Exception): 10 | """ 11 | This ERROR should be used when there is a problem with the 12 | arguments being passed in from the command line. 13 | """ 14 | pass 15 | 16 | 17 | class SassGenerationError(Exception): 18 | """ 19 | This ERROR is used when there was a problem using Sass to 20 | generate css files. This could be caused by issues with the 21 | sass file itself. 22 | """ 23 | pass 24 | 25 | 26 | class SassException(Exception): 27 | pass 28 | 29 | class SassConfigException(Exception): 30 | pass -------------------------------------------------------------------------------- /sass/listeners.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.db.models import signals 3 | from sass.models import SassModel 4 | 5 | 6 | def set_last_modified_time(sender, instance, **kwargs): 7 | # set the last modified time based on the os.stat system call. 8 | st_mtime = 8 # last_modified_time column 9 | last_modified_time = os.stat(instance.sass_path)[st_mtime] 10 | instance.source_modified_time = last_modified_time 11 | 12 | 13 | def start_listening(): 14 | signals.pre_save.connect(set_last_modified_time, sender=SassModel) 15 | -------------------------------------------------------------------------------- /sass/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashchristopher/django-sass/6a43d2ae2ef699a887f0e0534643fb46c031a59e/sass/management/__init__.py -------------------------------------------------------------------------------- /sass/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashchristopher/django-sass/6a43d2ae2ef699a887f0e0534643fb46c031a59e/sass/management/commands/__init__.py -------------------------------------------------------------------------------- /sass/management/commands/sassify.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from optparse import make_option 4 | 5 | from django.core.management.base import BaseCommand 6 | from django.conf import settings 7 | from django.core.management.color import no_style 8 | 9 | from sass.models import SASS_ROOT, SassModel 10 | from sass.utils import update_needed 11 | from sass.exceptions import SassConfigException, SassConfigurationError, SassCommandArgumentError, SassException 12 | 13 | 14 | class Command(BaseCommand): 15 | """ 16 | The user may whish to keep their sass files in their MEDIA_ROOT directory, 17 | or they may wish them to be somewhere outsite - even outside their project 18 | directory. We try to support both. 19 | 20 | The same is true for the CSS output file. We recommend putting it in the 21 | MEDIA_ROOT but if there is a reason not to, we support that as well. 22 | """ 23 | 24 | requires_model_validation = False 25 | can_import_settings = True 26 | style = no_style() 27 | 28 | option_list = BaseCommand.option_list + ( 29 | make_option('--style', '-t', dest='sass_style', default='nested', help='Sass output style. Can be nested (default), compact, compressed, or expanded.'), 30 | make_option('--list', '-l', action='store_true', dest='list_sass' , default=None, help='Display information about the status of your sass files.'), 31 | make_option('--force', '-f', action='store_true', dest='force_sass', default=False, help='Force sass to run.'), 32 | make_option('--clean', '-c', action='store_true', dest='clean', default=False, help='Remove all the generated CSS files.'), 33 | ) 34 | help = 'Converts Sass files into CSS.' 35 | 36 | 37 | def __init__(self, *args, **kwargs): 38 | super(Command, self).__init__(*args, **kwargs) 39 | self.bin = getattr(settings, "SASS_BIN", None) 40 | if not self.bin: 41 | raise SassConfigurationError('SASS_BIN is not defined in settings.py file.') 42 | # test that the binary actually exists. 43 | if not os.path.exists(self.bin): 44 | raise SassConfigurationError('Sass binary defined by SASS_BIN does not exist: %s' % self.bin) 45 | 46 | self.sass_style = getattr(settings, "SASS_STYLE", 'nested') 47 | 48 | 49 | def handle(self, *args, **kwargs): 50 | # make sure the Sass style given is valid. 51 | self.sass_style = kwargs.get('sass_style') 52 | if self.sass_style not in ('nested', 'compact', 'compressed', 'expanded'): 53 | raise SassCommandArgumentError("Invalid sass style argument: %s" % self.sass_style) 54 | 55 | force = kwargs.get('force_sass') 56 | list_sass = kwargs.get('list_sass') 57 | clean = kwargs.get('clean') 58 | 59 | # we process the args in the order of least importance to hopefully stop 60 | # any unwanted permanent behavior. 61 | 62 | if list_sass: 63 | self.list() 64 | elif clean: 65 | self.clean() 66 | else: 67 | self.process_sass(force=force) 68 | 69 | 70 | def get_sass_definitions(self): 71 | definitions = [] 72 | for sass_def in getattr(settings, "SASS", ()): 73 | try: 74 | sd = { 75 | 'name' : sass_def['name'], 76 | 'input_file' : os.path.join(SASS_ROOT, sass_def['details']['input']), 77 | 'output_file' : os.path.join(SASS_ROOT, sass_def['details']['output']), 78 | # 'media_output' : settings.MEDIA_URL + urlquote(sass_def['details']['output']), 79 | } 80 | definitions.append(sd) 81 | except KeyError: 82 | raise SassConfigurationError("Improperly defined SASS definition.") 83 | return definitions 84 | 85 | 86 | def process_sass(self, name=None, force=False): 87 | if force: 88 | print("Forcing sass to run on all files.") 89 | sass_definitions = self.get_sass_definitions() 90 | for sass_def in sass_definitions: 91 | if not name or name == sass_def['name']: 92 | self.generate_css_file(force=force, **sass_def) 93 | 94 | 95 | 96 | def generate_css_file(self, force, name, input_file, output_file, **kwargs): 97 | # check that the sass input file actually exists. 98 | if not os.path.exists(input_file): 99 | raise SassConfigException('The input \'%s\' does not exist.\n' %input_file) 100 | output_path = output_file.rsplit('/', 1)[0] 101 | if not os.path.exists(output_path): 102 | # try to create path 103 | try: 104 | os.mkdirs(output_path, 0o644) 105 | except os.error as e: 106 | raise SassConfigException(e.message) 107 | except AttributeError as e: 108 | # we have an older version of python that doesn't support os.mkdirs - fail gracefully. 109 | raise SassConfigException("Output path does not exist - please create manually: %s\n" % output_path) 110 | 111 | try: 112 | sass_obj = SassModel.objects.get(name=name) 113 | was_created = False 114 | except SassModel.DoesNotExist: 115 | sass_obj = SassModel(name=name) 116 | was_created = True 117 | 118 | sass_obj.sass_path = input_file 119 | sass_obj.css_path = output_file 120 | 121 | needs_update = was_created or force or update_needed(sass_obj) 122 | if needs_update: 123 | sass_dict = { 'bin' : self.bin, 'sass_style' : self.sass_style, 'input' : input_file, 'output' : output_file } 124 | cmd = "%(bin)s -t %(sass_style)s -C %(input)s > %(output)s" %sass_dict 125 | p = subprocess.Popen([self.bin, "-t", self.sass_style, "--no-cache", input_file, output_file]) 126 | stdout, stderr = p.communicate() 127 | if p.returncode != 0: # Process failed (nonzero exit code) 128 | raise SassException(stderr) 129 | sass_obj.save() 130 | 131 | 132 | def clean(self): 133 | for s in SassModel.objects.all(): 134 | try: 135 | print("Removing css: %s" % s.css_path) 136 | os.remove(s.css_path) 137 | s.delete() 138 | except OSError as e: 139 | raise e 140 | 141 | 142 | def list(self): 143 | """ 144 | We check to see if the Sass outlined in the SASS setting are different from what the databse 145 | has stored. We only care about listing those files that are in the SASS setting. Ignore the 146 | settings in the DB if the files/settings have been removed. 147 | """ 148 | # process the Sass information in the settings. 149 | sass_definitions = self.get_sass_definitions() 150 | for sass_def in sass_definitions: 151 | print("[%s]" % sass_def["name"]) 152 | try: 153 | sass_obj = SassModel.objects.get(name=sass_def['name']) 154 | sass_obj.sass_path = sass_def['input_file'] 155 | sass_obj.css_path = sass_def['output_file'] 156 | was_created = False 157 | except SassModel.DoesNotExist: 158 | sass_obj = SassModel(name=sass_def['name']) 159 | was_created = True 160 | needs_update = was_created or update_needed(sass_obj) 161 | if needs_update: 162 | print("\tChanges detected.") 163 | -------------------------------------------------------------------------------- /sass/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | from django.utils.http import urlquote 4 | 5 | SASS_ROOT = getattr(settings, 'SASS_ROOT', settings.MEDIA_ROOT) 6 | SASS_URL = getattr(settings, 'SASS_URL', settings.MEDIA_URL) 7 | 8 | 9 | class SassModel(models.Model): 10 | name = models.CharField(max_length=60, primary_key=True, help_text='Name of the Sass conversion.') 11 | sass_path = models.CharField(max_length=255, help_text='Path submitted for the Sass file.') 12 | css_path = models.CharField(max_length=255, help_text='Path to the generated CSS file.') 13 | style = models.CharField(choices='', max_length=10, help_text='The style used when creating the css file.') 14 | source_modified_time = models.CharField(max_length=12, help_text='Last time the source file was modified.') 15 | 16 | def __unicode__(self): 17 | return self.name 18 | 19 | def relative_css_path(self): 20 | return self.css_path.split(SASS_ROOT)[1].lstrip('/') 21 | 22 | def css_media_path(self): 23 | return SASS_URL + urlquote(self.relative_css_path()) 24 | 25 | 26 | from sass import listeners 27 | listeners.start_listening() 28 | -------------------------------------------------------------------------------- /sass/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashchristopher/django-sass/6a43d2ae2ef699a887f0e0534643fb46c031a59e/sass/templatetags/__init__.py -------------------------------------------------------------------------------- /sass/templatetags/sass_tag.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from sass.models import SassModel 3 | from sass.management.commands import sassify 4 | 5 | 6 | register = template.Library() 7 | 8 | class SassNode(template.Node): 9 | def __init__(self, name): 10 | try: 11 | command = sassify.Command() 12 | command.process_sass(name=name) 13 | self.model = SassModel.objects.get(name=name) 14 | except SassModel.DoesNotExist: 15 | raise template.TemplateSyntaxError('Sass name "%s" does not exist.' % self.name) 16 | 17 | 18 | def render(self, context): 19 | media_url = self.model.css_media_path() 20 | css = "" %(media_url, self.model.source_modified_time) 21 | return css 22 | 23 | 24 | @register.tag(name="sass") 25 | def do_sass(parser, token): 26 | try: 27 | # get the tag and the sass resource. 28 | tag_name, resource = token.split_contents() 29 | except ValueError: 30 | raise template.TemplateSyntaxError('%s tag requires a single argument.' %token.contents.split()[0]) 31 | if not (resource[0] == resource[-1] and resource[0] in ('"', "'")): 32 | raise template.TemplateSyntaxError("%r tag's argument should be in quotes" % tag_name) 33 | return SassNode(resource[1:-1]) 34 | -------------------------------------------------------------------------------- /sass/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates two different styles of tests (one doctest and one 3 | unittest). These will both pass when you run "manage.py test". 4 | 5 | Replace these with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | class SimpleTest(TestCase): 11 | def test_basic_addition(self): 12 | """ 13 | Tests that 1 + 1 always equals 2. 14 | """ 15 | self.failUnlessEqual(1 + 1, 2) 16 | 17 | __test__ = {"doctest": """ 18 | Another way to test that 1 + 1 is equal to 2. 19 | 20 | >>> 1 + 1 == 2 21 | True 22 | """} 23 | 24 | -------------------------------------------------------------------------------- /sass/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hashlib 3 | 4 | from django.conf import settings 5 | from django.utils.http import urlquote 6 | 7 | from sass.models import SassModel 8 | from sass.exceptions import SassConfigException 9 | 10 | def update_needed(new_sass_model): 11 | # check the database and see if the 12 | name = new_sass_model.name 13 | orig_sass_model = SassModel.objects.get(name=name) 14 | 15 | # if the output file doesn't exist we need to update 16 | if not os.path.exists(new_sass_model.css_path): 17 | return True 18 | 19 | # if the model has been modified, then we need to update. 20 | for key in ['sass_path', 'css_path', 'style',]: 21 | if not getattr(orig_sass_model, key) == getattr(new_sass_model, key): 22 | return True 23 | 24 | # if the source file has been updated, then we need to update. 25 | try: 26 | last_modified_time = os.stat(new_sass_model.sass_path)[8] 27 | if not str(last_modified_time) == new_sass_model.source_modified_time: 28 | return True 29 | except OSError: 30 | # file does not exist so we need to update 31 | return True 32 | 33 | return False 34 | 35 | 36 | class SassUtils(object): 37 | @staticmethod 38 | def get_file_path(path): 39 | if os.path.isabs(path): 40 | return path 41 | return settings.MEDIA_ROOT + os.path.sep + path 42 | 43 | @staticmethod 44 | def get_media_url(path, media_url=settings.MEDIA_URL): 45 | return media_url + urlquote(path) 46 | 47 | @staticmethod 48 | def build_sass_structure(): 49 | sass_definitions = getattr(settings, "SASS", ()) 50 | 51 | sass_struct = [] 52 | for sass_def in sass_definitions: 53 | try: 54 | sass_name = sass_def.get('name', None) 55 | sass_details = sass_def.get('details', {}) 56 | sass_input = sass_details.get('input', None) 57 | sass_output = sass_details.get('output', None) 58 | 59 | # i hate generic exception message - try to give the user a meaningful message about what exactly the problem is. 60 | for prop in [('name', sass_name), ('details', sass_details), ('input', sass_input), ('output', sass_output)]: 61 | if not prop[1]: 62 | raise SassConfigException('Sass \'%s\' property not defined in configuration:\n%s\n' %(prop[0], sass_def)) 63 | except SassConfigException: 64 | return 65 | sass_input_root = SassUtils.get_file_path(sass_input) 66 | sass_output_root = SassUtils.get_file_path(sass_output) 67 | sass_output_media = SassUtils.get_media_url(sass_output) 68 | sass_struct.append({ 69 | 'name' : sass_name, 70 | 'input' : sass_input_root, 71 | 'output' : sass_output_root, 72 | 'media_out' : sass_output_media, 73 | }) 74 | return sass_struct 75 | 76 | @staticmethod 77 | def md5_file(filename): 78 | try: 79 | md5 = hashlib.md5() 80 | fd = open(filename,"rb") 81 | content = fd.readlines() 82 | fd.close() 83 | for line in content: 84 | md5.update(line) 85 | return md5.hexdigest() 86 | except IOError as e: 87 | raise SassConfigException(e.message) 88 | -------------------------------------------------------------------------------- /sass/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from distribute_setup import use_setuptools; use_setuptools() 3 | from setuptools import setup, find_packages 4 | 5 | def read(fname): 6 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 7 | 8 | setup( 9 | name='django-sass', 10 | version='0.1.0', 11 | packages = find_packages(), 12 | author='Ash Christopher', 13 | author_email='ash@newthink.net', 14 | description='Django library that integrates Sass into your project.', 15 | license='LICENSE.txt', 16 | url='http://github.com/ashchristopher/django-sass', 17 | keywords='django sass ', 18 | long_description=read('README'), 19 | 20 | ) --------------------------------------------------------------------------------