##########################
Django Assets Livereload
##########################

Django Assets Livereload is an enhanced version of the Django `staticfiles <https://docs.djangoproject.com/en/1.11/ref/contrib/staticfiles/>`_ app
which was inspired by the excellent `Django Pipeline <https://django-pipeline.readthedocs.io/en/latest/index.html>`__.  Its main features are:

 - Asset preprocessing (compiling sass files, minifying javascript, ...)
 - Livereload functionality (automatically preprocessing an asset, copying it to
   the static root directory and refreshing browser tabs when the asset changes)

Note that it has no connection with the `Django Assets <https://django-assets.readthedocs.io>`_ package.

Dependencies
============
  - Python3 (might work on Python2 but was not tested)

For the livereload functionality

  - `gevent <http://www.gevent.org/>`_, `geventwebsocket <https://gitlab.com/noppo/gevent-websocket>`_, `inotify <https://github.com/dsoprea/PyInotify>`_

For the preprocessing functionality

  - `cssmin <https://github.com/zacharyvoase/cssmin>`_ for minifying css files
  - `pyScss <https://github.com/Kronuz/pyScss>`_ (or the sass command) for compiling sass files
  - `google's clojure compiler <https://developers.google.com/closure/compiler/>`_ (which must be available on the PATH as a ``clojure`` or ``clojure-compiler`` executable)
    for minifying javascript
  - `ttf2woff2 <https://github.com/nfroidure/ttf2woff2>`_ for compiling ttf fonts to woff2 fonts (install it via ``npm -g install ttf2woff2``)


Installation
============

.. code-block:: bash

    $ pip install django-assets-livereload


Using the app
=============

As usual, to use the app include it in the ``INSTALLED_APPS`` setting in ``settings.py``:

.. code-block:: python

    INSTALLED_APPS = (
        ...
        'django.contrib.staticfiles',
        'assets',
    )

Note that, as shown above, you need to include the standard staticfiles app as well! Next
set the ``STATICFILES_FINDERS`` variable in ``settings.py`` to:

.. code-block:: python

    STATICFILES_FINDERS = (
        'assets.finders.ManifestFinder',
        'assets.finders.ManifestAppDirsFinder',
    )

Your app should now work exactly as before. To use the additional features you need to include a ``manifest.json`` file in the
static directories. The file describes where to find the assets and how to process them. We will get to the format in a moment.
First, however, it will be instructive to describe another feature of the app---the ``asset`` tag. Note that the ``asset`` tag
is optional, the standard ``static`` tag should still work.

The ``Asset`` tag
-----------------

The ``asset`` tag is compatible with both the standard Django template
system as well as with `Jinja2 <https://docs.djangoproject.com/en/1.11/topics/templates/#django.template.backends.jinja2.Jinja2>`_.
To use it in Django templates, you need to load the ``asset`` library in the templates where you want to use it:

.. code-block:: django

    {% load asset %}

You use the tag whenever you want to reference an asset in your templates. It has a single string argument---``bundle_id``---which
identifies the asset you want to reference. A typical usage would be as follows:

.. code-block:: django

    {% asset 'myapp:javascript:jquery': %}

The ``id`` consists of three parts separated by a colon (``:``). The first part, called the *prefix*, is optional. It is used
for namespacing reasons. If the asset comes from an app, the prefix should be the app's name. Otherwise it should be omitted.
The second part is called a *scope*. A scope specifies the type of asset being referenced and controls what will be the output of
the asset tag. There are a few predefined scopes, but you can use arbitrary alphanumeric strings as scope identifiers. The following
table shows the predefined scopes with the asset tag output (the last row shows the output for scopes which are not predefined):

