├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── devrecargar
├── __init__.py
├── templates
│ └── devrecargar
│ │ └── devrecargar.js
├── urls.py
└── views.py
├── setup.cfg
└── setup.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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Scott Woodall
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 | include LICENSE
3 | recursive-include devrecargar/templates *
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## django-devrecargar ##
2 | During the development of a Django application, do you get tired of:
3 |
4 | 1. Making a change to source code
5 | 1. Alt-tab to your browser
6 | 1. Refresh
7 | 1. Rinse and repeat
8 |
9 | This project aims to make you more productive by keeping you in the text editor by automatically refreshing the browser after any file modification within your Django project (even CSS, JS or HTML).
10 |
11 | #### Why devrecargar over other solutions? ####
12 | 1. Python only
13 | 1. No browser plugins
14 | 1. Easier for remote workflows where you are SSH'ed into a development server making changes but viewing the site with a browser on your local PC.
15 |
16 | ### Installation ###
17 |
18 | pip install devrecargar
19 |
20 | ### Usage ###
21 | 1. Add `devrecargar` to `INSTALLED_APPS` in `settings.py`
22 | 1. Add an entry to `urls.py`:
23 |
24 | url(r'^devrecargar/', include('devrecargar.urls', namespace='devrecargar'))
25 | 1. Add a javascript snippet to a base template:
26 |
27 |
28 |
29 | ### Configuration ###
30 | devrecargar looks for `BASE_DIR` within `settings.py` as the default directory to recursively watch for file changes. If `BASE_DIR` doesn't exist then you need to set `DEVRECARGAR_PATHS_TO_WATCH` within `settings.py`. This should be a list of dictionaries like so:
31 |
32 | DEVRECARGAR_PATHS_TO_WATCH = [{
33 | 'path': , # required
34 | 'recursive': True, # not required, default is shown here
35 | 'patterns': ['*.html', '*.js', '*.css'] # not required, default is shown here
36 | 'ignore_directories': True, # not required, default is shown here
37 | }]
38 |
39 | If devrecargar doesn't find either of these variables then `django.core.exceptions.ImproperlyConfigured` will be raised.
40 |
41 | ### FAQ ####
42 | #### How It Works ####
43 | The javascript snippet makes a Server-Sent Event (SSE) request to the Django devserver. Anytime the devserver process restarts the SSE request is disconnected within the browser and it will try to automatically reconnect. SSE will send an `open` event after the browser re-establishes the connection to the devserver after it's been restarted. The javascript snippet listens for the `open` event and issues a `location.reload()` that refreshes the browser.
44 |
45 | After modifying a python file the devserver restarts automatically. To get CSS, JS, HTML support we use the [watchdog](http://pythonhosted.org/watchdog/) module to listen for any file modifications. When a `watchdog` modification event happens an `__init__.py` file within the devrecargar project is "touched" which the devserver process notices because it's a python file and restarts itself, which triggers the SSE `open` event to fire.
46 |
47 | #### How can I keep this out of my production environment? ####
48 | * In the HTML template wrap the javascript snippet in a `debug` conditional.
49 |
50 | {% if debug %}{% endif %}
51 |
52 | * You need `django.template.context_processors.debug` added to `TEMPLATE_CONTEXT_PROCESSORS` in `settings.py`. If you don't have `TEMPLATE_CONTEXT_PROCESSORS` defined then by default [Django includes it](https://docs.djangoproject.com/en/1.9/ref/settings/#template-context-processors).
53 | * Be sure your IP address is listed in `INTERNAL_IPS ` within `settings.py`
54 | * In `urls.py` only add the route if `DEBUG=True`
55 |
56 | from django.conf import settings
57 | from django.conf.urls import include, patterns, url
58 | urlpatterns = patterns() # all your other routes
59 | if settings.DEBUG:
60 | urlpatterns += (
61 | url(r'^devrecargar/', include('devrecargar.urls', namespace='devrecargar')
62 | )
63 |
64 | #### Where does the name devrecargar come from? ####
65 | Recargar is Spanish for "reload".
66 |
--------------------------------------------------------------------------------
/devrecargar/__init__.py:
--------------------------------------------------------------------------------
1 | """django-devrecargar - A Django app that automatically reloads your browser when a file changes"""
2 |
3 | __version__ = '0.1.0'
4 | __author__ = 'Scott Woodall '
5 | __all__ = []
6 |
--------------------------------------------------------------------------------
/devrecargar/templates/devrecargar/devrecargar.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | var source = new EventSource("{% url "devrecargar:ping" %}");
3 | var isFirstConnect = true;
4 |
5 | source.addEventListener("open", function () {
6 | if ( ! isFirstConnect) {
7 | console.log("devrecargar: reloading");
8 | location.reload();
9 | }
10 |
11 | isFirstConnect = false;
12 | }, false);
13 | })();
14 |
--------------------------------------------------------------------------------
/devrecargar/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 |
3 | from .views import ping
4 |
5 | app_name = 'devrecargar'
6 |
7 | urlpatterns = [
8 | url(r'^ping/', ping, name='ping'),
9 | ]
10 |
--------------------------------------------------------------------------------
/devrecargar/views.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 |
4 | from django.conf import settings
5 | from django.http import StreamingHttpResponse
6 | from django.core.exceptions import ImproperlyConfigured
7 |
8 | from watchdog.events import PatternMatchingEventHandler
9 | from watchdog.observers import Observer
10 |
11 | INIT_FILE = os.path.join(os.path.dirname(__file__), '__init__.py')
12 |
13 | try:
14 | paths_to_watch = settings.DEVRECARGAR_PATHS_TO_WATCH
15 | except AttributeError:
16 | try:
17 | paths_to_watch = [{'path': settings.BASE_DIR}]
18 | except AttributeError:
19 | message = """
20 | No paths were found for devrecargar to watch. See
21 | https://github.com/scottwoodall/django-devrecargar/blob/master/README.md
22 | for configuring this.
23 | """
24 | raise ImproperlyConfigured(message)
25 |
26 |
27 | class EventHandler(PatternMatchingEventHandler):
28 | def on_modified(self, event):
29 | with open(INIT_FILE, 'a'):
30 | os.utime(INIT_FILE, None)
31 |
32 | observer = Observer()
33 |
34 | for path in paths_to_watch:
35 | event_handler = EventHandler(
36 | patterns=path.get('patterns', ['*.html', '*.js', '*.css']),
37 | ignore_directories=path.get('ignore_directories', True),
38 | )
39 |
40 | observer.schedule(
41 | event_handler,
42 | path['path'],
43 | recursive=path.get('recursive', True),
44 | )
45 |
46 | observer.start()
47 |
48 |
49 | def ping_generator():
50 | # function to hold the SSE connection open
51 | while True:
52 | yield time.time()
53 | time.sleep(30)
54 |
55 |
56 | def ping(request):
57 | pings = (
58 | 'retry:100\ndata:{"time":"%s"}\n\n' % ping
59 | for ping in ping_generator()
60 | )
61 |
62 | response = StreamingHttpResponse(
63 | (ping for ping in pings),
64 | content_type="text/event-stream"
65 | )
66 |
67 | return response
68 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [wheel]
2 | universal = 1
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | setuptools.setup(
4 | name="devrecargar",
5 | version="0.1.4",
6 | url="https://github.com/scottwoodall/django-devrecargar",
7 | author="Scott Woodall",
8 | author_email="scott.woodall@gmail.com",
9 |
10 | description="""
11 | A Django app that automatically reloads your browser when a file
12 | (py, html, js, css) changes.
13 | """,
14 |
15 | long_description=open('README.md').read(),
16 | packages=setuptools.find_packages(),
17 | license="MIT",
18 | install_requires=['watchdog'],
19 | include_package_data=True,
20 |
21 | classifiers=[
22 | 'Environment :: Web Environment',
23 | 'Framework :: Django',
24 | 'Intended Audience :: Developers',
25 | 'Programming Language :: Python',
26 | 'Programming Language :: Python :: 2',
27 | 'Programming Language :: Python :: 2.7',
28 | 'Programming Language :: Python :: 3',
29 | 'Programming Language :: Python :: 3.4',
30 | ],
31 | )
32 |
--------------------------------------------------------------------------------