├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── requirements.txt ├── setup.py └── static_autocollect ├── __init__.py ├── apps.py └── management ├── __init__.py └── commands ├── __init__.py └── watch_static.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 4 | 5 | *.iml 6 | 7 | ## Directory-based project format: 8 | .idea/ 9 | # if you remove the above rule, at least ignore the following: 10 | 11 | # User-specific stuff: 12 | # .idea/workspace.xml 13 | # .idea/tasks.xml 14 | # .idea/dictionaries 15 | 16 | # Sensitive or high-churn files: 17 | # .idea/dataSources.ids 18 | # .idea/dataSources.xml 19 | # .idea/sqlDataSources.xml 20 | # .idea/dynamic.xml 21 | # .idea/uiDesigner.xml 22 | 23 | # Gradle: 24 | # .idea/gradle.xml 25 | # .idea/libraries 26 | 27 | # Mongo Explorer plugin: 28 | # .idea/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.ipr 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | /out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Crashlytics plugin (for Android Studio and IntelliJ) 46 | com_crashlytics_export_strings.xml 47 | crashlytics.properties 48 | crashlytics-build.properties 49 | ### Python template 50 | # Byte-compiled / optimized / DLL files 51 | __pycache__/ 52 | *.py[cod] 53 | *$py.class 54 | 55 | # C extensions 56 | *.so 57 | 58 | # Distribution / packaging 59 | .Python 60 | env/ 61 | build/ 62 | develop-eggs/ 63 | dist/ 64 | downloads/ 65 | eggs/ 66 | .eggs/ 67 | lib/ 68 | lib64/ 69 | parts/ 70 | sdist/ 71 | var/ 72 | *.egg-info/ 73 | .installed.cfg 74 | *.egg 75 | 76 | # PyInstaller 77 | # Usually these files are written by a python script from a template 78 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 79 | *.manifest 80 | *.spec 81 | 82 | # Installer logs 83 | pip-log.txt 84 | pip-delete-this-directory.txt 85 | 86 | # Unit test / coverage reports 87 | htmlcov/ 88 | .tox/ 89 | .coverage 90 | .coverage.* 91 | .cache 92 | nosetests.xml 93 | coverage.xml 94 | *,cover 95 | 96 | # Translations 97 | *.mo 98 | *.pot 99 | 100 | # Django stuff: 101 | *.log 102 | 103 | # Sphinx documentation 104 | docs/_build/ 105 | 106 | # PyBuilder 107 | target/ 108 | ### OSX template 109 | .DS_Store 110 | .AppleDouble 111 | .LSOverride 112 | 113 | # Icon must end with two \r 114 | Icon 115 | 116 | # Thumbnails 117 | ._* 118 | 119 | # Files that might appear in the root of a volume 120 | .DocumentRevisions-V100 121 | .fseventsd 122 | .Spotlight-V100 123 | .TemporaryItems 124 | .Trashes 125 | .VolumeIcon.icns 126 | 127 | # Directories potentially created on remote AFP share 128 | .AppleDB 129 | .AppleDesktop 130 | Network Trash Folder 131 | Temporary Items 132 | .apdisk 133 | 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Andrey Rusanov 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 met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of django-static-autocollect nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Sometimes with Django you need to collect static during development. It is really annoying to make it manually every time. 2 | There are a few ways to handle it. 3 | static_autocollect watches for changes in static files and runs ``collectstatic`` command for you automatically. 4 | 5 | Quick start 6 | ----------- 7 | 1. Install with pip: 8 | ``pip install django-static-autocollect`` 9 | 10 | 2. Add "static_autocollect" to your INSTALLED_APPS: 11 | 12 | .. code-block:: python 13 | 14 | INSTALLED_APPS = [ 15 | ... 16 | 'static_autocollect', 17 | ] 18 | # or, since it is the app for development purposes only: 19 | if DEBUG: 20 | INSTALLED_APPS.append('static_autocollect') 21 | 22 | Peronally I just append it in my local settings to INSTALLED_APPS. 23 | 24 | 3. Run ``python manage.py watch_static`` to run static watcher. It will show collectstatic output, so you will be able to see what exactly(and when) has been synced. 25 | 26 | 27 | The lib is working and quite stable already and I will update it with some tests and (perhaps) some minor features soon. 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argh==0.26.1 2 | Django==1.9.5 3 | pathtools==0.1.2 4 | PyYAML==3.11 5 | watchdog==0.8.3 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from setuptools import setup, find_packages 5 | 6 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: 7 | README = readme.read() 8 | 9 | setup( 10 | name='django-static-autocollect', 11 | version='0.1', 12 | url='https://github.com/andreyrusanov/django-static-autocollect', 13 | license='BSD', 14 | description='Runs collectstatic automatically.', 15 | long_description=README, 16 | author='Andrey Rusanov', 17 | author_email='andrey@rusanov.me', 18 | packages=find_packages(), 19 | install_requires=[ 20 | 'Django >= 1.6', 21 | 'watchdog == 0.8.3' 22 | ], 23 | requires=[ 24 | 'Django(>= 1.6)', 25 | 'watchdog(== 0.8.3)' 26 | ], 27 | zip_safe=False, 28 | classifiers=[ 29 | 'Environment :: Web Environment', 30 | 'Framework :: Django', 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: BSD License', 33 | 'Operating System :: OS Independent', 34 | 'Programming Language :: Python', 35 | 'Programming Language :: Python :: 3', 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /static_autocollect/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreyrusanov/django-static-autocollect/d289b92e4f4bc51ce24958a8441005b560fc348a/static_autocollect/__init__.py -------------------------------------------------------------------------------- /static_autocollect/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class StaticAutocollectConfig(AppConfig): 7 | name = 'static_autocollect' 8 | -------------------------------------------------------------------------------- /static_autocollect/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreyrusanov/django-static-autocollect/d289b92e4f4bc51ce24958a8441005b560fc348a/static_autocollect/management/__init__.py -------------------------------------------------------------------------------- /static_autocollect/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreyrusanov/django-static-autocollect/d289b92e4f4bc51ce24958a8441005b560fc348a/static_autocollect/management/commands/__init__.py -------------------------------------------------------------------------------- /static_autocollect/management/commands/watch_static.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import sys 3 | 4 | from django.contrib.staticfiles import finders 5 | from django.conf import settings 6 | from django.core.management import call_command 7 | from django.core.management.base import BaseCommand 8 | 9 | from watchdog.utils.dirsnapshot import DirectorySnapshot, DirectorySnapshotDiff 10 | 11 | 12 | class Command(BaseCommand): 13 | 14 | def handle(self, *args, **options): 15 | snapshots = {} 16 | roots = self.find_static_roots() 17 | 18 | if not roots: 19 | self.write('No static roots has been found in your application') 20 | return 21 | 22 | for path in roots: 23 | snapshots[path] = DirectorySnapshot(path) 24 | 25 | if not os.path.exists(settings.STATIC_ROOT): 26 | self.write('{} does not exist'.format(settings.STATIC_ROOT)) 27 | call_command('collectstatic', interactive=False) 28 | 29 | start_message = 'Directories to watch:\n{}'.format('\n'.join(roots)) 30 | self.write(start_message) 31 | 32 | events = ('files_created', 'files_deleted', 'files_modified', 'files_moved') 33 | try: 34 | while True: 35 | for path in roots: 36 | snapshot = DirectorySnapshot(path) 37 | diff = DirectorySnapshotDiff(snapshot, snapshots[path]) 38 | if any(getattr(diff, event) for event in events): 39 | snapshots[path] = snapshot 40 | self.write('Changes detected') 41 | call_command('collectstatic', interactive=False) 42 | except KeyboardInterrupt: 43 | pass 44 | 45 | @staticmethod 46 | def find_static_roots(): 47 | found_files = set() 48 | for finder in finders.get_finders(): 49 | for path, storage in finder.list([]): 50 | found_files.add(storage.base_location) 51 | return found_files 52 | 53 | @staticmethod 54 | def write(message): 55 | sys.stdout.write('{}\n'.format(message)) 56 | 57 | 58 | --------------------------------------------------------------------------------