+--------------+------------------+--------------------------------------------------------------------------------------------------+
| **Scope**    | **Aliases**      | **Output**                                                                                       |
+==============+==================+==================================================================================================+
| javascript   |  js, scripts     | ``<script src='{{ url }}'></script>``                                                            |
+--------------+------------------+--------------------------------------------------------------------------------------------------+
| stylesheets  |  styles          | ``<link rel='stylesheet' href='{{ url }}' type='text/css'></link>``                              |
+--------------+------------------+--------------------------------------------------------------------------------------------------+
| images       |                  | ``src='{{ url }}'``                                                                              |
+--------------+------------------+--------------------------------------------------------------------------------------------------+
| all other    |                  | ``{{ url }}``                                                                                    |
+--------------+------------------+--------------------------------------------------------------------------------------------------+

The ``{{ url }}`` in the output above is replaced by the static url of the referenced asset.

The last part of the ``bundle_id`` identifies the asset(s) being referenced. It can either be an id uniquely
identifying the referenced asset (in the given namespace), a path to the referenced asset (relative to some subdirectory of a static directory)
or a pattern used to match against asset ids. The pattern can have three forms. Either it starts with a ``*`` and
ends with a ``*``. In that case all asset ids which contain the rest of the pattern match. Next if the pattern ends with
a ``*`` all ids starting with the pattern match. Finally, the pattern is interpreted as a regular expression
and matching ids are those which match the expression at applied at the start (as tested by the ``re.match`` function).
If multiple ids match, the output in the above table is repeated for each matched asset.

The Manifest file
-----------------

The special features of the app are used by supplying a manifest file which describes which assets to include and
how to process them. Each manifest describes a single static directory in ``settings.STATIC_DIRS``.  It is
formatted as a `JSON <https://en.wikipedia.org/wiki/JSON>`_ file encoding a single object.
The keys to this object are *scopes* (see above). Each scope has a ``target_dir`` attribute which defines under
what directory the static files from this scope will live. It must also include either an ``items`` key
or a ``copy`` key.

If it contains a ``copy`` key, the value must be an object with two keys: ``sources`` and
``pattern`` (and, optionally, a ``filters`` key and a ``target_pattern`` key). The sources is a list of subdirectory
paths (relative to the top-level directory contining the ``manifest.json`` file) and ``pattern`` is a regular expression.
When running the ``collectstatic`` command, all files in any subdirectory of any path specified in the ``sources`` list whose
filenames match the given pattern will be copied into the ``target_dir`` (and, optionally, passed through the specified filters)
subdirectory (possibly prefixed by the app name) of ``settings.STATIC_ROOT`` (if the ``target_pattern`` is specified, the file
is renamed to the result of running ``pattern.sub(target_pattern, src_name)`` on the filename).


