├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── requirements.txt ├── setup.py └── simplestatic ├── __init__.py ├── compiler.jar ├── compress.py ├── conf.py ├── management ├── __init__.py └── commands │ ├── __init__.py │ └── static_sync.py ├── templatetags ├── __init__.py └── simplestatic_tags.py └── urls.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | *.DS_Store 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Eric Florenzano 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include simplestatic *.py *.jar -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-simplestatic 2 | =================== 3 | 4 | Django SimpleStatic is an opinionated Django app which makes it very simple to 5 | deal with static media, with extremely minimal configuration, as long as: 6 | 7 | * You store your static media in one directory, rather than alongside each app. 8 | * You want your files served from S3, rather from your own servers. 9 | * You want to use Google Closure Compiler to compress your JavaScript. 10 | * You want to compress your javascript ahead of time, rather than during the 11 | request. 12 | * You don't use any of those fancy CSS precompilers like LESS or SCSS. (This 13 | may change someday as my personal preferences change.) 14 | 15 | If any of the above don't hold true, then this library probably won't work for 16 | you. That said, if all of the above do hold true for you, then this app will 17 | likely be the simplest and best way to handle your static media. 18 | 19 | 20 | Installation 21 | ------------ 22 | 23 | 1. pip install django-simplestatic 24 | 25 | 2. Add 'simplestatic' to your INSTALLED_APPS: 26 | 27 | .. code-block:: python 28 | 29 | INSTALLED_APPS = ( 30 | 'django.contrib.auth', 31 | 'django.contrib.contenttypes', 32 | 'django.contrib.sessions', 33 | 'django.contrib.sites', 34 | 'django.contrib.admin', 35 | 36 | # ... all your installed apps 37 | 38 | 'simplestatic', 39 | ) 40 | 41 | 3. In your settings file, set the following values: 42 | 43 | .. code-block:: python 44 | 45 | SIMPLESTATIC_DIR = '/path/to/your/static/media/directory' 46 | 47 | AWS_ACCESS_KEY_ID = 'YOUR_ACCESS_KEY_HERE' 48 | AWS_SECRET_ACCESS_KEY = 'YOUR_SECRET_KEY_HERE' 49 | AWS_STORAGE_BUCKET_NAME = 'YOUR_STATIC_BUCKET_HERE' 50 | 51 | 4. In your urls.py, import the simplestatic_debug_urls function and execute it 52 | to the end of your urlpatterns: 53 | 54 | .. code-block:: python 55 | 56 | from simplestatic.urls import simplestatic_debug_urls 57 | 58 | urlpatterns = patterns('', 59 | # ... all of your url patterns right here 60 | ) + simplestatic_debug_urls() 61 | 62 | 5. In your template (or templates) import and use the simplestatic template 63 | tags, which might look something like this: 64 | 65 | .. code-block:: html+django 66 | 67 | {% load simplestatic_tags %} 68 | 69 | 70 | I love django-simplestatic! 71 | 72 | {% simplestatic %} 73 | {% compress_css "css/bootstrap.css" %} 74 | {% compress_css "css/screen.css" %} 75 | {% compress_js "js/jquery-1.9.1.js" %} 76 | {% compress_js "js/global.js" %} 77 | {% endsimplestatic %} 78 | 79 | 80 | 6. Before you push your code, run the static_sync management command to 81 | compress any CSS and JS and upload the whole directory to S3: 82 | 83 | .. code-block:: console 84 | 85 | python manage.py static_sync 86 | 87 | 88 | Advanced Configuration 89 | ---------------------- 90 | 91 | Even though in the vast majority of cases, you'll only need to do what was 92 | mentioned above, django-simplestatic offers a number of settings that you might 93 | want to tweak. Provided here is a reference of every setting 94 | 95 | 96 | Required Settings 97 | ~~~~~~~~~~~~~~~~~ 98 | 99 | SIMPLESTATIC_DIR: 100 | The directory where you store all of your static media. 101 | 102 | AWS_ACCESS_KEY_ID: 103 | Your Amazon Web Services access key. 104 | 105 | AWS_SECRET_ACCESS_KEY: 106 | Your Amazon Web Services secret access key. 107 | 108 | AWS_STORAGE_BUCKET_NAME: 109 | The S3 bucket in which to store and serve all of your static media. 110 | 111 | 112 | Optional Settings 113 | ~~~~~~~~~~~~~~~~~ 114 | 115 | SIMPLESTATIC_DEBUG: (Defaults to DEBUG) 116 | A boolean determining whether to use the minimized, compressed versions of 117 | the files uploaded to S3. If set to True, then the full development 118 | versions of the files will be served instead. You shouldn't have to touch 119 | this, as by default it's set to the same value as your Django DEBUG value. 120 | 121 | SIMPLESTATIC_DEBUG_PATH: (Defaults to 'static/') 122 | The URL path from which to serve static media during development. 123 | 124 | SIMPLESTATIC_CUSTOM_DOMAIN: (Defaults to S3 subdomain URL) 125 | A custom domain from which to serve static media in production. For 126 | example, you may want to use CloudFront as a CDN in front of your S3 127 | bucket, which can be achieved by 128 | 129 | .. code-block:: python 130 | 131 | SIMPLESTATIC_CUSTOM_DOMAIN = 'asdfasdf.cloudfront.net' 132 | 133 | 134 | SIMPLESTATIC_COMPRESSED_DIR: (Defaults to 'compressed') 135 | The URL path in S3 to place the compressed and minified versions of the CSS 136 | and JS. 137 | 138 | For example, in the default case where this is set to 'compressed', your 139 | css and js might be located in a location like one of the following: 140 | 141 | http://example.s3.amazonaws.com/compressed/6bf0c67b74b26425832a17bbf27b9cb9.css 142 | http://example.s3.amazonaws.com/compressed/97a548fc6b62d5bb9f50e6a95b25d8db.js 143 | 144 | CLOSURE_COMPILATION_LEVEL: (Defaults to 'SIMPLE_OPTIMIZATIONS') 145 | The Google Closure Compiler compilation level option. See the following 146 | page for more information: 147 | 148 | https://developers.google.com/closure/compiler/docs/compilation_levels 149 | 150 | CLOSURE_COMPILER_COMMAND: (Defaults to 'java -jar /path/to/supplied/closure.jar') 151 | The command required to run Google Closure Compiler. 152 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto==2.8.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import codecs 4 | from fnmatch import fnmatchcase 5 | from distutils.util import convert_path 6 | from setuptools import setup, find_packages 7 | 8 | 9 | def read(fname): 10 | return codecs.open(os.path.join(os.path.dirname(__file__), fname)).read() 11 | 12 | # Provided as an attribute, so you can append to these instead 13 | # of replicating them: 14 | standard_exclude = ('*.py', '*.pyc', '*$py.class', '*~', '.*', '*.bak') 15 | standard_exclude_directories = ('.*', 'CVS', '_darcs', './build', 16 | './dist', 'EGG-INFO', '*.egg-info') 17 | 18 | 19 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 20 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 21 | # Note: you may want to copy this into your setup.py file verbatim, as 22 | # you can't import this from another package, when you don't know if 23 | # that package is installed yet. 24 | def find_package_data( 25 | where='.', package='', 26 | exclude=standard_exclude, 27 | exclude_directories=standard_exclude_directories, 28 | only_in_packages=True, 29 | show_ignored=False): 30 | """ 31 | Return a dictionary suitable for use in ``package_data`` 32 | in a distutils ``setup.py`` file. 33 | 34 | The dictionary looks like:: 35 | 36 | {'package': [files]} 37 | 38 | Where ``files`` is a list of all the files in that package that 39 | don't match anything in ``exclude``. 40 | 41 | If ``only_in_packages`` is true, then top-level directories that 42 | are not packages won't be included (but directories under packages 43 | will). 44 | 45 | Directories matching any pattern in ``exclude_directories`` will 46 | be ignored; by default directories with leading ``.``, ``CVS``, 47 | and ``_darcs`` will be ignored. 48 | 49 | If ``show_ignored`` is true, then all the files that aren't 50 | included in package data are shown on stderr (for debugging 51 | purposes). 52 | 53 | Note patterns use wildcards, or can be exact paths (including 54 | leading ``./``), and all searching is case-insensitive. 55 | """ 56 | 57 | out = {} 58 | stack = [(convert_path(where), '', package, only_in_packages)] 59 | while stack: 60 | where, prefix, package, only_in_packages = stack.pop(0) 61 | for name in os.listdir(where): 62 | fn = os.path.join(where, name) 63 | if os.path.isdir(fn): 64 | bad_name = False 65 | for pattern in exclude_directories: 66 | if (fnmatchcase(name, pattern) 67 | or fn.lower() == pattern.lower()): 68 | bad_name = True 69 | if show_ignored: 70 | print >> sys.stderr, ( 71 | "Directory %s ignored by pattern %s" 72 | % (fn, pattern)) 73 | break 74 | if bad_name: 75 | continue 76 | if (os.path.isfile(os.path.join(fn, '__init__.py')) 77 | and not prefix): 78 | if not package: 79 | new_package = name 80 | else: 81 | new_package = package + '.' + name 82 | stack.append((fn, '', new_package, False)) 83 | else: 84 | stack.append((fn, prefix + name + '/', package, only_in_packages)) 85 | elif package or not only_in_packages: 86 | # is a file 87 | bad_name = False 88 | for pattern in exclude: 89 | if (fnmatchcase(name, pattern) 90 | or fn.lower() == pattern.lower()): 91 | bad_name = True 92 | if show_ignored: 93 | print >> sys.stderr, ( 94 | "File %s ignored by pattern %s" 95 | % (fn, pattern)) 96 | break 97 | if bad_name: 98 | continue 99 | out.setdefault(package, []).append(prefix + name) 100 | return out 101 | 102 | setup( 103 | name='django-simplestatic', 104 | version='0.0.5', 105 | url='https://github.com/ericflo/django-simplestatic', 106 | license='MIT', 107 | description='A highly opinionated drop-in library for static file management in Django', 108 | long_description=read('README.rst'), 109 | author='Eric Florenzano', 110 | author_email='floguy@gmail.com', 111 | packages=find_packages(exclude=['tests', 'tests.*']), 112 | package_data=find_package_data('simplestatic', only_in_packages=False), 113 | classifiers=[ 114 | 'Development Status :: 2 - Pre-Alpha', 115 | 'Intended Audience :: Developers', 116 | 'License :: OSI Approved :: MIT License', 117 | 'Programming Language :: Python', 118 | 'Programming Language :: Python :: 2.6', 119 | 'Programming Language :: Python :: 2.7', 120 | ], 121 | install_requires=['boto >= 2.8.0'], 122 | zip_safe=False, 123 | ) 124 | -------------------------------------------------------------------------------- /simplestatic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericflo/django-simplestatic/d413102121fc3d2fc5313c5ffc87dd7b74e54858/simplestatic/__init__.py -------------------------------------------------------------------------------- /simplestatic/compiler.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericflo/django-simplestatic/d413102121fc3d2fc5313c5ffc87dd7b74e54858/simplestatic/compiler.jar -------------------------------------------------------------------------------- /simplestatic/compress.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import os 3 | import subprocess 4 | 5 | from StringIO import StringIO 6 | 7 | from django.core.urlresolvers import reverse 8 | from django.core.cache.backends.locmem import LocMemCache 9 | 10 | from simplestatic import conf 11 | 12 | 13 | CACHE = LocMemCache('simplestatic', {}) 14 | CHUNK_SIZE = 8192 15 | 16 | 17 | def uncached_hash_for_paths(paths): 18 | hsh = hashlib.md5() 19 | 20 | for path in paths: 21 | full_path = os.path.join(conf.SIMPLESTATIC_DIR, path) 22 | if not os.path.exists(full_path): 23 | # TODO: Log some kind of warning here 24 | continue 25 | 26 | with open(full_path, 'r') as f: 27 | while 1: 28 | data = f.read(CHUNK_SIZE) 29 | if not data: 30 | break 31 | hsh.update(data) 32 | 33 | return hsh.hexdigest() 34 | 35 | 36 | def cached_hash_for_paths(paths): 37 | cache_key = hashlib.md5('!'.join(sorted(paths))).hexdigest() 38 | hsh = CACHE.get(cache_key) 39 | if hsh is not None: 40 | return hsh 41 | hsh = uncached_hash_for_paths(paths) 42 | CACHE.set(cache_key, hsh, 3600) 43 | return hsh 44 | 45 | 46 | hash_for_paths = (uncached_hash_for_paths if conf.SIMPLESTATIC_DEBUG else 47 | cached_hash_for_paths) 48 | 49 | 50 | def debug_url(path): 51 | hsh = hash_for_paths([path]) 52 | url = reverse('django.views.static.serve', kwargs={'path': path}) 53 | return '%s?devcachebuster=%s' % (url, hsh) 54 | 55 | 56 | def prod_url(paths, ext=None): 57 | if ext is None: 58 | ext = paths[0].rpartition('.')[-1] 59 | hsh = hash_for_paths(paths) 60 | return '//%s/%s/%s.%s' % ( 61 | conf.SIMPLESTATIC_CUSTOM_DOMAIN, 62 | conf.SIMPLESTATIC_COMPRESSED_DIR, 63 | hsh, 64 | ext, 65 | ) 66 | 67 | 68 | def url(path): 69 | if conf.SIMPLESTATIC_DEBUG: 70 | return debug_url(path) 71 | return '//%s/%s/%s' % ( 72 | conf.SIMPLESTATIC_CUSTOM_DOMAIN, 73 | conf.SIMPLESTATIC_COMPRESSED_DIR, 74 | path, 75 | ) 76 | 77 | 78 | def css_url(paths): 79 | return prod_url(paths, 'css') 80 | 81 | 82 | def js_url(paths): 83 | return prod_url(paths, 'js') 84 | 85 | 86 | def compress_css(paths): 87 | output = StringIO() 88 | for path in paths: 89 | with open(path, 'r') as in_file: 90 | while 1: 91 | data = in_file.read(CHUNK_SIZE) 92 | if not data: 93 | break 94 | output.write(data) 95 | output.write('\n') 96 | return output.getvalue() 97 | 98 | 99 | def compress_js(paths): 100 | cmd = '%s --compilation_level %s %s' % ( 101 | conf.CLOSURE_COMPILER_COMMAND, 102 | conf.CLOSURE_COMPILATION_LEVEL, 103 | ' '.join(['--js %s' % (path,) for path in paths]), 104 | ) 105 | output = subprocess.Popen( 106 | cmd, 107 | shell=True, 108 | stdout=subprocess.PIPE 109 | ).communicate()[0] 110 | return output 111 | -------------------------------------------------------------------------------- /simplestatic/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.conf import settings 3 | from django.core.exceptions import ImproperlyConfigured 4 | 5 | SIMPLESTATIC_DIR = getattr(settings, 'SIMPLESTATIC_DIR', None) 6 | if not SIMPLESTATIC_DIR: 7 | raise ImproperlyConfigured('You must set SIMPLESTATIC_DIR in settings.') 8 | 9 | SIMPLESTATIC_DEBUG = getattr(settings, 'SIMPLESTATIC_DEBUG', 10 | settings.DEBUG) 11 | 12 | SIMPLESTATIC_DEBUG_PATH = getattr(settings, 'SIMPLESTATIC_DEBUG_PATH', 13 | 'static/') 14 | 15 | SIMPLESTATIC_COMPRESSED_DIR = getattr(settings, 16 | 'SIMPLESTATIC_COMPRESSED_DIR', 'compressed') 17 | 18 | AWS_ACCESS_KEY_ID = getattr(settings, 'AWS_ACCESS_KEY_ID', None) 19 | if not AWS_ACCESS_KEY_ID: 20 | raise ImproperlyConfigured('You must set AWS_ACCESS_KEY_ID in settings.') 21 | 22 | AWS_SECRET_ACCESS_KEY = getattr(settings, 'AWS_SECRET_ACCESS_KEY', None) 23 | if not AWS_SECRET_ACCESS_KEY: 24 | raise ImproperlyConfigured( 25 | 'You must set AWS_SECRET_ACCESS_KEY in settings.') 26 | 27 | AWS_STORAGE_BUCKET_NAME = getattr(settings, 'AWS_STORAGE_BUCKET_NAME', None) 28 | if not AWS_STORAGE_BUCKET_NAME: 29 | raise ImproperlyConfigured( 30 | 'You must set AWS_STORAGE_BUCKET_NAME in settings.') 31 | 32 | SIMPLESTATIC_CUSTOM_DOMAIN = getattr(settings, 'SIMPLESTATIC_CUSTOM_DOMAIN', 33 | '%s.s3.amazonaws.com' % (AWS_STORAGE_BUCKET_NAME,)) 34 | 35 | CLOSURE_COMPILER_JAR = getattr(settings, 'CLOSURE_COMPILER_JAR', None) 36 | if not CLOSURE_COMPILER_JAR: 37 | CLOSURE_COMPILER_JAR = os.path.abspath( 38 | os.path.join(os.path.dirname(__file__), 'compiler.jar') 39 | ) 40 | 41 | CLOSURE_COMPILATION_LEVEL = getattr(settings, 'CLOSURE_COMPILATION_LEVEL', 42 | 'SIMPLE_OPTIMIZATIONS') 43 | 44 | CLOSURE_COMPILER_COMMAND = getattr(settings, 'CLOSURE_COMPILER_COMMAND', None) 45 | if not CLOSURE_COMPILER_COMMAND: 46 | CLOSURE_COMPILER_COMMAND = 'java -jar %s' % (CLOSURE_COMPILER_JAR,) 47 | -------------------------------------------------------------------------------- /simplestatic/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericflo/django-simplestatic/d413102121fc3d2fc5313c5ffc87dd7b74e54858/simplestatic/management/__init__.py -------------------------------------------------------------------------------- /simplestatic/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericflo/django-simplestatic/d413102121fc3d2fc5313c5ffc87dd7b74e54858/simplestatic/management/commands/__init__.py -------------------------------------------------------------------------------- /simplestatic/management/commands/static_sync.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | import os 3 | 4 | from threading import local, RLock 5 | 6 | from multiprocessing.pool import ThreadPool 7 | 8 | from boto.s3.connection import S3Connection 9 | 10 | from django.template import Template, Context 11 | from django.core.management.base import NoArgsCommand 12 | from django.conf import settings 13 | 14 | from simplestatic.compress import compress_css, compress_js, hash_for_paths 15 | from simplestatic import conf 16 | from simplestatic.templatetags.simplestatic_tags import SimpleStaticNode 17 | 18 | 19 | def s3_bucket(local=local()): 20 | bucket = getattr(local, 'bucket', None) 21 | if bucket is not None: 22 | return bucket 23 | conn = S3Connection( 24 | conf.AWS_ACCESS_KEY_ID, 25 | conf.AWS_SECRET_ACCESS_KEY 26 | ) 27 | local.bucket = conn.get_bucket(conf.AWS_STORAGE_BUCKET_NAME) 28 | return local.bucket 29 | 30 | 31 | def locked_print(s, lock=RLock()): 32 | with lock: 33 | print s 34 | 35 | 36 | def set_content_type(key): 37 | _, ext = os.path.splitext(key.name) 38 | if ext: 39 | content_type = mimetypes.types_map.get(ext) 40 | if content_type: 41 | key.content_type = content_type 42 | 43 | 44 | class Command(NoArgsCommand): 45 | help = ('Syncs the contents of your SIMPLESTATIC_DIR to S3, compressing ' 46 | + 'any assets as needed') 47 | 48 | def compress_and_upload(self, template, paths, compress, ext): 49 | bucket = s3_bucket() 50 | name = '%s/%s.%s' % ( 51 | conf.SIMPLESTATIC_COMPRESSED_DIR, 52 | hash_for_paths(paths), 53 | ext, 54 | ) 55 | key = bucket.get_key(name) 56 | if key is None: 57 | locked_print('Compressing %s from %s' % (ext, template)) 58 | compressed = compress(paths) 59 | locked_print('Uploading %s from %s' % (name, template)) 60 | key = bucket.new_key(name) 61 | set_content_type(key) 62 | key.set_contents_from_string(compressed, policy='public-read', 63 | replace=True) 64 | 65 | def sync_file(self, base, filename): 66 | name = filename[len(base) + 1:] 67 | bucket = s3_bucket() 68 | key = bucket.get_key(name) 69 | if key: 70 | etag = key.etag.lstrip('"').rstrip('"') 71 | with open(filename) as f: 72 | md5 = key.compute_md5(f)[0] 73 | if etag != md5: 74 | locked_print('Syncing %s' % (name,)) 75 | set_content_type(key) 76 | key.set_contents_from_filename(filename, policy='public-read', 77 | md5=md5, replace=True) 78 | else: 79 | locked_print('Syncing %s' % (name,)) 80 | key = bucket.new_key(name) 81 | set_content_type(key) 82 | key.set_contents_from_filename(filename, policy='public-read', 83 | replace=True) 84 | 85 | def handle_template(self, base, filename): 86 | with open(filename, 'r') as f: 87 | tmpl = Template(f.read()) 88 | template = filename[len(base) + 1:] 89 | nodes = tmpl.nodelist.get_nodes_by_type(SimpleStaticNode) 90 | for node in nodes: 91 | css, js = node.get_css_js_paths(Context()) 92 | if css: 93 | self.compress_and_upload(template, css, compress_css, 'css') 94 | if js: 95 | self.compress_and_upload(template, js, compress_js, 'js') 96 | 97 | def walk_tree(self, paths, func): 98 | while len(paths): 99 | popped = paths.pop() 100 | try: 101 | base, current_path = popped 102 | except (ValueError, TypeError): 103 | base = current_path = popped 104 | 105 | for root, dirs, files in os.walk(current_path): 106 | for d in dirs: 107 | normdir = os.path.join(root, d) 108 | if os.path.islink(normdir): 109 | paths.append((base, normdir)) 110 | for fn in files: 111 | if fn.startswith('.'): 112 | continue 113 | func(base, os.path.join(root, fn)) 114 | 115 | def handle_noargs(self, **options): 116 | mimetypes.init() 117 | 118 | locked_print('===> Syncing static directory') 119 | pool = ThreadPool(20) 120 | 121 | # Sync every file in the static media dir with S3 122 | def pooled_sync_file(base, filename): 123 | pool.apply_async(self.sync_file, args=[base, filename]) 124 | 125 | self.walk_tree([conf.SIMPLESTATIC_DIR], pooled_sync_file) 126 | pool.close() 127 | pool.join() 128 | locked_print('===> Static directory syncing complete') 129 | 130 | locked_print('===> Compressing and uploading CSS and JS') 131 | pool = ThreadPool(20) 132 | 133 | # Iterate over every template, looking for SimpleStaticNode 134 | def pooled_handle_template(base, filename): 135 | pool.apply_async(self.handle_template, args=[base, filename]) 136 | 137 | self.walk_tree(list(settings.TEMPLATE_DIRS), pooled_handle_template) 138 | pool.close() 139 | pool.join() 140 | locked_print('===> Finished compressing and uploading CSS and JS') 141 | -------------------------------------------------------------------------------- /simplestatic/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericflo/django-simplestatic/d413102121fc3d2fc5313c5ffc87dd7b74e54858/simplestatic/templatetags/__init__.py -------------------------------------------------------------------------------- /simplestatic/templatetags/simplestatic_tags.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django import template 4 | 5 | from simplestatic import conf 6 | from simplestatic.compress import debug_url, css_url, js_url, url 7 | 8 | register = template.Library() 9 | 10 | CSS_TMPL = '' 11 | JS_TMPL = '' 12 | 13 | 14 | class MediaNode(template.Node): 15 | def __init__(self, path): 16 | self.path = template.Variable(path) 17 | 18 | def resolve(self, context): 19 | return self.path.resolve(context) 20 | 21 | def render(self, context): 22 | return self.TMPL % (debug_url(self.path.resolve(context)),) 23 | 24 | 25 | class CSSNode(MediaNode): 26 | TMPL = CSS_TMPL 27 | 28 | 29 | class JSNode(MediaNode): 30 | TMPL = JS_TMPL 31 | 32 | 33 | class URLNode(template.Node): 34 | def __init__(self, path): 35 | self.path = template.Variable(path) 36 | 37 | def render(self, context): 38 | return url(self.path.resolve(context)) 39 | 40 | 41 | class SimpleStaticNode(template.Node): 42 | def __init__(self, nodes): 43 | self.nodes = nodes 44 | 45 | def render(self, context): 46 | if conf.SIMPLESTATIC_DEBUG: 47 | return self.render_debug(context) 48 | else: 49 | return self.render_prod(context) 50 | 51 | def render_debug(self, context): 52 | return '\n'.join((n.render(context) for n in self.nodes)) 53 | 54 | def render_prod(self, context): 55 | css, js = self.get_css_js_paths(context) 56 | 57 | resp = [] 58 | if css: 59 | resp.append(CSS_TMPL % (css_url(css),)) 60 | if js: 61 | resp.append(JS_TMPL % (js_url(js),)) 62 | 63 | return '\n'.join(resp) 64 | 65 | def get_css_js_paths(self, context): 66 | pre = conf.SIMPLESTATIC_DIR 67 | css, js = [], [] 68 | for node in self.nodes: 69 | if isinstance(node, CSSNode): 70 | css.append(os.path.join(pre, node.resolve(context))) 71 | elif isinstance(node, JSNode): 72 | js.append(os.path.join(pre, node.resolve(context))) 73 | return css, js 74 | 75 | 76 | @register.tag 77 | def simplestatic(parser, token): 78 | tag_name = token.split_contents()[0] 79 | nodes = parser.parse('end%s' % (tag_name,)) 80 | parser.delete_first_token() 81 | return SimpleStaticNode(nodes) 82 | 83 | 84 | @register.tag 85 | def compress_css(parser, token): 86 | path = token.split_contents()[1] 87 | return CSSNode(path) 88 | 89 | 90 | @register.tag 91 | def compress_js(parser, token): 92 | path = token.split_contents()[1] 93 | return JSNode(path) 94 | 95 | 96 | @register.tag 97 | def simplestatic_url(parser, token): 98 | path = token.split_contents()[1] 99 | return URLNode(path) 100 | -------------------------------------------------------------------------------- /simplestatic/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from simplestatic import conf 4 | 5 | 6 | def simplestatic_debug_urls(): 7 | if not conf.SIMPLESTATIC_DEBUG: 8 | return patterns('') 9 | 10 | return patterns('', url( 11 | r'^%s(?P.*)$' % conf.SIMPLESTATIC_DEBUG_PATH, 12 | 'django.views.static.serve', 13 | {'show_indexes': True, 'document_root': conf.SIMPLESTATIC_DIR}, 14 | )) 15 | --------------------------------------------------------------------------------