├── .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 |
--------------------------------------------------------------------------------