If it contains an ``items`` key, this must be an object with keys being asset ids (used by the ``asset`` tag,
see above) and values being objects describing the given asset. An object describing an asset has to have a
``sources`` key and can optionally contain a ``filters`` key and a ``watch`` key. The ``sources`` key should
be a list of filepaths (relative to the top-level directory contining the ``manifest.json`` file).
The content of these files will be concatenated together, optionally passed through specified filters,
and saved to a file under the ``target_dir`` subdirectory (possibly prefixed by the app name) of
``settings.STATIC_ROOT``. The filename is the asset id to which an optional extension
(given by the containing scope's optional ``target_ext`` key) is appended. If the ``watch`` key is specified,
it is a list of files changes to which will lead to a recompilation of the assset when using the
Livereload functionality (see below)

For example if the ``myapp/static/`` directory contains the following file ``manifest.json`` file:

.. code-block:: json

    {
        "javascript": {
            "target_dir":"js",
            "target_ext":".js",
            "items": {
                "courses":{"sources":["IS/js/controllers.js"]},
                "schedule":{"sources":["IS/js/sched.js", "IS/js/time.js"]}
            }
        },
        "python": {
            "target_dir":"python",
            "copy":{
                    "sources":["IS/python"],
                    "pattern":".*\\.py"
            }
        },
        "fonts": {
            "target_dir":"fonts",
            "copy":{
                    "sources":["IS/fonts"],
                    "pattern":"(.*)\\.ttf",
                    "target_pattern":"\\1.woff2",
                    "filters":["ttf2woff2"]
            }
        },
        "stylesheets": {
            "target_dir":"css",
            "target_ext":".css",
            "items": {
                "IS": {"sources":["IS/sass/IS.scss"],
                       "watch":["IS/sass/*.scss","IS/sass/vendor/_bootstrap.scss"],
                       "filters":["sass","cssmin"]}
            }
        }
    }

Then, when running the ``collectstatic`` management command, the following actions will be taken:

    - the contents of ``myapp/static/IS/js/controllers.js`` will be copied to ``STATIC_ROOT/myapp/js/courses.js``
    - the contents of ``myapp/static/IS/js/sched.js`` and ``myapp/static/IS/js/time.js`` will concatenated and saved to ``STATIC_ROOT/myapp/js/schedule.js``
    - all files living below ``myapp/static/IS/python`` and ending with a ``.py`` will be copied to ``STATIC_ROOT/myapp/python`` directory
    - all files living below ``myapp/static/IS/fonts`` and ending with ``.ttf`` will be converted (using the ``ttf2woff2`` command) to woff2 and copied
      to ``STATIC_ROOT/myapp/fonts`` directory and renamed to end with a ``woff2`` extension
    - the file ``myapp/static/IS/sass/IS.scss`` will be compiled by sass, its output will be minified
      using cssmin and saved to ``STATIC_ROOT/myapp/css/IS.css``

Assuming the same ``manifest.json`` the following are some examples of ``asset`` tag usage in templates:

.. code-block:: django

    {% asset 'myapp:javascript:*' %}

would result in

.. code-block:: html

    <script src='STATIC_URL/myapp/js/courses.js'></script>
    <script src='STATIC_URL/myapp/js/schedule.js'></script>

One can also specify just a single asset:

.. code-block:: django

    {% asset 'myapp:javascript:courses' %}

resulting in

.. code-block:: html

    <script src='STATIC_URL/myapp/js/courses.js'></script>

Including a stylesheet:

.. code-block:: django

    {% asset 'myapp:stylesheets:IS' %}

results in

.. code-block:: html

    <link rel='stylesheet' href='STATIC_URL/myapp/css/IS.css' type='text/css'></link>

LiveReload functionality
------------------------

To use the livereload functionality use the provided ``liveserver`` command instead of ``manage.py runserver``:

.. code-block:: bash

    $ ./manage.py liveserver

This will serve your static files as well as your app. When you now open the site in your browser and
change any of your assets (or, in case of processed files, any of the source files) your browser should
reload with the changes visible. The browser should also reload when you change any code in your app.

When this command is run, it creates a ``livereload.pid`` in the current directory. You can then issue

.. code-block:: bash

    $ ./manage.py reload

to force browser refresh and

.. code-block:: bash

    $ ./manage.py restart

to force restarting the app server.

Please note that the implementation of the livereload script is not as efficient as the official
`livereload <http://livereload.com/>`_. Eventually, I might use the real livereload script (which has
the advantage of reloading only the resources that actually changed). For now, I don't think it is
worth the effort.

Alternatives
============


  - `Django Pipeline <https://django-pipeline.readthedocs.io/en/latest/index.html>`_ this is an excellent solution which is supported and used by big sites; the downside is that it is only geared towards stylesheets and javascript (so it doesn't handle images, for example; or other media), it doesn't have livereload functionality and its settings live in the settings.py (my personal preference is to keep the asset stuff out of the settings). If you don't need to handle images (or are fine with handling them via staticfiles) and don't need livereload, definitely go for Django Pipeline!
  - `Django Compressor <https://django-compressor.readthedocs.io/en/latest/>`_ another nice library, haven't tried it though. Does not have livereload.
  - `django-livereload-server <https://github.com/tjwalch/django-livereload-server>`_ livereload but without the pipeline stuff (i.e. you can't use it to compile your files whenever you change the sources)
  - `Django Assets <https://django-assets.readthedocs.io>`_ does not have livereload, otherwise looks interesting, uses
  the webassets python package; I personally don't like the fact that assets are defined in python source files


LICENSE
=======

MIT License

Copyright (c) 2017 Jonathan L. Verner

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


