From b0a1685f46bb88b0474d0380134ba748bbb235e8 Mon Sep 17 00:00:00 2001 From: Sergey Kolekonov Date: Thu, 26 Feb 2015 13:17:46 +0300 Subject: [PATCH] Build python-compressor for Ubuntu 14.04 According to Juno requirements [1], python-django-compressor have to be updated to 1.4 version, Trusty contains old package [1] https://github.com/openstack/requirements/blob/stable/juno/global-requirements.txt#L19 Sources have been copied from packages/precise/python-django-compressor 6.1 branch Commit e0a2d84cb9eb5ff2ec8306ae30787fd1259058d3 Change-Id: Id83f5b47ccea8629bcac949fcb7baac670f0efe7 --- debian/changelog | 55 ++ debian/compat | 1 + debian/control | 32 ++ debian/copyright | 108 ++++ debian/docs | 0 debian/pydist-overrides | 1 + debian/rules | 32 ++ debian/source/format | 1 + debian/watch | 2 + django-compressor/AUTHORS | 94 ++++ django-compressor/LICENSE | 128 +++++ django-compressor/MANIFEST.in | 10 + django-compressor/Makefile | 11 + django-compressor/PKG-INFO | 104 ++++ django-compressor/README.rst | 81 +++ django-compressor/compressor/__init__.py | 2 + django-compressor/compressor/base.py | 339 ++++++++++++ django-compressor/compressor/cache.py | 151 ++++++ django-compressor/compressor/conf.py | 120 +++++ .../compressor/contrib/__init__.py | 0 .../compressor/contrib/jinja2ext.py | 55 ++ .../compressor/contrib/sekizai.py | 18 + django-compressor/compressor/css.py | 53 ++ django-compressor/compressor/exceptions.py | 54 ++ .../compressor/filters/__init__.py | 3 + django-compressor/compressor/filters/base.py | 188 +++++++ .../compressor/filters/closure.py | 10 + .../compressor/filters/css_default.py | 108 ++++ .../compressor/filters/cssmin/__init__.py | 13 + .../compressor/filters/cssmin/cssmin.py | 245 +++++++++ .../compressor/filters/cssmin/rcssmin.py | 360 +++++++++++++ .../compressor/filters/csstidy.py | 10 + .../compressor/filters/datauri.py | 57 ++ .../compressor/filters/jsmin/__init__.py | 10 + .../compressor/filters/jsmin/rjsmin.py | 300 +++++++++++ .../compressor/filters/jsmin/slimit.py | 10 + .../compressor/filters/template.py | 12 + .../compressor/filters/yuglify.py | 26 + django-compressor/compressor/filters/yui.py | 28 + django-compressor/compressor/finders.py | 15 + django-compressor/compressor/js.py | 25 + .../compressor/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/compress.py | 274 ++++++++++ .../management/commands/mtime_cache.py | 82 +++ django-compressor/compressor/models.py | 1 + .../compressor/offline/__init__.py | 0 .../compressor/offline/django.py | 143 +++++ .../compressor/offline/jinja2.py | 125 +++++ .../compressor/parser/__init__.py | 34 ++ django-compressor/compressor/parser/base.py | 42 ++ .../compressor/parser/beautifulsoup.py | 48 ++ .../compressor/parser/default_htmlparser.py | 79 +++ .../compressor/parser/html5lib.py | 59 +++ django-compressor/compressor/parser/lxml.py | 81 +++ django-compressor/compressor/signals.py | 4 + django-compressor/compressor/storage.py | 96 ++++ .../templates/compressor/css_file.html | 1 + .../templates/compressor/css_inline.html | 1 + .../templates/compressor/js_file.html | 1 + .../templates/compressor/js_inline.html | 1 + .../compressor/templatetags/__init__.py | 0 .../compressor/templatetags/compress.py | 214 ++++++++ django-compressor/compressor/test_settings.py | 40 ++ .../compressor/tests/__init__.py | 0 .../compressor/tests/precompiler.py | 35 ++ .../compressor/tests/static/css/datauri.css | 4 + .../compressor/tests/static/css/nonasc.css | 1 + .../compressor/tests/static/css/one.css | 1 + .../compressor/tests/static/css/two.css | 1 + .../tests/static/css/url/2/url2.css | 5 + .../tests/static/css/url/nonasc.css | 2 + .../compressor/tests/static/css/url/test.css | 1 + .../compressor/tests/static/css/url/url1.css | 5 + .../compressor/tests/static/img/add.png | Bin 0 -> 733 bytes .../compressor/tests/static/img/python.png | Bin 0 -> 11155 bytes .../tests/static/js/nonasc-latin1.js | 1 + .../compressor/tests/static/js/nonasc.js | 1 + .../compressor/tests/static/js/one.coffee | 1 + .../compressor/tests/static/js/one.js | 1 + .../compressor/tests/test_base.py | 270 ++++++++++ .../compressor/tests/test_filters.py | 303 +++++++++++ .../compressor/tests/test_jinja2ext.py | 156 ++++++ .../compressor/tests/test_offline.py | 499 ++++++++++++++++++ .../compressor/tests/test_parsers.py | 125 +++++ .../compressor/tests/test_signals.py | 68 +++ .../compressor/tests/test_storages.py | 64 +++ .../basic/test_compressor_offline.html | 8 + .../test_templates/test_block_super/base.html | 15 + .../test_compressor_offline.html | 13 + .../base.html | 10 + .../base2.html | 8 + .../test_compressor_offline.html | 8 + .../test_block_super_extra/base.html | 15 + .../test_compressor_offline.html | 19 + .../test_block_super_multiple/base.html | 15 + .../test_block_super_multiple/base2.html | 10 + .../test_compressor_offline.html | 11 + .../base.html | 15 + .../base2.html | 3 + .../test_compressor_offline.html | 11 + .../test_complex/test_compressor_offline.html | 20 + .../test_compressor_offline.html | 7 + .../test_compressor_offline.html | 13 + .../test_error_handling/buggy_extends.html | 10 + .../test_error_handling/buggy_template.html | 12 + .../test_error_handling/missing_extends.html | 10 + .../test_compressor_offline.html | 8 + .../with_coffeescript.html | 7 + .../test_compressor_offline.html | 7 + .../test_compressor_offline.html | 6 + .../test_compressor_offline.html | 7 + .../test_compressor_offline.html | 7 + .../basic/test_compressor_offline.html | 8 + .../test_block_super/base.html | 15 + .../test_compressor_offline.html | 12 + .../test_block_super_extra/base.html | 15 + .../test_compressor_offline.html | 18 + .../test_block_super_multiple/base.html | 15 + .../test_block_super_multiple/base2.html | 3 + .../test_compressor_offline.html | 10 + .../base.html | 15 + .../base2.html | 3 + .../test_compressor_offline.html | 10 + .../test_coffin/test_compressor_offline.html | 11 + .../test_complex/test_compressor_offline.html | 24 + .../test_compressor_offline.html | 7 + .../test_error_handling/buggy_extends.html | 9 + .../test_error_handling/buggy_template.html | 10 + .../test_error_handling/missing_extends.html | 9 + .../test_compressor_offline.html | 8 + .../with_coffeescript.html | 5 + .../test_compressor_offline.html | 7 + .../test_jingo/test_compressor_offline.html | 11 + .../test_compressor_offline.html | 6 + .../test_compressor_offline.html | 7 + .../test_compressor_offline.html | 7 + .../compressor/tests/test_templatetags.py | 256 +++++++++ .../compressor/utils/__init__.py | 73 +++ .../compressor/utils/decorators.py | 64 +++ .../compressor/utils/staticfiles.py | 27 + .../compressor/utils/stringformat.py | 260 +++++++++ .../django_compressor.egg-info/PKG-INFO | 104 ++++ .../django_compressor.egg-info/SOURCES.txt | 156 ++++++ .../dependency_links.txt | 1 + .../django_compressor.egg-info/not-zip-safe | 1 + .../django_compressor.egg-info/requires.txt | 1 + .../django_compressor.egg-info/top_level.txt | 1 + django-compressor/docs/Makefile | 130 +++++ django-compressor/docs/behind-the-scenes.txt | 56 ++ django-compressor/docs/changelog.txt | 398 ++++++++++++++ django-compressor/docs/conf.py | 222 ++++++++ django-compressor/docs/contributing.txt | 174 ++++++ django-compressor/docs/django-sekizai.txt | 24 + django-compressor/docs/index.txt | 47 ++ django-compressor/docs/jinja2.txt | 175 ++++++ django-compressor/docs/make.bat | 170 ++++++ django-compressor/docs/quickstart.txt | 100 ++++ django-compressor/docs/remote-storages.txt | 91 ++++ django-compressor/docs/scenarios.txt | 67 +++ django-compressor/docs/settings.txt | 465 ++++++++++++++++ django-compressor/docs/usage.txt | 234 ++++++++ django-compressor/requirements/tests.txt | 10 + django-compressor/setup.cfg | 8 + django-compressor/setup.py | 145 +++++ django-compressor/tox.ini | 121 +++++ 166 files changed, 9843 insertions(+) create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/docs create mode 100644 debian/pydist-overrides create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 debian/watch create mode 100644 django-compressor/AUTHORS create mode 100644 django-compressor/LICENSE create mode 100644 django-compressor/MANIFEST.in create mode 100644 django-compressor/Makefile create mode 100644 django-compressor/PKG-INFO create mode 100644 django-compressor/README.rst create mode 100644 django-compressor/compressor/__init__.py create mode 100644 django-compressor/compressor/base.py create mode 100644 django-compressor/compressor/cache.py create mode 100644 django-compressor/compressor/conf.py create mode 100644 django-compressor/compressor/contrib/__init__.py create mode 100644 django-compressor/compressor/contrib/jinja2ext.py create mode 100644 django-compressor/compressor/contrib/sekizai.py create mode 100644 django-compressor/compressor/css.py create mode 100644 django-compressor/compressor/exceptions.py create mode 100644 django-compressor/compressor/filters/__init__.py create mode 100644 django-compressor/compressor/filters/base.py create mode 100644 django-compressor/compressor/filters/closure.py create mode 100644 django-compressor/compressor/filters/css_default.py create mode 100644 django-compressor/compressor/filters/cssmin/__init__.py create mode 100644 django-compressor/compressor/filters/cssmin/cssmin.py create mode 100644 django-compressor/compressor/filters/cssmin/rcssmin.py create mode 100644 django-compressor/compressor/filters/csstidy.py create mode 100644 django-compressor/compressor/filters/datauri.py create mode 100644 django-compressor/compressor/filters/jsmin/__init__.py create mode 100755 django-compressor/compressor/filters/jsmin/rjsmin.py create mode 100644 django-compressor/compressor/filters/jsmin/slimit.py create mode 100644 django-compressor/compressor/filters/template.py create mode 100644 django-compressor/compressor/filters/yuglify.py create mode 100644 django-compressor/compressor/filters/yui.py create mode 100644 django-compressor/compressor/finders.py create mode 100644 django-compressor/compressor/js.py create mode 100644 django-compressor/compressor/management/__init__.py create mode 100644 django-compressor/compressor/management/commands/__init__.py create mode 100644 django-compressor/compressor/management/commands/compress.py create mode 100644 django-compressor/compressor/management/commands/mtime_cache.py create mode 100644 django-compressor/compressor/models.py create mode 100644 django-compressor/compressor/offline/__init__.py create mode 100644 django-compressor/compressor/offline/django.py create mode 100644 django-compressor/compressor/offline/jinja2.py create mode 100644 django-compressor/compressor/parser/__init__.py create mode 100644 django-compressor/compressor/parser/base.py create mode 100644 django-compressor/compressor/parser/beautifulsoup.py create mode 100644 django-compressor/compressor/parser/default_htmlparser.py create mode 100644 django-compressor/compressor/parser/html5lib.py create mode 100644 django-compressor/compressor/parser/lxml.py create mode 100644 django-compressor/compressor/signals.py create mode 100644 django-compressor/compressor/storage.py create mode 100644 django-compressor/compressor/templates/compressor/css_file.html create mode 100644 django-compressor/compressor/templates/compressor/css_inline.html create mode 100644 django-compressor/compressor/templates/compressor/js_file.html create mode 100644 django-compressor/compressor/templates/compressor/js_inline.html create mode 100644 django-compressor/compressor/templatetags/__init__.py create mode 100644 django-compressor/compressor/templatetags/compress.py create mode 100644 django-compressor/compressor/test_settings.py create mode 100644 django-compressor/compressor/tests/__init__.py create mode 100644 django-compressor/compressor/tests/precompiler.py create mode 100644 django-compressor/compressor/tests/static/css/datauri.css create mode 100644 django-compressor/compressor/tests/static/css/nonasc.css create mode 100644 django-compressor/compressor/tests/static/css/one.css create mode 100644 django-compressor/compressor/tests/static/css/two.css create mode 100644 django-compressor/compressor/tests/static/css/url/2/url2.css create mode 100644 django-compressor/compressor/tests/static/css/url/nonasc.css create mode 100644 django-compressor/compressor/tests/static/css/url/test.css create mode 100644 django-compressor/compressor/tests/static/css/url/url1.css create mode 100644 django-compressor/compressor/tests/static/img/add.png create mode 100644 django-compressor/compressor/tests/static/img/python.png create mode 100644 django-compressor/compressor/tests/static/js/nonasc-latin1.js create mode 100644 django-compressor/compressor/tests/static/js/nonasc.js create mode 100644 django-compressor/compressor/tests/static/js/one.coffee create mode 100644 django-compressor/compressor/tests/static/js/one.js create mode 100644 django-compressor/compressor/tests/test_base.py create mode 100644 django-compressor/compressor/tests/test_filters.py create mode 100644 django-compressor/compressor/tests/test_jinja2ext.py create mode 100644 django-compressor/compressor/tests/test_offline.py create mode 100644 django-compressor/compressor/tests/test_parsers.py create mode 100644 django-compressor/compressor/tests/test_signals.py create mode 100644 django-compressor/compressor/tests/test_storages.py create mode 100644 django-compressor/compressor/tests/test_templates/basic/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super/base.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base2.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super_extra/base.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super_extra/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super_multiple/base.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super_multiple/base2.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super_multiple/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base2.html create mode 100644 django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_complex/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_condition/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_duplicate/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_error_handling/buggy_extends.html create mode 100644 django-compressor/compressor/tests/test_templates/test_error_handling/buggy_template.html create mode 100644 django-compressor/compressor/tests/test_templates/test_error_handling/missing_extends.html create mode 100644 django-compressor/compressor/tests/test_templates/test_error_handling/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_error_handling/with_coffeescript.html create mode 100644 django-compressor/compressor/tests/test_templates/test_inline_non_ascii/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_static_templatetag/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_templatetag/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates/test_with_context/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/basic/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_block_super/base.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_block_super/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/base.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base2.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base2.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_coffin/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_complex/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_condition/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_extends.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_template.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/missing_extends.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/with_coffeescript.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_inline_non_ascii/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_jingo/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_static_templatetag/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_templatetag/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templates_jinja2/test_with_context/test_compressor_offline.html create mode 100644 django-compressor/compressor/tests/test_templatetags.py create mode 100644 django-compressor/compressor/utils/__init__.py create mode 100644 django-compressor/compressor/utils/decorators.py create mode 100644 django-compressor/compressor/utils/staticfiles.py create mode 100644 django-compressor/compressor/utils/stringformat.py create mode 100644 django-compressor/django_compressor.egg-info/PKG-INFO create mode 100644 django-compressor/django_compressor.egg-info/SOURCES.txt create mode 100644 django-compressor/django_compressor.egg-info/dependency_links.txt create mode 100644 django-compressor/django_compressor.egg-info/not-zip-safe create mode 100644 django-compressor/django_compressor.egg-info/requires.txt create mode 100644 django-compressor/django_compressor.egg-info/top_level.txt create mode 100644 django-compressor/docs/Makefile create mode 100644 django-compressor/docs/behind-the-scenes.txt create mode 100644 django-compressor/docs/changelog.txt create mode 100644 django-compressor/docs/conf.py create mode 100644 django-compressor/docs/contributing.txt create mode 100644 django-compressor/docs/django-sekizai.txt create mode 100644 django-compressor/docs/index.txt create mode 100644 django-compressor/docs/jinja2.txt create mode 100644 django-compressor/docs/make.bat create mode 100644 django-compressor/docs/quickstart.txt create mode 100644 django-compressor/docs/remote-storages.txt create mode 100644 django-compressor/docs/scenarios.txt create mode 100644 django-compressor/docs/settings.txt create mode 100644 django-compressor/docs/usage.txt create mode 100644 django-compressor/requirements/tests.txt create mode 100644 django-compressor/setup.cfg create mode 100644 django-compressor/setup.py create mode 100644 django-compressor/tox.ini diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..655fbb6 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,55 @@ +python-django-compressor (1.4-0ubuntu1~cloud0~mos6.1) trusty; urgency=low + + * Build python-django-compressor for Ubuntu 14.04 + + -- Sergey Kolekonov Thu, 26 Feb 2015 13:13:11 +0300 + +python-django-compressor (1.4-0ubuntu1~cloud0) precise-juno; urgency=low + + * Update version for MOS + + -- Sergey Otpouschennikov Tue, 13 Aug 2013 15:40:41 +0300 + +python-django-compressor (1.3-1ubuntu3) trusty; urgency=medium + + * d/control: Drop python-beautifulsoup from BD's (LP: #1252627), its + only required to run tests and is a optional parser at runtime. + * d/rules: Correct path for django-admin.py so tests actually run, + even if the result is ignored. + + -- James Page Fri, 07 Mar 2014 13:09:39 +0000 + +python-django-compressor (1.3-1ubuntu2) trusty; urgency=medium + + * Rebuild to drop files installed into /usr/share/pyshared. + + -- Matthias Klose Sun, 23 Feb 2014 13:51:17 +0000 + +python-django-compressor (1.3-1ubuntu1) trusty; urgency=medium + + * Drop use of external discover-runner as this is included in + django >= 1.6 (LP: #1252627): + - d/patches/django-1.6-compat.patch: Patch out use of discover_runner. + - d/control: Drop BD on python-django-discover-runner, version BD on + python-django >= 1.6. + + -- James Page Wed, 08 Jan 2014 10:32:15 +0000 + +python-django-compressor (1.3-1) unstable; urgency=low + + * New upstream release. + * Added unit tests build-depends and ran wrap-and-sort. + + -- Thomas Goirand Wed, 26 Jun 2013 14:29:00 +0800 + +python-django-compressor (1.2-2) unstable; urgency=low + + * Uploading to unstable. + + -- Thomas Goirand Sun, 12 May 2013 15:20:14 +0000 + +python-django-compressor (1.2-1) experimental; urgency=low + + * Initial release. + + -- Thomas Goirand Sun, 14 Oct 2012 10:51:47 +0000 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..b621576 --- /dev/null +++ b/debian/control @@ -0,0 +1,32 @@ +Source: python-django-compressor +Section: python +Priority: optional +Maintainer: MOS Horizon Team +Build-Depends: debhelper (>= 9), + openstack-pkg-tools, + python-all (>= 2.6.6-3~), + python-setuptools +Build-Depends-Indep: python-appconf, + python-coverage, + python-django (>= 1.6), + python-html5lib, + python-jinja2, + python-lxml, + python-mock, + python-nose, + python-unittest2 +Standards-Version: 3.9.4 +Homepage: http://pypi.python.org/pypi/django_compressor/ + +Package: python-compressor +Architecture: all +Pre-Depends: dpkg (>= 1.15.6~) +Depends: python-appconf, + python-django (>= 1.1), + ${misc:Depends}, + ${python:Depends} +Provides: ${python:Provides} +Description: Compresses linked and inline JavaScript or CSS into single cached files + Django Compressor combines and compresses linked and inline Javascript or CSS + in a Django templates into cacheable static files by using the compress + template tag. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..c573e42 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,108 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: python-django-compressor +Source: http://pypi.python.org/pypi/django_compressor/ + +Files: debian/* +Copyright: (c) 2012, Thomas Goirand +License: MIT + +Files: compressor/filters/jsmin/rjsmin.py +Copyright: 2006-2011, André Malo +License: Apache-2.0 + +Files: compressor/filters/cssmin/* +Copyright: (c) 2010 Zachary Voase +License: MIT + +Files: compressor/utils/decorators.py +Copyright: 2009-2011, Ask Solem and contributors +License: BSD-2-clauses + +Files: compressor/utils/stringformat.py +Copyright: 2010, Florent Xicluna +License: BSD-3-clauses + +Files: * +Copyright: 2009-2011 django_compressor authors (see AUTHORS file) + 2008 Andreas Pelme + 2006-2011 André Malo or his licensors, as applicable + 2009-2011 Ask Solem and contributors. + 2010 by Florent Xicluna. +License: MIT + +License: MIT + 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. + +License: BSD-3-clauses + Redistribution and use in source and binary forms of the software as well as + documentation, with or without modification, are permitted provided that the + following conditions are met: + . + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The names of the contributors may not be used to endorse or promote products + derived from this software without specific prior written permission. + . + THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + +License: BSD-2-clauses + Redistribution and use in source and binary forms of the software as well as + documentation, with or without modification, are permitted provided that the + following conditions are met: + . + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + . + THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); you may not + use this file except in compliance with the License. You may obtain a copy of + the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations under + the License. + . + On Debian based systems, the full text of the Apache-2.0 license is available + in this file: /usr/share/common-licenses/Apache-2.0 diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000..e69de29 diff --git a/debian/pydist-overrides b/debian/pydist-overrides new file mode 100644 index 0000000..3a0edc6 --- /dev/null +++ b/debian/pydist-overrides @@ -0,0 +1 @@ +django_appconf python-appconf diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..b4db7a3 --- /dev/null +++ b/debian/rules @@ -0,0 +1,32 @@ +#!/usr/bin/make -f + +#export DH_VERBOSE=1 + +UPSTREAM_GIT = git://github.com/jezdez/django_compressor.git + +include /usr/share/openstack-pkg-tools/pkgos.make + +%: + dh $@ --with python2 + +PYDEF=$(shell pyversions -d) + +ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS))) +override_dh_auto_test: + PYTHONPATH=$PYTHONPATH:. python /usr/lib/$(PYDEF)/dist-packages/django/bin/django-admin.py test --settings=compressor.test_settings compressor || true + rm -rf $(CURDIR)/compressor/tests/static/CACHE +endif + +override_dh_auto_build: + +override_dh_install: + set -e ; for i in `pyversions -s` ; do \ + $$i setup.py install --install-layout=deb --root=debian/python-compressor ; \ + rm -f $(CURDIR)/debian/usr/lib/$$i/dist-packages/compressor/tests/static/CACHE/css/* ; \ + rm -f $(CURDIR)/debian/usr/lib/$$i/dist-packages/compressor/tests/static/CACHE/js/* ; \ + done + find debian/python-compressor -iname '*.pyc' -delete + +override_dh_usrlocal: + rm -f $(CURDIR)/debian/usr/share/pyshared/compressor/tests/static/CACHE/css/* + rm -f $(CURDIR)/debian/usr/share/pyshared/compressor/tests/static/CACHE/js/* diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/watch b/debian/watch new file mode 100644 index 0000000..a0ca5aa --- /dev/null +++ b/debian/watch @@ -0,0 +1,2 @@ +version=3 +http://pypi.python.org/packages/source/d/django-compressor/django-compressor-(.*)\.tar.gz diff --git a/django-compressor/AUTHORS b/django-compressor/AUTHORS new file mode 100644 index 0000000..de59146 --- /dev/null +++ b/django-compressor/AUTHORS @@ -0,0 +1,94 @@ +Christian Metts +Carl Meyer +Jannis Leidel +Mathieu Pillard + + +Django Compressor's filters started life as the filters from Andreas Pelme's +django-compress. + +Contributors: + +Aaron Godfrey +Adam "Cezar" Jenkins +Adrian Holovaty +Alen Mujezinovic +Alex Kessinger +Andreas Pelme +Antti Hirvonen +Apostolos Bessas +Ashley Camba Garrido +Atamert Ölçgen +Aymeric Augustin +Bartek Ciszkowski +Ben Firshman +Ben Spaulding +Benjamin Gilbert +Benjamin Wohlwend +Bojan Mihelac +Boris Shemigon +Brad Whittington +Bruno Renié +Cassus Adam Banko +Chris Adams +Chris Streeter +Clay McClure +David Medina +David Ziegler +Eugene Mirotin +Fenn Bailey +Francisco Souza +Gert Van Gool +Greg McGuire +Harro van der Klauw +Isaac Bythewood +Iván Raskovsky +Jaap Roes +James Roe +Jason Davies +Jens Diemer +Jeremy Dunck +Jervis Whitley +John-Scott Atlakson +Jonas von Poser +Jonathan Lukens +Julian Scheid +Julien Phalip +Justin Lilly +Lucas Tan +Luis Nell +Lukas Lehner +Łukasz Balcerzak +Łukasz Langa +Maciek Szczesniak +Maor Ben-Dayan +Mark Lavin +Marsel Mavletkulov +Matt Schick +Matthew Tretter +Mehmet S. Catalbas +Michael van de Waeter +Mike Yumatov +Nicolas Charlot +Niran Babalola +Paul McMillan +Petar Radosevic +Peter Bengtsson +Peter Lundberg +Philipp Bosch +Philipp Wollermann +Rich Leland +Sam Dornan +Saul Shanabrook +Selwin Ong +Shabda Raaj +Stefano Brentegani +Sébastien Piquemal +Thom Linton +Thomas Schreiber +Tino de Bruijn +Ulrich Petri +Ulysses V +Vladislav Poluhin +wesleyb +Wilson Júnior diff --git a/django-compressor/LICENSE b/django-compressor/LICENSE new file mode 100644 index 0000000..d9432d5 --- /dev/null +++ b/django-compressor/LICENSE @@ -0,0 +1,128 @@ +django_compressor +----------------- +Copyright (c) 2009-2014 Django Compressor authors (see AUTHORS file) + +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. + + +django_compressor contains code from Andreas Pelme's django-compress +-------------------------------------------------------------------- +Copyright (c) 2008 Andreas Pelme + +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. + + +rjsmin.py (License-information from the file) +--------------------------------------------- +Copyright 2006, 2007, 2008, 2009, 2010, 2011 +André Malo or his licensors, as applicable + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +utils.cache.cached_property extracted from Celery +------------------------------------------- +Copyright (c) 2009-2011, Ask Solem and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +Neither the name of Ask Solem nor the names of its contributors may be used +to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +utils.FormattableString +----------------------- +Copyright (c) 2010 by Florent Xicluna. + +Some rights reserved. + +Redistribution and use in source and binary forms of the software as well +as documentation, with or without modification, are permitted provided +that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +* The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/django-compressor/MANIFEST.in b/django-compressor/MANIFEST.in new file mode 100644 index 0000000..a470974 --- /dev/null +++ b/django-compressor/MANIFEST.in @@ -0,0 +1,10 @@ +include AUTHORS +include README.rst +include LICENSE +include Makefile +include tox.ini +recursive-include docs * +recursive-include requirements * +recursive-include compressor/templates/compressor *.html +recursive-include compressor/tests/media *.js *.css *.png *.coffee +recursive-include compressor/tests/test_templates *.html diff --git a/django-compressor/Makefile b/django-compressor/Makefile new file mode 100644 index 0000000..0c4c65f --- /dev/null +++ b/django-compressor/Makefile @@ -0,0 +1,11 @@ +testenv: + pip install -e . + pip install -r requirements/tests.txt + pip install Django + +test: + flake8 compressor --ignore=E501,E128,E701,E261,E301,E126,E127,E131 + coverage run --branch --source=compressor `which django-admin.py` test --settings=compressor.test_settings compressor + coverage report --omit=compressor/test*,compressor/filters/jsmin/rjsmin*,compressor/filters/cssmin/cssmin*,compressor/utils/stringformat* + +.PHONY: test diff --git a/django-compressor/PKG-INFO b/django-compressor/PKG-INFO new file mode 100644 index 0000000..6b41def --- /dev/null +++ b/django-compressor/PKG-INFO @@ -0,0 +1,104 @@ +Metadata-Version: 1.1 +Name: django_compressor +Version: 1.4 +Summary: Compresses linked and inline JavaScript or CSS into single cached files. +Home-page: http://django-compressor.readthedocs.org/en/latest/ +Author: Jannis Leidel +Author-email: jannis@leidel.info +License: MIT +Description: Django Compressor + ================= + + .. image:: https://coveralls.io/repos/django-compressor/django-compressor/badge.png?branch=develop + :target: https://coveralls.io/r/django-compressor/django-compressor?branch=develop + + .. image:: https://pypip.in/v/django_compressor/badge.png + :target: https://pypi.python.org/pypi/django_compressor + + .. image:: https://pypip.in/d/django_compressor/badge.png + :target: https://pypi.python.org/pypi/django_compressor + + .. image:: https://secure.travis-ci.org/django-compressor/django-compressor.png?branch=develop + :alt: Build Status + :target: http://travis-ci.org/django-compressor/django-compressor + + Django Compressor combines and compresses linked and inline Javascript + or CSS in a Django template into cacheable static files by using the + ``compress`` template tag. + + HTML in between ``{% compress js/css %}`` and ``{% endcompress %}`` is + parsed and searched for CSS or JS. These styles and scripts are subsequently + processed with optional, configurable compilers and filters. + + The default filter for CSS rewrites paths to static files to be absolute + and adds a cache busting timestamp. For Javascript the default filter + compresses it using ``jsmin``. + + As the final result the template tag outputs a `` \ No newline at end of file diff --git a/django-compressor/compressor/templates/compressor/js_inline.html b/django-compressor/compressor/templates/compressor/js_inline.html new file mode 100644 index 0000000..403bec5 --- /dev/null +++ b/django-compressor/compressor/templates/compressor/js_inline.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/django-compressor/compressor/templatetags/__init__.py b/django-compressor/compressor/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django-compressor/compressor/templatetags/compress.py b/django-compressor/compressor/templatetags/compress.py new file mode 100644 index 0000000..a45f454 --- /dev/null +++ b/django-compressor/compressor/templatetags/compress.py @@ -0,0 +1,214 @@ +from django import template +from django.core.exceptions import ImproperlyConfigured +from django.utils import six + +from compressor.cache import (cache_get, cache_set, get_offline_hexdigest, + get_offline_manifest, get_templatetag_cachekey) +from compressor.conf import settings +from compressor.exceptions import OfflineGenerationError +from compressor.utils import get_class + +register = template.Library() + +OUTPUT_FILE = 'file' +OUTPUT_INLINE = 'inline' +OUTPUT_MODES = (OUTPUT_FILE, OUTPUT_INLINE) + + +class CompressorMixin(object): + + def get_original_content(self, context): + raise NotImplementedError + + @property + def compressors(self): + return { + 'js': settings.COMPRESS_JS_COMPRESSOR, + 'css': settings.COMPRESS_CSS_COMPRESSOR, + } + + def compressor_cls(self, kind, *args, **kwargs): + if kind not in self.compressors.keys(): + raise template.TemplateSyntaxError( + "The compress tag's argument must be 'js' or 'css'.") + return get_class(self.compressors.get(kind), + exception=ImproperlyConfigured)(*args, **kwargs) + + def get_compressor(self, context, kind): + return self.compressor_cls(kind, + content=self.get_original_content(context), context=context) + + def debug_mode(self, context): + if settings.COMPRESS_DEBUG_TOGGLE: + # Only check for the debug parameter + # if a RequestContext was used + request = context.get('request', None) + if request is not None: + return settings.COMPRESS_DEBUG_TOGGLE in request.GET + + def is_offline_compression_enabled(self, forced): + """ + Check if offline compression is enabled or forced + + Defaults to just checking the settings and forced argument, + but can be overridden to completely disable compression for + a subclass, for instance. + """ + return (settings.COMPRESS_ENABLED and + settings.COMPRESS_OFFLINE) or forced + + def render_offline(self, context, forced): + """ + If enabled and in offline mode, and not forced check the offline cache + and return the result if given + """ + if self.is_offline_compression_enabled(forced) and not forced: + key = get_offline_hexdigest(self.get_original_content(context)) + offline_manifest = get_offline_manifest() + if key in offline_manifest: + return offline_manifest[key] + else: + raise OfflineGenerationError('You have offline compression ' + 'enabled but key "%s" is missing from offline manifest. ' + 'You may need to run "python manage.py compress".' % key) + + def render_cached(self, compressor, kind, mode, forced=False): + """ + If enabled checks the cache for the given compressor's cache key + and return a tuple of cache key and output + """ + if settings.COMPRESS_ENABLED and not forced: + cache_key = get_templatetag_cachekey(compressor, mode, kind) + cache_content = cache_get(cache_key) + return cache_key, cache_content + return None, None + + def render_compressed(self, context, kind, mode, forced=False): + + # See if it has been rendered offline + cached_offline = self.render_offline(context, forced=forced) + if cached_offline: + return cached_offline + + # Take a shortcut if we really don't have anything to do + if ((not settings.COMPRESS_ENABLED and + not settings.COMPRESS_PRECOMPILERS) and not forced): + return self.get_original_content(context) + + context['compressed'] = {'name': getattr(self, 'name', None)} + compressor = self.get_compressor(context, kind) + + # Prepare the actual compressor and check cache + cache_key, cache_content = self.render_cached(compressor, kind, mode, forced=forced) + if cache_content is not None: + return cache_content + + # call compressor output method and handle exceptions + try: + rendered_output = self.render_output(compressor, mode, forced=forced) + if cache_key: + cache_set(cache_key, rendered_output) + assert isinstance(rendered_output, six.string_types) + return rendered_output + except Exception: + if settings.DEBUG or forced: + raise + + # Or don't do anything in production + return self.get_original_content(context) + + def render_output(self, compressor, mode, forced=False): + return compressor.output(mode, forced=forced) + + +class CompressorNode(CompressorMixin, template.Node): + + def __init__(self, nodelist, kind=None, mode=OUTPUT_FILE, name=None): + self.nodelist = nodelist + self.kind = kind + self.mode = mode + self.name = name + + def get_original_content(self, context): + return self.nodelist.render(context) + + def debug_mode(self, context): + if settings.COMPRESS_DEBUG_TOGGLE: + # Only check for the debug parameter + # if a RequestContext was used + request = context.get('request', None) + if request is not None: + return settings.COMPRESS_DEBUG_TOGGLE in request.GET + + def render(self, context, forced=False): + + # Check if in debug mode + if self.debug_mode(context): + return self.get_original_content(context) + + return self.render_compressed(context, self.kind, self.mode, forced=forced) + + +@register.tag +def compress(parser, token): + """ + Compresses linked and inline javascript or CSS into a single cached file. + + Syntax:: + + {% compress %} + + {% endcompress %} + + Examples:: + + {% compress css %} + + + + {% endcompress %} + + Which would be rendered something like:: + + + + or:: + + {% compress js %} + + + {% endcompress %} + + Which would be rendered something like:: + + + + Linked files must be on your COMPRESS_URL (which defaults to STATIC_URL). + If DEBUG is true off-site files will throw exceptions. If DEBUG is false + they will be silently stripped. + """ + + nodelist = parser.parse(('endcompress',)) + parser.delete_first_token() + + args = token.split_contents() + + if not len(args) in (2, 3, 4): + raise template.TemplateSyntaxError( + "%r tag requires either one, two or three arguments." % args[0]) + + kind = args[1] + + if len(args) >= 3: + mode = args[2] + if mode not in OUTPUT_MODES: + raise template.TemplateSyntaxError( + "%r's second argument must be '%s' or '%s'." % + (args[0], OUTPUT_FILE, OUTPUT_INLINE)) + else: + mode = OUTPUT_FILE + if len(args) == 4: + name = args[3] + else: + name = None + return CompressorNode(nodelist, kind, mode, name) diff --git a/django-compressor/compressor/test_settings.py b/django-compressor/compressor/test_settings.py new file mode 100644 index 0000000..a5abf92 --- /dev/null +++ b/django-compressor/compressor/test_settings.py @@ -0,0 +1,40 @@ +import os +import django + +TEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'tests') + +COMPRESS_CACHE_BACKEND = 'locmem://' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } +} + +INSTALLED_APPS = [ + 'compressor', + 'coffin', + 'jingo', +] + +STATIC_URL = '/static/' + + +STATIC_ROOT = os.path.join(TEST_DIR, 'static') + +TEMPLATE_DIRS = ( + # Specifically choose a name that will not be considered + # by app_directories loader, to make sure each test uses + # a specific template without considering the others. + os.path.join(TEST_DIR, 'test_templates'), +) + +if django.VERSION[:2] < (1, 6): + TEST_RUNNER = 'discover_runner.DiscoverRunner' + +SECRET_KEY = "iufoj=mibkpdz*%bob952x(%49rqgv8gg45k36kjcg76&-y5=!" + +PASSWORD_HASHERS = ( + 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', +) diff --git a/django-compressor/compressor/tests/__init__.py b/django-compressor/compressor/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django-compressor/compressor/tests/precompiler.py b/django-compressor/compressor/tests/precompiler.py new file mode 100644 index 0000000..059a322 --- /dev/null +++ b/django-compressor/compressor/tests/precompiler.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +from __future__ import with_statement +import optparse +import sys + + +def main(): + p = optparse.OptionParser() + p.add_option('-f', '--file', action="store", + type="string", dest="filename", + help="File to read from, defaults to stdin", default=None) + p.add_option('-o', '--output', action="store", + type="string", dest="outfile", + help="File to write to, defaults to stdout", default=None) + + options, arguments = p.parse_args() + + if options.filename: + f = open(options.filename) + content = f.read() + f.close() + else: + content = sys.stdin.read() + + content = content.replace('background:', 'color:') + + if options.outfile: + with open(options.outfile, 'w') as f: + f.write(content) + else: + print(content) + + +if __name__ == '__main__': + main() diff --git a/django-compressor/compressor/tests/static/css/datauri.css b/django-compressor/compressor/tests/static/css/datauri.css new file mode 100644 index 0000000..756276b --- /dev/null +++ b/django-compressor/compressor/tests/static/css/datauri.css @@ -0,0 +1,4 @@ +.add { background-image: url("../img/add.png"); } +.add-with-hash { background-image: url("../img/add.png#add"); } +.python { background-image: url("../img/python.png"); } +.datauri { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0 vr4MkhoXe0rZigAAAABJRU5ErkJggg=="); } diff --git a/django-compressor/compressor/tests/static/css/nonasc.css b/django-compressor/compressor/tests/static/css/nonasc.css new file mode 100644 index 0000000..43159ab --- /dev/null +++ b/django-compressor/compressor/tests/static/css/nonasc.css @@ -0,0 +1 @@ +.byline:before { content: " — "; } \ No newline at end of file diff --git a/django-compressor/compressor/tests/static/css/one.css b/django-compressor/compressor/tests/static/css/one.css new file mode 100644 index 0000000..769b83f --- /dev/null +++ b/django-compressor/compressor/tests/static/css/one.css @@ -0,0 +1 @@ +body { background:#990; } \ No newline at end of file diff --git a/django-compressor/compressor/tests/static/css/two.css b/django-compressor/compressor/tests/static/css/two.css new file mode 100644 index 0000000..b73f594 --- /dev/null +++ b/django-compressor/compressor/tests/static/css/two.css @@ -0,0 +1 @@ +body { color:#fff; } \ No newline at end of file diff --git a/django-compressor/compressor/tests/static/css/url/2/url2.css b/django-compressor/compressor/tests/static/css/url/2/url2.css new file mode 100644 index 0000000..45686ca --- /dev/null +++ b/django-compressor/compressor/tests/static/css/url/2/url2.css @@ -0,0 +1,5 @@ +p { background: url('../../../img/add.png'); } +p { background: url(../../../img/add.png); } +p { background: url( ../../../img/add.png ); } +p { background: url( '../../../img/add.png' ); } +p { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../../../img/add.png'); } diff --git a/django-compressor/compressor/tests/static/css/url/nonasc.css b/django-compressor/compressor/tests/static/css/url/nonasc.css new file mode 100644 index 0000000..2afa456 --- /dev/null +++ b/django-compressor/compressor/tests/static/css/url/nonasc.css @@ -0,0 +1,2 @@ +p { background: url( '../../images/test.png' ); } +.byline:before { content: " — "; } \ No newline at end of file diff --git a/django-compressor/compressor/tests/static/css/url/test.css b/django-compressor/compressor/tests/static/css/url/test.css new file mode 100644 index 0000000..0d4a22b --- /dev/null +++ b/django-compressor/compressor/tests/static/css/url/test.css @@ -0,0 +1 @@ +p { background: url('/static/images/image.gif') } \ No newline at end of file diff --git a/django-compressor/compressor/tests/static/css/url/url1.css b/django-compressor/compressor/tests/static/css/url/url1.css new file mode 100644 index 0000000..609c111 --- /dev/null +++ b/django-compressor/compressor/tests/static/css/url/url1.css @@ -0,0 +1,5 @@ +p { background: url('../../img/python.png'); } +p { background: url(../../img/python.png); } +p { background: url( ../../img/python.png ); } +p { background: url( '../../img/python.png' ); } +p { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../../img/python.png'); } diff --git a/django-compressor/compressor/tests/static/img/add.png b/django-compressor/compressor/tests/static/img/add.png new file mode 100644 index 0000000000000000000000000000000000000000..6332fefea4be19eeadf211b0b202b272e8564898 GIT binary patch literal 733 zcmV<30wVp1P)9VHk(~TedF+gQSL8D5xnVSSWAVY>J9b+m>@{iq7_KE}go~11+5s4;8hc+i0Xa zI1j@EX5!S+Me6HNqKzU5YQwL;-W5$p%ZMKMeR<%zp69-~?<4?8|C8S?bklXr4v&Ov zb&06v2|-x?qB`90yn>Qi%Sh2^G4n)$ZdyvTPf9}1)_buUT7>`e2G&2VU@~Bb(o+Mz zi4)>IxlSY${Dj4k={-9RzU^W5g9|2V5RZ2ZulL9s2xQbZ@r6eP9Ra5u(s|C0Nj#&4>wTSkb?%#=9?@ z^oxDy-O@tyN{L@by(WWvQ3%CyEu8x{+#Jb4-h&K9Owi)2pgg+heWDyked|3R$$kL@A z#sp1v-r+=G4B8D6DqsDH0@7OztA7aT9qc1Py{()w`m``?Y0&gi2=ROcc-9+nU^I6< zT=e_Y=vSnG@?3Ue{BW5ONFttcE!R-R_W4O01|0-|K-YNXLo2`4Qv z`r1LxR6#yf3FB%T95gJnaKKivA~Z}S9A(ZxEDK}O3T04USJ P00000NkvXXu0mjf^IS-S literal 0 HcmV?d00001 diff --git a/django-compressor/compressor/tests/static/img/python.png b/django-compressor/compressor/tests/static/img/python.png new file mode 100644 index 0000000000000000000000000000000000000000..738f6ed41f499d1f57fd4db356dc1d64769bf725 GIT binary patch literal 11155 zcmdUVWn5IzyDmd_NQaal2t$J+ol*jVbf+L4(%q$k_$O4P6i{-=p&38~1f+zaLz)mfY@00XQM~#e_i5LqDi%dgZMIQ?b+Z22sAjAis zmi_Vf!5>^-B@F{Y@Cqe#NCdx$ywpv7v9O2_{=Fee<>J}kB;6}jlUK?0TYDc*cSnbpw(L({`Zxx7`ndYC zYn!l(3JZ%T_?Hi3VX(%->L@DUDIuIY)C`>CCbyk_-T<}6^)I*C_H@QqdIY1*i>j@X(n9|*#px% zxcYZq+^^6(T%6O_J8ke+2j`4c&-7?jhPr8u#C44H+_G1AD~D4&%N_=2YL#@cHE7OK zpyP7k(<5fmZeax#@G6*@+Q+A)qM{xhK7TKgZV~f=$6MSsIlVkN0Et% zT?Hs9z6@e$he33Oa=hAq-Vxh^)RKw0WgQ9I&5eHcPSa z2n^f&Ex<7Up(}Uv?o?(o_s;+zEAc6Yn)s0qzWkEcM?pE^Ycjuc%sa<=g3WEPM~DsL zn}#<-$ug0df`6)CExWk39F2J*f0a+R3HXsLNCZo>w(frpJPX)YT+hAxu2mj9E-srs zraMl*E0(s{ZkRHR1#PyJa5f2b1tA06p-rs&qqFs6jc?mAIMdd&0JN=s3MJZ%5=r(#P zs14&N0d&CYtM&Z9aW6V<`Ij+I!6=n~qpBjRrm0uy?4AV7#U~|%6MPumooWuE&J=uj zaN;Ypa-!9&9F!I57(sTB`<~WINrn{1`g&6L@L+$hYxAx-PBH=C6l;zcYD}nnodc#+Y2{I zi)?l+dgfhM8&mAb$=Qc~xX>~+%&)USwNpvbLsPiGFb*CRM-hh(E9dHv#6&8h=Te)R zJEsx_<_x?#?5&2uM)ub!Rb9H{$=q*;hKBrha3Y(U9{)#^d$W1Z3^_N)vcA49Ge9bJ z>cHLCl!9D(NQ!aZvqDBXe(YOJEuk~o_oj1J3Q0Y#XBl|JmKW9zgtxdd?NRvkI}-KG zFyoPXv7eKhTe*9SgL7y z8+TP=+-z+jMfypQ6!kRdKpGd($6~+M;!-~1I3+eSTEX~ouER#{-+#v(3;6higzmLQ zI!gYBUM9SLCj%e2xzS)lUYh(;m)UlgdScT|YU%0uWDb7s^7oT(CS>t-a>FMueeX+# zIZY6vEWcWwCSr(9SY4e3+B4^f-l(Yj`D7tTU}&gUg*3l3FhsX*3u`dcr1#mpkk2`s9_OmVhs%LJ~HlzRG={VIOP~j&< zF9x=8(crl#Cv!X~h815%PqH)-N3_BU8~ky!Bb0Rj-Y|57Hp4u}^57Wvt9aI^NZMba z0{bguPoSKIenRj2v780vg17%XV37-L92Ov^P&WF@klFfoVc=DfH;xli7K^p3TwrYe z2zF*jwmkxbi18>K06-^jV^O|XI&LGa!L`90WaP~M+7nK z3KxW(l2EzSwY9YZlZMV@SyQ6F%&;qL9dpK#BS<`Ti_C(T3jr8Z#FH1Q?;OFG;R8ka zhw2MEa3>*~4RCxuu~e~&Y()0JKp5&y;^0qeCX@^#G0CiNotu3EoNXwOd2tm)YxV3j z(lIczeo`Bpv;Zo(D#q5o1l~VNkzNR&oQO631~ek_-)t!yDLFxC8zLc5 zzA5u31+Ya2zcit^Nxv}OJ*_AGdSw$c9`klgjEsz>d8(Gk>ekk|>WH)mHd3*8=+W}S z&V}WA4{SOyBldbTGH^Hz=c-m1#Z;n3{BAGhi^pH~)sU*{r46iI92MyQx55{c@`-C} zT^TB)l4KD$G1mp|3m;X->n1xGC~LJiwfC;AIXZvL{I-aP>c{V1 zsz32=_SwbP=?G;_*$}kfQu?w>C;6mTtb{}AvcWhtZTPESx6$O$RUA^tuIQ$CpebjY zj>zP9@9pN|?fd%qEFCPQ^~T(-i{wF1-!(k z`-`a-g7lhU9)mW9|81MIRw-f*z@rpc2!>=bva$6}O=Wvpj*pMOEx7kIxzNVPNB>*7 zddORr$75!Ovs!Ido+LaeDJku}eKoJ2-j4~pJVndC`d+$yRy+9y=s>6(g{q#Cv+4fe zzBY)>aizk%$z#D)G;*g7J*y6D5;B(qi%Zkirt6P`<`RTYG*I4)_jyJR>5vg*t>+}> zh?)u8FgKZwhBh=NN?H!jt+}>#A$C-pth6+qseA|R`Vn7MPgq2R>LS5Do6Vm_po$&Z zwe!Ht#3Ztimw}l%MK=3eX|hifi`juN%D}MRbHASkxgYJ*^xCdkQn#--cQ6dGBXtd!;Q{7Z#=SG+NOv)u}7RIr5kUmrY_hhczpV+bhX^-HpX+upA9rm9`+ z_gv)u?CrH@e)9SU?+lr^1P8NSV0x2V{SiC`-S!fa}4 zs!R>|7`fxmok4%BoTca*I5a$L@8KbsX`^2@AuKCKq4fB{H_$u1?(f( z{O8ZAMfyac<>uziWG;H)`;*M^DBAUp!^N9joe@QNNv^s#>Q8#`fgNli2%_RmL7|3Y~F($YK0$_>JM_V5$!&47OT*GWU= zkHrNBd_>x$f>{H74x%|&C_6j5F7J*$+Siw-yC|(lS~BjfSIvGeJD>x1*_P_fDkL~8 zPMnO##i3BB&F#&?h6X;7Ns|TnC1xLU^;oB5ydvTrI`}RL2}yQd9(}MUP>wXmrV@p6 zRIux}g;XvLUK(O>s2AyXZGansg8`WN)m7{M@VAW8(!Lk9@2ulw2Eo3>FT1T*P6pN! zOiUmHU*KgMk+j0NMDq!-Pr^tmZD$Axz8BgvN^2Mk&bBFIoQF$favsVGG9TG0$1l9Z z#l^+o!3+HHHR8(m8mL!lr8mtK5fO_fnG#`< zde!&H#d7Fl97^-AhZf4@cn=0!u zrM=~{%x9PM7wAUHcOVbEbTcco@grS@u#i?i@+r^bETDAvnxZw*xj+av*`(jOx(fY< zA|G9PMuVsJmSyYl5Be0i-4@%-i=7gK7mIIG&j2h%Et|xDl;CBK>o8Be-1_7|6hZ^QY0%k>W2r15CN)sSO|r zG<_?7tDQhyp}Y0UEYRwiP#cAKAeV+xwaJgw)vqJO2b{@0g# z$t%Qi-#lSPKl8alO$V|;w88(?i9>~lwH%(=b6%R*8zm#dj5bqcpjpdP)%FF8a@-l3 zO;)5f$e|-#&STis{l);TbgpI0LuqN|i}Nq{FBTYTntRh|qu*y3f6|S`&a6JoGbVf~ z+Zf9b#UZ!ftcsnf97yC9I_ulpz|^Vjnb&qJ-g(LdNss8{dH;vxc;5VcL#)mnp0ZK6?0&wiA0N{x)J z>Q3Y+Q!}odH?gd-6`;gzrznt(#X8vGCU#V6ZQ7-UE%~-0&*xDM;*{n^L(a#iw6E}? zuF~a@fdZYXAoHCemwPi`sMdZh(WFXx%t58}WYmYh^?9L{DP(ER4Pk{Gf=To#2fsR< zMI6wZAhfwdNTNX~iN)IGC{E#!+i0F^kWQuV(0;2tJJZnCPOgoatFCZoMq2w5nl!MM zf%FE;VtG}YUn4!Xl2?pgg8PV5%7TZfMqHbkFJwvKWT5uz_{)L_{z7+FE(;1FGg4Do zsoGaT=nqToKCMcLrax(>X%ny^19B`|0ih#J4rQk=_AF<;$V}u?Cyzs9t*%Xb&ymuj z*0A}(q5?E`-yKR$mEdQe>7Kqxb{_T|i${K8QYyY5U(WhD{9I-dJCl2*!q~qkQFjDlMf{X*VdnFNCEjvo>D@MGW zpG$Ss=W&5;|JQ5tBzxWa4lWxCeI%r;tzvoMz8cg<_UPu) z8}8TW!D5>$c{~_m|MK`)KcNEYj?(*zm0Vq}JKHYxOt%%!NR*8z zK*{-0H#Q`HiCRfT>Gkt-c1T5SMJ~MGyNPcE?vWRXheaTNbW{HZz5uhhrq@H*>1N&= zUBI&XsqIa*OP1V1A1~WMLjxxI4Yho75E)k=7KLL(0jUrWmdJbL>MNCHKF_&7QPeur zncB)0f|;vrSh=#Un7B9genJ}|ie+K#<31kDJ#z`b8*i#MTc0`1Bn_G06LwYn98V?l(+Liy>2KV`-1>fAPS}9mjZr$ahJ2L@%t8nnEBKM zm$iW)j^4ZaR_82~Jx91mbWeH|+?7_5#Q*`=w<-55-1cJWdHzzE)3J5o6N=ORC?+R8IT=}^ZmrK(L9fB^a4pJ zcQev*xVhg}(uN^UWVbn=un)5Mj%D6$De2v|L_qK^+m1RIo&l6w!0y@q#~wn&W>e90%rI>trS>cphP^w%6^PSM@YwmOM$xBW1rW#EqezS>??OK=FYUQ-aQ$p-?# z%qZrLEdd5RBD37ygtMD?tc2Ud#KO+B3F>!|T%xNReyz`gaAx)4f5q4?Te1VsWcP+b zP*58SKO)-rjp7x>_+ixg9y}apE)eJ2En(oX>a7i}Kd=NN_Ewj)L|!IR)3)s(os8!} z`z!DwkDkMBww~M&|Ct$mg32(qz~f8Ao7zpO$XGfD+_(E$2)mJ*ziaC%%k~r^5a5OS z=i26%iM!b${DBI-q$W@9ev6=PJdIgD$Z2c;hMkT)v_(?;d)+IH;tD2+R=izd&(K7- z5Q$?k)C9vM{06qg;E@jJ3ZUMTEnh-0 z7+|Zsh(hDF!s6B1*hzFkOBBUKMgN)?v&qAoD@ii!`#As2B>F{~lGmm_qaF|1 z{eZWQuZv#pkYnIQ+jI=AA%eI}4$kf{zaQAjF-7j`T*F3z35IVdxT=KI(5NOga;s<; z%DZ|KRDc4sz~_GqP|>%M(uE4N9QCaSvig-OR?=`Q}IiswKD>u5B~)1baHyi zj#ZbCkc(qNQvNH zx}BztxAiVn*Kf4ct3NB(mGmtLR#~wl4^|9*vhL-)^?sR9AC|j3V)pZ%gINcKK7L)X zQ5lgg`)`M?>ltrBhM@kns|554xhdy{y%3!#uq|HS;oTvg1`at)BwoUi2aropUIixq z@VWsxn@h0wpr(F6qHk&&P3$t+3&YmWKJN9GBQ?8s!NIMezj^#Lu=eE<=h&wrRIr<^ zCDN}Tw_K@!QrVUF0gz1{sBrIMr>_NpaP;zz6n&ZBCzcW{yId`>><2bYvQl`^sz@%g zssV3B)S>jkm6SW{S`G+KY1~qw)Ulgfc4m1h!8KS*f4)Dbf`9$4|NSgeqhvhiM8_RW z{-?Ezl9I%v0frrj4`sj?#hAqtFIt@vvMYupLsP>QdDYp8@4%vQzgo@WVM((=Dn|Y8 zPPiC;+X0{@_JqKZA~oh21-BMohLT(!MxdnBg|vc7dKiF0cFsiHM9M~~dkYVy<4N1F zfj5NzGPkP5gSxt7MZ&}1^kJ*ZV;h<5Qj@vtv*1)v?LyQ`#-ldulga}Hm=2t;nb_Dc z0^`o&oD^msi_gimG}F0gh+l?*v{3OfjbldstomY;9_H3&8D9?_F@3m$_192W9tTV~ zD?{uyjpI^^ME$FC_-2Q!H#m)rCs63AU=Dn|1oarOd!_DpkZz8JFlPP(W#g2hF#KQ1 zNliHdPdYCrAMSKV008Op>%FnrH5S-N&{Gr04^-f2KHWc)3Hy>)7r}w7*ZIren8;&MRmC4z?$sRwM5WBBG{JH-E?^~&h!!l@TEEhBB4M+Rh@7b=h`%(<&BQ|=Y0^C&70UFZF79n%8TxV7h54L7t zl(|;9k)5bxhW~NjuufZXv-z$5_t>qiEt9S{P*6aRZlk3(3jJ)|+@5aQCG+G4a@NV~ zfOEwqC1D|opu8%A9xk{@RS#ANh~T1#;1`XN9o@@}s-L>@gtW;I!H zYFRpej0cZ|!A#7;!IX~p24FGPH%D15R-7v1;s8LnJeAZ;kF~4r>)%^_`MiT!_^A)Y zT#}l6puEn_YR~#x&@mZLEAZ=Zg}tPGMIm~6ZH3OBmA$(^NuGe&W>{xAm7cVpUZ^8$ z^t{tuIO=TU;X&@}>zn4t_~9u&0OK%w^X|sS6DYqwE)wAQR>~Ck4VpKc;Q!!aYKj45 z>^F!qB&q_FJ@f*AeGaDnEXm{IWX+YzT7>uN;J+j(8uB-GunubX2q5f5rUvJG75 z4yLsCW@;3_1TzVM>X1O+llKZ2-RgX*dUn=SvdNKn;yj&fk9}}~ZK3QkFa~^apppmo zAwgGuFP(W90R60U@6*sxvIGTKPde8>*e2}F&?WgP)65Wu7C!V&&tp@nso+@w$?!8Q za=EKq8^}PU@)XQFXLyxH!49MZ%F;PA^u~zh{X7VUCnVb|G?eA}^XFYCZS?k1XUscX z=zB()zwJSlO}~-V_PutpRnP$-?VVC5O{8Dz#daB0ol7IYqMmAN_s|7`hDZ6vmt{n5 zd7@j}uF=)FZYX7HtLLNKD}UOOVQA-TlOY0g&kPBbDP`49tT^gv3e;a06u)hp5f?!tnqxQo$>h$Y_Yd!!E>Ra;(`;-kG z5N$sx5?bCW$+HUJpY7%TtOFnq;?hz(fB%Q+K|$WP%mvF`X|&aII04IN%c;wA=lo|K z^71Kp?XcL43%{Ho?Ox3lpozlRMpKISOPA=@4`(rza&m-ohqg9auuBxTD$upnRW49e zt#527e^{;H)%-?17Xu(FN&g)N1H;`5O{JDsQe>)n`MjFQPQK*xXWtEod+ezGr`?7l zrWi4`dVD^KM*xcRg7TEi;f-p~#dZW{m#b#RhQULu{N<9xjVEHQ<1LTP?v1ZPuyx(O z)8~)5xu?4sQa-DR8$uYD`k7`I-4h(u%rzs5VfJf7k2YfcuW+4zK>eSOeb9lLX9~eP z)x9#-n9Gj|1y-o`48Sv({Ko|tb*^i7`dDlm6f`a3z?U4|uTjn6`A%cD2HHFJkg?ytyVyh~-vOPTAG9!ry7V7c+>fCh(1f+Hk)@?!8Z?fwQQ;3^ zo&hN4Qx$~yYyFWsTC%Rho%uJq%fHohvE|wmp-CfAIj5~+Yu~4tE}1~gEOcU}o9|DB zMzWJFQjUTkp!snmI zUqF*PkB(|KP&+-Gg}k8mqX|Z^wnwx6bAHS6y@pG7PpMT0_wR%+eEpDEh)B!ey4$8$ zc1yxMV4b{Qh ziUs%fkiuHl<1=%$o`pwx^xNoWPhBIytd#E(Ds>A$GW|aVhSa_Q2z=pv#>~p%UVxkH zsq^D{)Wdja^6w;S9 zHXR-wGMFi=plFqi#@uE*F1WttLWhG%Z!dliUBH93wtfc>t&FqL1&*uy{k2x(g|HK% zqf$-kj?<`Jp@`HPF846$O14)JP4Q!fEm6W=Pu?fRX$&V1+~P-ETfg$ypHN+zxZRwq zljwvEZ7PmQVrm^q1tx02N zwy`Fxkl~of5w)I6d0`Z7Htr2wdkR842O(8*TNeAp?ufnN(~}~0?2pSX9v;5~I`!T| z2P8w!yna-0a@zH27N5vtSKg2Y9v3ZO>l;G`tcJHw*T#o#=eDcm7v%e`pxYmdUw$*- z`({+u4j-&ecVqzy=fT^%y6uhcHErX`vt*Qu$snB>HZXbx9~xx)m+E*swOy99z)+P>B% zknhO+hM>B~ouAu`x!7M{5`t6(EKaEa!OfKTtqL$&dZZh$FszWhtCbfcW;gqYS^iOs zL(_!sWej4oq>(%`MyUWxBR$mko^>kdet`_gEWT4G(~*W44}fl`7F44OfXDI>J~u&X z7H}itbxz~8MKE-mgH2EAfoQ%4k1OF*XRXRflYBI@oe)=LW5b?1x5 znf6DO0iyqTW$B40Xo>V*?W1xKMfu*C)tmJU4nCc22j21BJ1Wqv1fLF~jD0n8C3JVF zl5G3TsutRb#ea*_leM+~A=TwCF>f01dBBdHrGaX2DjszP z9HjbqasZqp|1VBetD^Y-!J;Z7LB>o_EjRYYJ;Fl7lZoVI0ADB!?D?~8Vq8gf%p`uN zuSg=`1i*kM5?2XxQ`Y@$Z|_97L>|E)k=3>_j^7DqQKZ&@>taJ{it|f?rU{o1eq^=G zH@LJ$llW1~g|V`#WpY9$|4Y~o`X6g(`3`wZ*8>?Me7+LTf=#)qYtGiL_v5aoVRvVT zKDS?9{RWNlKc1wf*IW$ctx6fzOD?mgj6S$OHr&q9d_i#!7V{kiP+qLPVWbwIqi(}c zbCJKIwMSj9qHKg}YLlC7FrJ^^lv2P8TbNsJ-fc)tO#H^iP-C~sT#4J2=J~BgCP3m+Qyf&L&|26GI@^z zBpadt^~i(NRPT7J!e=|EMvk7y!o%^pVYgS(y@hHws%GDJwgLYuyW-%Dg~KN>7F2Om z)SnYsxt>mAlH$wQ3kGkRXEUNbAnM{4)9`E7>r=?iD3D1f!mp-qRhXd6ef*FTgRb6j zyu!{yg>&&cQt=nfBi=dJ7NjPCjR#yH?=;Rk_t^)p9^(my1eN4Fu)2DfhkVY7 z-z(n`n{N$z9=Ml28w!OZV>c{_2K#-acx0lPR%ofH&^SEpYD3`?9MRh0aJl_#m`9_~ zJiCvCE!>zPS+Ig~)kXPT5liE196*-?wfxho=-18&pScanKhd$@t-^7yGW8E;dG1tN z3`jhhJiT!-QN{-Ep}gP-jQdxWbq19ylM6a+l{a=eUej z`3{TZ`gC4IM%eEE_=(KQlUUwYMuf3~aQhhia&~#*aM>sA#yHJ8zR>Z|&Hy%GM4*wx zWT3PB%T<0(Ieif>S)R17_^UCFtLorKfdZhQ-T6lhA~eaZ!h1k{rz5v`lHVih*G3>v z5yjp#2~w5r_d!LrvD$|ux_8$YzXD5D2k4$H%r@qrbbWq-SvWCY5l| + +""" + self.css_node = CssCompressor(self.css) + + self.js = """\ + +""" + self.js_node = JsCompressor(self.js) + + def assertEqualCollapsed(self, a, b): + """ + assertEqual with internal newlines collapsed to single, and + trailing whitespace removed. + """ + collapse = lambda x: re.sub(r'\n+', '\n', x).rstrip() + self.assertEqual(collapse(a), collapse(b)) + + def assertEqualSplits(self, a, b): + """ + assertEqual for splits, particularly ignoring the presence of + a trailing newline on the content. + """ + mangle = lambda split: [(x[0], x[1], x[2], x[3].rstrip()) for x in split] + self.assertEqual(mangle(a), mangle(b)) + + def test_css_split(self): + out = [ + ( + SOURCE_FILE, + os.path.join(settings.COMPRESS_ROOT, 'css', 'one.css'), + 'css/one.css', '', + ), + ( + SOURCE_HUNK, + 'p { border:5px solid green;}', + None, + '', + ), + ( + SOURCE_FILE, + os.path.join(settings.COMPRESS_ROOT, 'css', 'two.css'), + 'css/two.css', + '', + ), + ] + split = self.css_node.split_contents() + split = [(x[0], x[1], x[2], self.css_node.parser.elem_str(x[3])) for x in split] + self.assertEqualSplits(split, out) + + def test_css_hunks(self): + out = ['body { background:#990; }', 'p { border:5px solid green;}', 'body { color:#fff; }'] + self.assertEqual(out, list(self.css_node.hunks())) + + def test_css_output(self): + out = 'body { background:#990; }\np { border:5px solid green;}\nbody { color:#fff; }' + hunks = '\n'.join([h for h in self.css_node.hunks()]) + self.assertEqual(out, hunks) + + def test_css_mtimes(self): + is_date = re.compile(r'^\d{10}[\.\d]+$') + for date in self.css_node.mtimes: + self.assertTrue(is_date.match(str(float(date))), + "mtimes is returning something that doesn't look like a date: %s" % date) + + def test_css_return_if_off(self): + settings.COMPRESS_ENABLED = False + self.assertEqualCollapsed(self.css, self.css_node.output()) + + def test_cachekey(self): + is_cachekey = re.compile(r'\w{12}') + self.assertTrue(is_cachekey.match(self.css_node.cachekey), + "cachekey is returning something that doesn't look like r'\w{12}'") + + def test_css_return_if_on(self): + output = css_tag('/static/CACHE/css/e41ba2cc6982.css') + self.assertEqual(output, self.css_node.output().strip()) + + def test_js_split(self): + out = [ + ( + SOURCE_FILE, + os.path.join(settings.COMPRESS_ROOT, 'js', 'one.js'), + 'js/one.js', + '', + ), + ( + SOURCE_HUNK, + 'obj.value = "value";', + None, + '', + ), + ] + split = self.js_node.split_contents() + split = [(x[0], x[1], x[2], self.js_node.parser.elem_str(x[3])) for x in split] + self.assertEqualSplits(split, out) + + def test_js_hunks(self): + out = ['obj = {};', 'obj.value = "value";'] + self.assertEqual(out, list(self.js_node.hunks())) + + def test_js_output(self): + out = '' + self.assertEqual(out, self.js_node.output()) + + def test_js_override_url(self): + self.js_node.context.update({'url': 'This is not a url, just a text'}) + out = '' + self.assertEqual(out, self.js_node.output()) + + def test_css_override_url(self): + self.css_node.context.update({'url': 'This is not a url, just a text'}) + output = css_tag('/static/CACHE/css/e41ba2cc6982.css') + self.assertEqual(output, self.css_node.output().strip()) + + @override_settings(COMPRESS_PRECOMPILERS=(), COMPRESS_ENABLED=False) + def test_js_return_if_off(self): + self.assertEqualCollapsed(self.js, self.js_node.output()) + + def test_js_return_if_on(self): + output = '' + self.assertEqual(output, self.js_node.output()) + + @override_settings(COMPRESS_OUTPUT_DIR='custom') + def test_custom_output_dir1(self): + output = '' + self.assertEqual(output, JsCompressor(self.js).output()) + + @override_settings(COMPRESS_OUTPUT_DIR='') + def test_custom_output_dir2(self): + output = '' + self.assertEqual(output, JsCompressor(self.js).output()) + + @override_settings(COMPRESS_OUTPUT_DIR='/custom/nested/') + def test_custom_output_dir3(self): + output = '' + self.assertEqual(output, JsCompressor(self.js).output()) + + @override_settings(COMPRESS_PRECOMPILERS=( + ('text/foobar', 'compressor.tests.test_base.TestPrecompiler'), + ), COMPRESS_ENABLED=True) + def test_precompiler_class_used(self): + css = '' + css_node = CssCompressor(css) + output = make_soup(css_node.output('inline')) + self.assertEqual(output.text, 'OUTPUT') + + @override_settings(COMPRESS_PRECOMPILERS=( + ('text/foobar', 'compressor.tests.test_base.NonexistentFilter'), + ), COMPRESS_ENABLED=True) + def test_nonexistent_precompiler_class_error(self): + css = '' + css_node = CssCompressor(css) + self.assertRaises(FilterDoesNotExist, css_node.output, 'inline') + + +class CssMediaTestCase(SimpleTestCase): + def setUp(self): + self.css = """\ + + + +""" + + def test_css_output(self): + css_node = CssCompressor(self.css) + if six.PY3: + links = make_soup(css_node.output()).find_all('link') + else: + links = make_soup(css_node.output()).findAll('link') + media = ['screen', 'print', 'all', None] + self.assertEqual(len(links), 4) + self.assertEqual(media, [l.get('media', None) for l in links]) + + def test_avoid_reordering_css(self): + css = self.css + '' + css_node = CssCompressor(css) + media = ['screen', 'print', 'all', None, 'print'] + if six.PY3: + links = make_soup(css_node.output()).find_all('link') + else: + links = make_soup(css_node.output()).findAll('link') + self.assertEqual(media, [l.get('media', None) for l in links]) + + @override_settings(COMPRESS_PRECOMPILERS=( + ('text/foobar', 'python %s {infile} {outfile}' % os.path.join(test_dir, 'precompiler.py')), + ), COMPRESS_ENABLED=False) + def test_passthough_when_compress_disabled(self): + css = """\ + + +""" + css_node = CssCompressor(css) + if six.PY3: + output = make_soup(css_node.output()).find_all(['link', 'style']) + else: + output = make_soup(css_node.output()).findAll(['link', 'style']) + self.assertEqual(['/static/css/one.css', '/static/css/two.css', None], + [l.get('href', None) for l in output]) + self.assertEqual(['screen', 'screen', 'screen'], + [l.get('media', None) for l in output]) + + +class VerboseTestCase(CompressorTestCase): + + def setUp(self): + super(VerboseTestCase, self).setUp() + settings.COMPRESS_VERBOSE = True + + +class CacheBackendTestCase(CompressorTestCase): + + def test_correct_backend(self): + from compressor.cache import cache + self.assertEqual(cache.__class__, locmem.CacheClass) diff --git a/django-compressor/compressor/tests/test_filters.py b/django-compressor/compressor/tests/test_filters.py new file mode 100644 index 0000000..b656a65 --- /dev/null +++ b/django-compressor/compressor/tests/test_filters.py @@ -0,0 +1,303 @@ +from __future__ import with_statement, unicode_literals +import io +import os +import sys +import textwrap + +from django.utils import six +from django.test import TestCase +from django.utils import unittest +from django.test.utils import override_settings + +from compressor.cache import get_hashed_mtime, get_hashed_content +from compressor.conf import settings +from compressor.css import CssCompressor +from compressor.utils import find_command +from compressor.filters.base import CompilerFilter +from compressor.filters.cssmin import CSSMinFilter +from compressor.filters.css_default import CssAbsoluteFilter +from compressor.filters.template import TemplateFilter +from compressor.tests.test_base import test_dir + + +@unittest.skipIf(find_command(settings.COMPRESS_CSSTIDY_BINARY) is None, + 'CSStidy binary %r not found' % settings.COMPRESS_CSSTIDY_BINARY) +class CssTidyTestCase(TestCase): + def test_tidy(self): + content = textwrap.dedent("""\ + /* Some comment */ + font,th,td,p{ + color: black; + } + """) + from compressor.filters.csstidy import CSSTidyFilter + ret = CSSTidyFilter(content).input() + self.assertIsInstance(ret, six.text_type) + self.assertEqual( + "font,th,td,p{color:#000;}", CSSTidyFilter(content).input()) + + +class PrecompilerTestCase(TestCase): + def setUp(self): + self.filename = os.path.join(test_dir, 'static/css/one.css') + with io.open(self.filename, encoding=settings.FILE_CHARSET) as file: + self.content = file.read() + self.test_precompiler = os.path.join(test_dir, 'precompiler.py') + + def test_precompiler_infile_outfile(self): + command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler) + compiler = CompilerFilter( + content=self.content, filename=self.filename, + charset=settings.FILE_CHARSET, command=command) + self.assertEqual("body { color:#990; }", compiler.input()) + + def test_precompiler_infile_stdout(self): + command = '%s %s -f {infile}' % (sys.executable, self.test_precompiler) + compiler = CompilerFilter( + content=self.content, filename=None, charset=None, command=command) + self.assertEqual("body { color:#990; }%s" % os.linesep, compiler.input()) + + def test_precompiler_stdin_outfile(self): + command = '%s %s -o {outfile}' % (sys.executable, self.test_precompiler) + compiler = CompilerFilter( + content=self.content, filename=None, charset=None, command=command) + self.assertEqual("body { color:#990; }", compiler.input()) + + def test_precompiler_stdin_stdout(self): + command = '%s %s' % (sys.executable, self.test_precompiler) + compiler = CompilerFilter( + content=self.content, filename=None, charset=None, command=command) + self.assertEqual("body { color:#990; }%s" % os.linesep, compiler.input()) + + def test_precompiler_stdin_stdout_filename(self): + command = '%s %s' % (sys.executable, self.test_precompiler) + compiler = CompilerFilter( + content=self.content, filename=self.filename, + charset=settings.FILE_CHARSET, command=command) + self.assertEqual("body { color:#990; }%s" % os.linesep, compiler.input()) + + def test_precompiler_output_unicode(self): + command = '%s %s' % (sys.executable, self.test_precompiler) + compiler = CompilerFilter(content=self.content, filename=self.filename, command=command) + self.assertEqual(type(compiler.input()), six.text_type) + + +class CssMinTestCase(TestCase): + def test_cssmin_filter(self): + content = """p { + + + background: rgb(51,102,153) url('../../images/image.gif'); + + + } + """ + output = "p{background:#369 url('../../images/image.gif')}" + self.assertEqual(output, CSSMinFilter(content).output()) + + +class CssAbsolutizingTestCase(TestCase): + hashing_method = 'mtime' + hashing_func = staticmethod(get_hashed_mtime) + content = ("p { background: url('../../img/python.png') }" + "p { filter: Alpha(src='../../img/python.png') }") + + def setUp(self): + self.old_enabled = settings.COMPRESS_ENABLED + self.old_url = settings.COMPRESS_URL + self.old_hashing_method = settings.COMPRESS_CSS_HASHING_METHOD + settings.COMPRESS_ENABLED = True + settings.COMPRESS_URL = '/static/' + settings.COMPRESS_CSS_HASHING_METHOD = self.hashing_method + self.css = """ + + + """ + self.css_node = CssCompressor(self.css) + + def tearDown(self): + settings.COMPRESS_ENABLED = self.old_enabled + settings.COMPRESS_URL = self.old_url + settings.COMPRESS_CSS_HASHING_METHOD = self.old_hashing_method + + def test_css_absolute_filter(self): + filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') + imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png') + params = { + 'url': settings.COMPRESS_URL, + 'hash': self.hashing_func(imagefilename), + } + output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" + "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params + filter = CssAbsoluteFilter(self.content) + self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + settings.COMPRESS_URL = params['url'] = 'http://static.example.com/' + filter = CssAbsoluteFilter(self.content) + filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') + output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" + "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params + self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + + def test_css_absolute_filter_url_fragment(self): + filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') + imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png') + params = { + 'url': settings.COMPRESS_URL, + 'hash': self.hashing_func(imagefilename), + } + content = "p { background: url('../../img/python.png#foo') }" + + output = "p { background: url('%(url)simg/python.png?%(hash)s#foo') }" % params + filter = CssAbsoluteFilter(content) + self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + settings.COMPRESS_URL = params['url'] = 'http://media.example.com/' + filter = CssAbsoluteFilter(content) + filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') + output = "p { background: url('%(url)simg/python.png?%(hash)s#foo') }" % params + self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + + def test_css_absolute_filter_only_url_fragment(self): + filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') + content = "p { background: url('#foo') }" + filter = CssAbsoluteFilter(content) + self.assertEqual(content, filter.input(filename=filename, basename='css/url/test.css')) + settings.COMPRESS_URL = 'http://media.example.com/' + filter = CssAbsoluteFilter(content) + filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') + self.assertEqual(content, filter.input(filename=filename, basename='css/url/test.css')) + + def test_css_absolute_filter_querystring(self): + filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') + imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png') + params = { + 'url': settings.COMPRESS_URL, + 'hash': self.hashing_func(imagefilename), + } + content = "p { background: url('../../img/python.png?foo') }" + + output = "p { background: url('%(url)simg/python.png?foo&%(hash)s') }" % params + filter = CssAbsoluteFilter(content) + self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + settings.COMPRESS_URL = params['url'] = 'http://media.example.com/' + filter = CssAbsoluteFilter(content) + filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') + output = "p { background: url('%(url)simg/python.png?foo&%(hash)s') }" % params + self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + + def test_css_absolute_filter_https(self): + filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') + imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png') + params = { + 'url': settings.COMPRESS_URL, + 'hash': self.hashing_func(imagefilename), + } + output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" + "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params + filter = CssAbsoluteFilter(self.content) + self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + settings.COMPRESS_URL = params['url'] = 'https://static.example.com/' + filter = CssAbsoluteFilter(self.content) + filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') + output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" + "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params + self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + + def test_css_absolute_filter_relative_path(self): + filename = os.path.join(settings.TEST_DIR, 'whatever', '..', 'static', 'whatever/../css/url/test.css') + imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png') + params = { + 'url': settings.COMPRESS_URL, + 'hash': self.hashing_func(imagefilename), + } + output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" + "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params + filter = CssAbsoluteFilter(self.content) + self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + settings.COMPRESS_URL = params['url'] = 'https://static.example.com/' + filter = CssAbsoluteFilter(self.content) + output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" + "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params + self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + + def test_css_hunks(self): + hash_dict = { + 'hash1': self.hashing_func(os.path.join(settings.COMPRESS_ROOT, 'img/python.png')), + 'hash2': self.hashing_func(os.path.join(settings.COMPRESS_ROOT, 'img/add.png')), + } + self.assertEqual(["""\ +p { background: url('/static/img/python.png?%(hash1)s'); } +p { background: url('/static/img/python.png?%(hash1)s'); } +p { background: url('/static/img/python.png?%(hash1)s'); } +p { background: url('/static/img/python.png?%(hash1)s'); } +p { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/static/img/python.png?%(hash1)s'); } +""" % hash_dict, + """\ +p { background: url('/static/img/add.png?%(hash2)s'); } +p { background: url('/static/img/add.png?%(hash2)s'); } +p { background: url('/static/img/add.png?%(hash2)s'); } +p { background: url('/static/img/add.png?%(hash2)s'); } +p { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/static/img/add.png?%(hash2)s'); } +""" % hash_dict], list(self.css_node.hunks())) + + def test_guess_filename(self): + for base_url in ('/static/', 'http://static.example.com/'): + settings.COMPRESS_URL = base_url + url = '%s/img/python.png' % settings.COMPRESS_URL.rstrip('/') + path = os.path.join(settings.COMPRESS_ROOT, 'img/python.png') + content = "p { background: url('%s') }" % url + filter = CssAbsoluteFilter(content) + self.assertEqual(path, filter.guess_filename(url)) + + +class CssAbsolutizingTestCaseWithHash(CssAbsolutizingTestCase): + hashing_method = 'content' + hashing_func = staticmethod(get_hashed_content) + + def setUp(self): + super(CssAbsolutizingTestCaseWithHash, self).setUp() + self.css = """ + + + """ + self.css_node = CssCompressor(self.css) + + +class CssDataUriTestCase(TestCase): + def setUp(self): + settings.COMPRESS_ENABLED = True + settings.COMPRESS_CSS_FILTERS = [ + 'compressor.filters.css_default.CssAbsoluteFilter', + 'compressor.filters.datauri.CssDataUriFilter', + ] + settings.COMPRESS_URL = '/static/' + settings.COMPRESS_CSS_HASHING_METHOD = 'mtime' + self.css = """ + + """ + self.css_node = CssCompressor(self.css) + + def test_data_uris(self): + datauri_hash = get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'img/python.png')) + out = ['''.add { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg=="); } +.add-with-hash { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg=="); } +.python { background-image: url("/static/img/python.png?%s"); } +.datauri { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0 vr4MkhoXe0rZigAAAABJRU5ErkJggg=="); } +''' % datauri_hash] + self.assertEqual(out, list(self.css_node.hunks())) + + +class TemplateTestCase(TestCase): + @override_settings(COMPRESS_TEMPLATE_FILTER_CONTEXT={ + 'stuff': 'thing', + 'gimmick': 'bold' + }) + def test_template_filter(self): + content = """ + #content {background-image: url("{{ STATIC_URL|default:stuff }}/images/bg.png");} + #footer {font-weight: {{ gimmick }};} + """ + input = """ + #content {background-image: url("thing/images/bg.png");} + #footer {font-weight: bold;} + """ + self.assertEqual(input, TemplateFilter(content).input()) diff --git a/django-compressor/compressor/tests/test_jinja2ext.py b/django-compressor/compressor/tests/test_jinja2ext.py new file mode 100644 index 0000000..5adc8ee --- /dev/null +++ b/django-compressor/compressor/tests/test_jinja2ext.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +from __future__ import with_statement, unicode_literals + +import sys + +from django.test import TestCase +from django.utils import unittest, six +from django.test.utils import override_settings + +from compressor.conf import settings +from compressor.tests.test_base import css_tag + + +@unittest.skipUnless(not six.PY3 or sys.version_info[:2] >= (3, 3), + 'Jinja can only run on Python < 3 and >= 3.3') +class TestJinja2CompressorExtension(TestCase): + """ + Test case for jinja2 extension. + + .. note:: + At tests we need to make some extra care about whitespace. Please note + that we use jinja2 specific controls (*minus* character at block's + beginning or end). For more information see jinja2 documentation. + """ + def assertStrippedEqual(self, result, expected): + self.assertEqual(result.strip(), expected.strip(), "%r != %r" % ( + result.strip(), expected.strip())) + + def setUp(self): + import jinja2 + self.jinja2 = jinja2 + from compressor.contrib.jinja2ext import CompressorExtension + self.env = self.jinja2.Environment(extensions=[CompressorExtension]) + + def test_error_raised_if_no_arguments_given(self): + self.assertRaises(self.jinja2.exceptions.TemplateSyntaxError, + self.env.from_string, '{% compress %}Foobar{% endcompress %}') + + def test_error_raised_if_wrong_kind_given(self): + self.assertRaises(self.jinja2.exceptions.TemplateSyntaxError, + self.env.from_string, '{% compress foo %}Foobar{% endcompress %}') + + def test_error_raised_if_wrong_closing_kind_given(self): + self.assertRaises(self.jinja2.exceptions.TemplateSyntaxError, + self.env.from_string, '{% compress js %}Foobar{% endcompress css %}') + + def test_error_raised_if_wrong_mode_given(self): + self.assertRaises(self.jinja2.exceptions.TemplateSyntaxError, + self.env.from_string, '{% compress css foo %}Foobar{% endcompress %}') + + @override_settings(COMPRESS_ENABLED=False) + def test_compress_is_disabled(self): + tag_body = '\n'.join([ + '', + '', + '', + ]) + template_string = '{% compress css %}' + tag_body + '{% endcompress %}' + template = self.env.from_string(template_string) + self.assertEqual(tag_body, template.render()) + + # Test with explicit kind + template_string = '{% compress css %}' + tag_body + '{% endcompress css %}' + template = self.env.from_string(template_string) + self.assertEqual(tag_body, template.render()) + + def test_empty_tag(self): + template = self.env.from_string("""{% compress js %}{% block js %} + {% endblock %}{% endcompress %}""") + context = {'STATIC_URL': settings.COMPRESS_URL} + self.assertEqual('', template.render(context)) + + def test_empty_tag_with_kind(self): + template = self.env.from_string("""{% compress js %}{% block js %} + {% endblock %}{% endcompress js %}""") + context = {'STATIC_URL': settings.COMPRESS_URL} + self.assertEqual('', template.render(context)) + + def test_css_tag(self): + template = self.env.from_string("""{% compress css -%} + + + + {% endcompress %}""") + context = {'STATIC_URL': settings.COMPRESS_URL} + out = css_tag("/static/CACHE/css/e41ba2cc6982.css") + self.assertEqual(out, template.render(context)) + + def test_nonascii_css_tag(self): + template = self.env.from_string("""{% compress css -%} + + + {% endcompress %}""") + context = {'STATIC_URL': settings.COMPRESS_URL} + out = css_tag("/static/CACHE/css/799f6defe43c.css") + self.assertEqual(out, template.render(context)) + + def test_js_tag(self): + template = self.env.from_string("""{% compress js -%} + + + {% endcompress %}""") + context = {'STATIC_URL': settings.COMPRESS_URL} + out = '' + self.assertEqual(out, template.render(context)) + + def test_nonascii_js_tag(self): + template = self.env.from_string("""{% compress js -%} + + + {% endcompress %}""") + context = {'STATIC_URL': settings.COMPRESS_URL} + out = '' + self.assertEqual(out, template.render(context)) + + def test_nonascii_latin1_js_tag(self): + template = self.env.from_string("""{% compress js -%} + + + {% endcompress %}""") + context = {'STATIC_URL': settings.COMPRESS_URL} + out = '' + self.assertEqual(out, template.render(context)) + + def test_css_inline(self): + template = self.env.from_string("""{% compress css, inline -%} + + + {% endcompress %}""") + context = {'STATIC_URL': settings.COMPRESS_URL} + out = '\n'.join([ + '', + ]) + self.assertEqual(out, template.render(context)) + + def test_js_inline(self): + template = self.env.from_string("""{% compress js, inline -%} + + + {% endcompress %}""") + context = {'STATIC_URL': settings.COMPRESS_URL} + out = '' + self.assertEqual(out, template.render(context)) + + def test_nonascii_inline_css(self): + org_COMPRESS_ENABLED = settings.COMPRESS_ENABLED + settings.COMPRESS_ENABLED = False + template = self.env.from_string('{% compress css %}' + '{% endcompress %}') + out = '' + settings.COMPRESS_ENABLED = org_COMPRESS_ENABLED + context = {'STATIC_URL': settings.COMPRESS_URL} + self.assertEqual(out, template.render(context)) diff --git a/django-compressor/compressor/tests/test_offline.py b/django-compressor/compressor/tests/test_offline.py new file mode 100644 index 0000000..327b901 --- /dev/null +++ b/django-compressor/compressor/tests/test_offline.py @@ -0,0 +1,499 @@ +from __future__ import with_statement, unicode_literals +import io +import os +import sys + +from django.core.management.base import CommandError +from django.template import Template, Context +from django.test import TestCase +from django.utils import six, unittest + +from compressor.cache import flush_offline_manifest, get_offline_manifest +from compressor.conf import settings +from compressor.exceptions import OfflineGenerationError +from compressor.management.commands.compress import Command as CompressCommand +from compressor.storage import default_storage + +if six.PY3: + # there is an 'io' module in python 2.6+, but io.StringIO does not + # accept regular strings, just unicode objects + from io import StringIO +else: + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO + +# The Jinja2 tests fail on Python 3.2 due to the following: +# The line in compressor/management/commands/compress.py: +# compressor_nodes.setdefault(template, []).extend(nodes) +# causes the error "unhashable type: 'Template'" +_TEST_JINJA2 = not(sys.version_info[0] == 3 and sys.version_info[1] == 2) + + +class OfflineTestCaseMixin(object): + template_name = "test_compressor_offline.html" + verbosity = 0 + # Change this for each test class + templates_dir = "" + expected_hash = "" + # Engines to test + if _TEST_JINJA2: + engines = ("django", "jinja2") + else: + engines = ("django",) + + def setUp(self): + self._old_compress = settings.COMPRESS_ENABLED + self._old_compress_offline = settings.COMPRESS_OFFLINE + self._old_template_dirs = settings.TEMPLATE_DIRS + self._old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT + self.log = StringIO() + + # Reset template dirs, because it enables us to force compress to + # consider only a specific directory (helps us make true, + # independant unit tests). + # Specify both Jinja2 and Django template locations. When the wrong engine + # is used to parse a template, the TemplateSyntaxError will cause the + # template to be skipped over. + django_template_dir = os.path.join(settings.TEST_DIR, 'test_templates', self.templates_dir) + jinja2_template_dir = os.path.join(settings.TEST_DIR, 'test_templates_jinja2', self.templates_dir) + settings.TEMPLATE_DIRS = (django_template_dir, jinja2_template_dir) + + # Enable offline compress + settings.COMPRESS_ENABLED = True + settings.COMPRESS_OFFLINE = True + + if "django" in self.engines: + self.template_path = os.path.join(django_template_dir, self.template_name) + + with io.open(self.template_path, encoding=settings.FILE_CHARSET) as file: + self.template = Template(file.read()) + + self._old_jinja2_get_environment = settings.COMPRESS_JINJA2_GET_ENVIRONMENT + + if "jinja2" in self.engines: + # Setup Jinja2 settings. + settings.COMPRESS_JINJA2_GET_ENVIRONMENT = lambda: self._get_jinja2_env() + jinja2_env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT() + self.template_path_jinja2 = os.path.join(jinja2_template_dir, self.template_name) + + with io.open(self.template_path_jinja2, encoding=settings.FILE_CHARSET) as file: + self.template_jinja2 = jinja2_env.from_string(file.read()) + + def tearDown(self): + settings.COMPRESS_JINJA2_GET_ENVIRONMENT = self._old_jinja2_get_environment + settings.COMPRESS_ENABLED = self._old_compress + settings.COMPRESS_OFFLINE = self._old_compress_offline + settings.TEMPLATE_DIRS = self._old_template_dirs + manifest_path = os.path.join('CACHE', 'manifest.json') + if default_storage.exists(manifest_path): + default_storage.delete(manifest_path) + + def _render_template(self, engine): + if engine == "django": + return self.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT)) + elif engine == "jinja2": + return self.template_jinja2.render(settings.COMPRESS_OFFLINE_CONTEXT) + "\n" + else: + return None + + def _test_offline(self, engine): + count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine) + self.assertEqual(1, count) + self.assertEqual([ + '' % (self.expected_hash, ), + ], result) + rendered_template = self._render_template(engine) + self.assertEqual(rendered_template, "".join(result) + "\n") + + def test_offline(self): + for engine in self.engines: + self._test_offline(engine=engine) + + def _get_jinja2_env(self): + import jinja2 + import jinja2.ext + from compressor.offline.jinja2 import url_for, SpacelessExtension + from compressor.contrib.jinja2ext import CompressorExtension + + # Extensions needed for the test cases only. + extensions = [ + CompressorExtension, + SpacelessExtension, + jinja2.ext.with_, + jinja2.ext.do, + ] + loader = self._get_jinja2_loader() + env = jinja2.Environment(extensions=extensions, loader=loader) + env.globals['url_for'] = url_for + + return env + + def _get_jinja2_loader(self): + import jinja2 + + loader = jinja2.FileSystemLoader(settings.TEMPLATE_DIRS, encoding=settings.FILE_CHARSET) + return loader + + +class OfflineGenerationSkipDuplicatesTestCase(OfflineTestCaseMixin, TestCase): + templates_dir = "test_duplicate" + + # We don't need to test multiples engines here. + engines = ("django",) + + def _test_offline(self, engine): + count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine) + # Only one block compressed, the second identical one was skipped. + self.assertEqual(1, count) + # Only 1 ', + ], result) + rendered_template = self._render_template(engine) + # But rendering the template returns both (identical) scripts. + self.assertEqual(rendered_template, "".join(result * 2) + "\n") + + +class OfflineGenerationBlockSuperTestCase(OfflineTestCaseMixin, TestCase): + templates_dir = "test_block_super" + expected_hash = "7c02d201f69d" + # Block.super not supported for Jinja2 yet. + engines = ("django",) + + +class OfflineGenerationBlockSuperMultipleTestCase(OfflineTestCaseMixin, TestCase): + templates_dir = "test_block_super_multiple" + expected_hash = "f8891c416981" + # Block.super not supported for Jinja2 yet. + engines = ("django",) + + +class OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase(OfflineTestCaseMixin, TestCase): + templates_dir = "test_block_super_multiple_cached" + expected_hash = "2f6ef61c488e" + # Block.super not supported for Jinja2 yet. + engines = ("django",) + + def setUp(self): + self._old_template_loaders = settings.TEMPLATE_LOADERS + settings.TEMPLATE_LOADERS = ( + ('django.template.loaders.cached.Loader', ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + )), + ) + super(OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase, self).setUp() + + def tearDown(self): + super(OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase, self).tearDown() + settings.TEMPLATE_LOADERS = self._old_template_loaders + + +class OfflineGenerationBlockSuperTestCaseWithExtraContent(OfflineTestCaseMixin, TestCase): + templates_dir = "test_block_super_extra" + # Block.super not supported for Jinja2 yet. + engines = ("django",) + + def _test_offline(self, engine): + count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine) + self.assertEqual(2, count) + self.assertEqual([ + '', + '' + ], result) + rendered_template = self._render_template(engine) + self.assertEqual(rendered_template, "".join(result) + "\n") + + +class OfflineGenerationConditionTestCase(OfflineTestCaseMixin, TestCase): + templates_dir = "test_condition" + expected_hash = "4e3758d50224" + + def setUp(self): + self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT + settings.COMPRESS_OFFLINE_CONTEXT = { + 'condition': 'red', + } + super(OfflineGenerationConditionTestCase, self).setUp() + + def tearDown(self): + self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context + super(OfflineGenerationConditionTestCase, self).tearDown() + + +class OfflineGenerationTemplateTagTestCase(OfflineTestCaseMixin, TestCase): + templates_dir = "test_templatetag" + expected_hash = "a27e1d3a619a" + + +class OfflineGenerationStaticTemplateTagTestCase(OfflineTestCaseMixin, TestCase): + templates_dir = "test_static_templatetag" + expected_hash = "dfa2bb387fa8" + + +class OfflineGenerationTestCaseWithContext(OfflineTestCaseMixin, TestCase): + templates_dir = "test_with_context" + expected_hash = "5838e2fd66af" + + def setUp(self): + self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT + settings.COMPRESS_OFFLINE_CONTEXT = { + 'content': 'OK!', + } + super(OfflineGenerationTestCaseWithContext, self).setUp() + + def tearDown(self): + settings.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context + super(OfflineGenerationTestCaseWithContext, self).tearDown() + + +class OfflineGenerationTestCaseErrors(OfflineTestCaseMixin, TestCase): + templates_dir = "test_error_handling" + + def _test_offline(self, engine): + count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine) + + if engine == "django": + self.assertEqual(2, count) + else: + # Because we use env.parse in Jinja2Parser, the engine does not + # actually load the "extends" and "includes" templates, and so + # it is unable to detect that they are missing. So all the "compress" + # nodes are processed correctly. + self.assertEqual(4, count) + self.assertEqual(engine, "jinja2") + self.assertIn('', result) + self.assertIn('', result) + + self.assertIn('', result) + self.assertIn('', result) + + +class OfflineGenerationTestCaseWithError(OfflineTestCaseMixin, TestCase): + templates_dir = 'test_error_handling' + + def setUp(self): + self._old_compress_precompilers = settings.COMPRESS_PRECOMPILERS + settings.COMPRESS_PRECOMPILERS = (('text/coffeescript', 'non-existing-binary'),) + super(OfflineGenerationTestCaseWithError, self).setUp() + + def _test_offline(self, engine): + """ + Test that a CommandError is raised with DEBUG being False as well as + True, as otherwise errors in configuration will never show in + production. + """ + self._old_debug = settings.DEBUG + + try: + settings.DEBUG = True + self.assertRaises(CommandError, CompressCommand().compress, engine=engine) + + settings.DEBUG = False + self.assertRaises(CommandError, CompressCommand().compress, engine=engine) + + finally: + settings.DEBUG = self._old_debug + + def tearDown(self): + settings.COMPRESS_PRECOMPILERS = self._old_compress_precompilers + super(OfflineGenerationTestCaseWithError, self).tearDown() + + +class OfflineGenerationTestCase(OfflineTestCaseMixin, TestCase): + templates_dir = "basic" + expected_hash = "f5e179b8eca4" + + def test_rendering_without_manifest_raises_exception(self): + # flush cached manifest + flush_offline_manifest() + self.assertRaises(OfflineGenerationError, + self.template.render, Context({})) + + @unittest.skipIf(not _TEST_JINJA2, "No Jinja2 testing") + def test_rendering_without_manifest_raises_exception_jinja2(self): + # flush cached manifest + flush_offline_manifest() + self.assertRaises(OfflineGenerationError, + self.template_jinja2.render, {}) + + def _test_deleting_manifest_does_not_affect_rendering(self, engine): + count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine) + get_offline_manifest() + manifest_path = os.path.join('CACHE', 'manifest.json') + if default_storage.exists(manifest_path): + default_storage.delete(manifest_path) + self.assertEqual(1, count) + self.assertEqual([ + '' % (self.expected_hash, ), + ], result) + rendered_template = self._render_template(engine) + self.assertEqual(rendered_template, "".join(result) + "\n") + + def test_deleting_manifest_does_not_affect_rendering(self): + for engine in self.engines: + self._test_deleting_manifest_does_not_affect_rendering(engine) + + def test_requires_model_validation(self): + self.assertFalse(CompressCommand.requires_model_validation) + + def test_get_loaders(self): + old_loaders = settings.TEMPLATE_LOADERS + settings.TEMPLATE_LOADERS = ( + ('django.template.loaders.cached.Loader', ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + )), + ) + try: + from django.template.loaders.filesystem import Loader as FileSystemLoader + from django.template.loaders.app_directories import Loader as AppDirectoriesLoader + except ImportError: + pass + else: + loaders = CompressCommand().get_loaders() + self.assertTrue(isinstance(loaders[0], FileSystemLoader)) + self.assertTrue(isinstance(loaders[1], AppDirectoriesLoader)) + finally: + settings.TEMPLATE_LOADERS = old_loaders + + +class OfflineGenerationBlockSuperBaseCompressed(OfflineTestCaseMixin, TestCase): + template_names = ["base.html", "base2.html", "test_compressor_offline.html"] + templates_dir = 'test_block_super_base_compressed' + expected_hash = ['028c3fc42232', '2e9d3f5545a6', 'f8891c416981'] + # Block.super not supported for Jinja2 yet. + engines = ("django",) + + def setUp(self): + super(OfflineGenerationBlockSuperBaseCompressed, self).setUp() + + self.template_paths = [] + self.templates = [] + for template_name in self.template_names: + template_path = os.path.join(settings.TEMPLATE_DIRS[0], template_name) + self.template_paths.append(template_path) + with io.open(template_path, encoding=settings.FILE_CHARSET) as file: + template = Template(file.read()) + self.templates.append(template) + + def _render_template(self, template, engine): + if engine == "django": + return template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT)) + elif engine == "jinja2": + return template.render(settings.COMPRESS_OFFLINE_CONTEXT) + "\n" + else: + return None + + def _test_offline(self, engine): + count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine) + self.assertEqual(len(self.expected_hash), count) + for expected_hash, template in zip(self.expected_hash, self.templates): + expected_output = '' % (expected_hash, ) + self.assertIn(expected_output, result) + rendered_template = self._render_template(template, engine) + self.assertEqual(rendered_template, expected_output + '\n') + + +class OfflineGenerationInlineNonAsciiTestCase(OfflineTestCaseMixin, TestCase): + templates_dir = "test_inline_non_ascii" + + def setUp(self): + self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT + settings.COMPRESS_OFFLINE_CONTEXT = { + 'test_non_ascii_value': '\u2014', + } + super(OfflineGenerationInlineNonAsciiTestCase, self).setUp() + + def tearDown(self): + self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context + super(OfflineGenerationInlineNonAsciiTestCase, self).tearDown() + + def _test_offline(self, engine): + count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine) + rendered_template = self._render_template(engine) + self.assertEqual(rendered_template, "".join(result) + "\n") + + +class OfflineGenerationComplexTestCase(OfflineTestCaseMixin, TestCase): + templates_dir = "test_complex" + + def setUp(self): + self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT + settings.COMPRESS_OFFLINE_CONTEXT = { + 'condition': 'OK!', + # Django templating does not allow definition of tuples in the + # templates. Make sure this is same as test_templates_jinja2/test_complex. + 'my_names': ("js/one.js", "js/nonasc.js"), + } + super(OfflineGenerationComplexTestCase, self).setUp() + + def tearDown(self): + self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context + super(OfflineGenerationComplexTestCase, self).tearDown() + + def _test_offline(self, engine): + count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine) + self.assertEqual(3, count) + self.assertEqual([ + '', + '', + '', + ], result) + rendered_template = self._render_template(engine) + result = (result[0], result[2]) + self.assertEqual(rendered_template, "".join(result) + "\n") + + +# Coffin does not work on Python 3.2+ due to: +# The line at coffin/template/__init__.py:15 +# from library import * +# causing 'ImportError: No module named library'. +# It seems there is no evidence nor indicated support for Python 3+. +@unittest.skipIf(sys.version_info >= (3, 2), + "Coffin does not support 3.2+") +class OfflineGenerationCoffinTestCase(OfflineTestCaseMixin, TestCase): + templates_dir = "test_coffin" + expected_hash = "32c8281e3346" + engines = ("jinja2",) + + def _get_jinja2_env(self): + import jinja2 + from coffin.common import env + from compressor.contrib.jinja2ext import CompressorExtension + + # Could have used the env.add_extension method, but it's only available + # in Jinja2 v2.5 + new_env = jinja2.Environment(extensions=[CompressorExtension]) + env.extensions.update(new_env.extensions) + + return env + + +# Jingo does not work when using Python 3.2 due to the use of Unicode string +# prefix (and possibly other stuff), but it actually works when using Python 3.3 +# since it tolerates the use of the Unicode string prefix. Python 3.3 support +# is also evident in its tox.ini file. +@unittest.skipIf(sys.version_info >= (3, 2) and sys.version_info < (3, 3), + "Jingo does not support 3.2") +class OfflineGenerationJingoTestCase(OfflineTestCaseMixin, TestCase): + templates_dir = "test_jingo" + expected_hash = "61ec584468eb" + engines = ("jinja2",) + + def _get_jinja2_env(self): + import jinja2 + import jinja2.ext + from jingo import env + from compressor.contrib.jinja2ext import CompressorExtension + from compressor.offline.jinja2 import SpacelessExtension, url_for + + # Could have used the env.add_extension method, but it's only available + # in Jinja2 v2.5 + new_env = jinja2.Environment(extensions=[CompressorExtension, SpacelessExtension, jinja2.ext.with_]) + env.extensions.update(new_env.extensions) + env.globals['url_for'] = url_for + + return env diff --git a/django-compressor/compressor/tests/test_parsers.py b/django-compressor/compressor/tests/test_parsers.py new file mode 100644 index 0000000..d9b4dd6 --- /dev/null +++ b/django-compressor/compressor/tests/test_parsers.py @@ -0,0 +1,125 @@ +from __future__ import with_statement +import os + +try: + import lxml +except ImportError: + lxml = None + +try: + import html5lib +except ImportError: + html5lib = None + +try: + from BeautifulSoup import BeautifulSoup +except ImportError: + BeautifulSoup = None + +from django.utils import unittest +from django.test.utils import override_settings + +from compressor.base import SOURCE_HUNK, SOURCE_FILE +from compressor.conf import settings +from compressor.tests.test_base import CompressorTestCase + + +class ParserTestCase(object): + def setUp(self): + self.old_parser = settings.COMPRESS_PARSER + settings.COMPRESS_PARSER = self.parser_cls + super(ParserTestCase, self).setUp() + + def tearDown(self): + settings.COMPRESS_PARSER = self.old_parser + + +@unittest.skipIf(lxml is None, 'lxml not found') +class LxmlParserTests(ParserTestCase, CompressorTestCase): + parser_cls = 'compressor.parser.LxmlParser' + + +@unittest.skipIf(html5lib is None, 'html5lib not found') +class Html5LibParserTests(ParserTestCase, CompressorTestCase): + parser_cls = 'compressor.parser.Html5LibParser' + # Special test variants required since xml.etree holds attributes + # as a plain dictionary, e.g. key order is unpredictable. + + def test_css_split(self): + split = self.css_node.split_contents() + out0 = ( + SOURCE_FILE, + os.path.join(settings.COMPRESS_ROOT, 'css', 'one.css'), + 'css/one.css', + '{http://www.w3.org/1999/xhtml}link', + {'rel': 'stylesheet', 'href': '/static/css/one.css', + 'type': 'text/css'}, + ) + self.assertEqual(out0, split[0][:3] + (split[0][3].tag, + split[0][3].attrib)) + out1 = ( + SOURCE_HUNK, + 'p { border:5px solid green;}', + None, + '', + ) + self.assertEqual(out1, split[1][:3] + + (self.css_node.parser.elem_str(split[1][3]),)) + out2 = ( + SOURCE_FILE, + os.path.join(settings.COMPRESS_ROOT, 'css', 'two.css'), + 'css/two.css', + '{http://www.w3.org/1999/xhtml}link', + {'rel': 'stylesheet', 'href': '/static/css/two.css', + 'type': 'text/css'}, + ) + self.assertEqual(out2, split[2][:3] + (split[2][3].tag, + split[2][3].attrib)) + + def test_js_split(self): + split = self.js_node.split_contents() + out0 = ( + SOURCE_FILE, + os.path.join(settings.COMPRESS_ROOT, 'js', 'one.js'), + 'js/one.js', + '{http://www.w3.org/1999/xhtml}script', + {'src': '/static/js/one.js', 'type': 'text/javascript'}, + None, + ) + self.assertEqual(out0, split[0][:3] + (split[0][3].tag, + split[0][3].attrib, + split[0][3].text)) + out1 = ( + SOURCE_HUNK, + 'obj.value = "value";', + None, + '{http://www.w3.org/1999/xhtml}script', + {'type': 'text/javascript'}, + 'obj.value = "value";', + ) + self.assertEqual(out1, split[1][:3] + (split[1][3].tag, + split[1][3].attrib, + split[1][3].text)) + + def test_css_return_if_off(self): + settings.COMPRESS_ENABLED = False + # Yes, they are semantically equal but attributes might be + # scrambled in unpredictable order. A more elaborate check + # would require parsing both arguments with a different parser + # and then evaluating the result, which no longer is + # a meaningful unit test. + self.assertEqual(len(self.css), len(self.css_node.output())) + + @override_settings(COMPRESS_PRECOMPILERS=(), COMPRESS_ENABLED=False) + def test_js_return_if_off(self): + # As above. + self.assertEqual(len(self.js), len(self.js_node.output())) + + +@unittest.skipIf(BeautifulSoup is None, 'BeautifulSoup not found') +class BeautifulSoupParserTests(ParserTestCase, CompressorTestCase): + parser_cls = 'compressor.parser.BeautifulSoupParser' + + +class HtmlParserTests(ParserTestCase, CompressorTestCase): + parser_cls = 'compressor.parser.HtmlParser' diff --git a/django-compressor/compressor/tests/test_signals.py b/django-compressor/compressor/tests/test_signals.py new file mode 100644 index 0000000..13d5eed --- /dev/null +++ b/django-compressor/compressor/tests/test_signals.py @@ -0,0 +1,68 @@ +from django.test import TestCase + +from mock import Mock + +from compressor.conf import settings +from compressor.css import CssCompressor +from compressor.js import JsCompressor +from compressor.signals import post_compress + + +class PostCompressSignalTestCase(TestCase): + def setUp(self): + settings.COMPRESS_ENABLED = True + settings.COMPRESS_PRECOMPILERS = () + settings.COMPRESS_DEBUG_TOGGLE = 'nocompress' + self.css = """\ + + +""" + self.css_node = CssCompressor(self.css) + + self.js = """\ + +""" + self.js_node = JsCompressor(self.js) + + def tearDown(self): + post_compress.disconnect() + + def test_js_signal_sent(self): + def listener(sender, **kwargs): + pass + callback = Mock(wraps=listener) + post_compress.connect(callback) + self.js_node.output() + args, kwargs = callback.call_args + self.assertEqual(JsCompressor, kwargs['sender']) + self.assertEqual('js', kwargs['type']) + self.assertEqual('file', kwargs['mode']) + context = kwargs['context'] + assert 'url' in context['compressed'] + + def test_css_signal_sent(self): + def listener(sender, **kwargs): + pass + callback = Mock(wraps=listener) + post_compress.connect(callback) + self.css_node.output() + args, kwargs = callback.call_args + self.assertEqual(CssCompressor, kwargs['sender']) + self.assertEqual('css', kwargs['type']) + self.assertEqual('file', kwargs['mode']) + context = kwargs['context'] + assert 'url' in context['compressed'] + + def test_css_signal_multiple_media_attributes(self): + css = """\ + + +""" + css_node = CssCompressor(css) + + def listener(sender, **kwargs): + pass + callback = Mock(wraps=listener) + post_compress.connect(callback) + css_node.output() + self.assertEqual(3, callback.call_count) diff --git a/django-compressor/compressor/tests/test_storages.py b/django-compressor/compressor/tests/test_storages.py new file mode 100644 index 0000000..91a36f2 --- /dev/null +++ b/django-compressor/compressor/tests/test_storages.py @@ -0,0 +1,64 @@ +from __future__ import with_statement, unicode_literals +import errno +import os + +from django.core.files.base import ContentFile +from django.core.files.storage import get_storage_class +from django.test import TestCase +from django.utils.functional import LazyObject + +from compressor import storage +from compressor.conf import settings +from compressor.tests.test_base import css_tag +from compressor.tests.test_templatetags import render + + +class GzipStorage(LazyObject): + def _setup(self): + self._wrapped = get_storage_class('compressor.storage.GzipCompressorFileStorage')() + + +class StorageTestCase(TestCase): + def setUp(self): + self.old_enabled = settings.COMPRESS_ENABLED + settings.COMPRESS_ENABLED = True + self.default_storage = storage.default_storage + storage.default_storage = GzipStorage() + + def tearDown(self): + storage.default_storage = self.default_storage + settings.COMPRESS_ENABLED = self.old_enabled + + def test_gzip_storage(self): + storage.default_storage.save('test.txt', ContentFile('yeah yeah')) + self.assertTrue(os.path.exists(os.path.join(settings.COMPRESS_ROOT, 'test.txt'))) + self.assertTrue(os.path.exists(os.path.join(settings.COMPRESS_ROOT, 'test.txt.gz'))) + + def test_css_tag_with_storage(self): + template = """{% load compress %}{% compress css %} + + + + {% endcompress %} + """ + context = {'STATIC_URL': settings.COMPRESS_URL} + out = css_tag("/static/CACHE/css/1d4424458f88.css") + self.assertEqual(out, render(template, context)) + + def test_race_condition_handling(self): + # Hold on to original os.remove + original_remove = os.remove + + def race_remove(path): + "Patched os.remove to raise ENOENT (No such file or directory)" + original_remove(path) + raise OSError(errno.ENOENT, 'Fake ENOENT') + + try: + os.remove = race_remove + self.default_storage.save('race.file', ContentFile('Fake ENOENT')) + self.default_storage.delete('race.file') + self.assertFalse(self.default_storage.exists('race.file')) + finally: + # Restore os.remove + os.remove = original_remove diff --git a/django-compressor/compressor/tests/test_templates/basic/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/basic/test_compressor_offline.html new file mode 100644 index 0000000..7deba32 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/basic/test_compressor_offline.html @@ -0,0 +1,8 @@ +{% load compress %}{% spaceless %} + +{% compress js %} + +{% endcompress %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super/base.html b/django-compressor/compressor/tests/test_templates/test_block_super/base.html new file mode 100644 index 0000000..e9ca3ad --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super/base.html @@ -0,0 +1,15 @@ +{% spaceless %} +{% block js %} + +{% endblock %} + +{% block css %} + +{% endblock %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_block_super/test_compressor_offline.html new file mode 100644 index 0000000..ee9270a --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super/test_compressor_offline.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% load compress %} + +{% block js %}{% spaceless %} + {% compress js %} + {{ block.super }} + + {% endcompress %} +{% endspaceless %}{% endblock %} + +{% block css %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base.html b/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base.html new file mode 100644 index 0000000..481ff40 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base.html @@ -0,0 +1,10 @@ +{% load compress %}{% spaceless %} + +{% compress js %} +{% block js %} + +{% endblock %} +{% endcompress %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base2.html b/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base2.html new file mode 100644 index 0000000..abd074d --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base2.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block js %}{% spaceless %} + {{ block.super }} + +{% endspaceless %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html new file mode 100644 index 0000000..01382ec --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html @@ -0,0 +1,8 @@ +{% extends "base2.html" %} + +{% block js %}{% spaceless %} + {{ block.super }} + +{% endspaceless %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_extra/base.html b/django-compressor/compressor/tests/test_templates/test_block_super_extra/base.html new file mode 100644 index 0000000..e9ca3ad --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super_extra/base.html @@ -0,0 +1,15 @@ +{% spaceless %} +{% block js %} + +{% endblock %} + +{% block css %} + +{% endblock %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_extra/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_block_super_extra/test_compressor_offline.html new file mode 100644 index 0000000..2293065 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super_extra/test_compressor_offline.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% load compress %} + +{% block js %}{% spaceless %} + {% compress js %} + + {% endcompress %} + + {% compress js %} + {{ block.super }} + + {% endcompress %} +{% endspaceless %}{% endblock %} + +{% block css %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_multiple/base.html b/django-compressor/compressor/tests/test_templates/test_block_super_multiple/base.html new file mode 100644 index 0000000..c9ee6cc --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super_multiple/base.html @@ -0,0 +1,15 @@ +{% spaceless %} +{% block js %} + +{% endblock %} + +{% block css %} + +{% endblock %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_multiple/base2.html b/django-compressor/compressor/tests/test_templates/test_block_super_multiple/base2.html new file mode 100644 index 0000000..c781fb5 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super_multiple/base2.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block js %}{% spaceless %} + {{ block.super }} + +{% endspaceless %}{% endblock %} + +{% block css %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_multiple/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_block_super_multiple/test_compressor_offline.html new file mode 100644 index 0000000..a05a7b7 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super_multiple/test_compressor_offline.html @@ -0,0 +1,11 @@ +{% extends "base2.html" %} +{% load compress %} + +{% block js %}{% spaceless %} + {% compress js %} + {{ block.super }} + + {% endcompress %} +{% endspaceless %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base.html b/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base.html new file mode 100644 index 0000000..c9ee6cc --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base.html @@ -0,0 +1,15 @@ +{% spaceless %} +{% block js %} + +{% endblock %} + +{% block css %} + +{% endblock %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base2.html b/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base2.html new file mode 100644 index 0000000..b0b2fef --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base2.html @@ -0,0 +1,3 @@ +{% extends "base.html" %} + +{% block css %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/test_compressor_offline.html new file mode 100644 index 0000000..a05a7b7 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/test_compressor_offline.html @@ -0,0 +1,11 @@ +{% extends "base2.html" %} +{% load compress %} + +{% block js %}{% spaceless %} + {% compress js %} + {{ block.super }} + + {% endcompress %} +{% endspaceless %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates/test_complex/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_complex/test_compressor_offline.html new file mode 100644 index 0000000..6eea06e --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_complex/test_compressor_offline.html @@ -0,0 +1,20 @@ +{% load compress static %}{% spaceless %} + +{% if condition %} + {% compress js%} + + {% with names=my_names %}{% spaceless %} + {% for name in names %} + + {% endfor %} + {% endspaceless %}{% endwith %} + {% endcompress %} +{% endif %}{% if not condition %} + {% compress js %} + + {% endcompress %} +{% else %} + {% compress js %} + + {% endcompress %} +{% endif %}{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_condition/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_condition/test_compressor_offline.html new file mode 100644 index 0000000..4b0223c --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_condition/test_compressor_offline.html @@ -0,0 +1,7 @@ +{% load compress %}{% spaceless %} + +{% if condition %} + {% compress js%} + + {% endcompress %} +{% endif %}{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_duplicate/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_duplicate/test_compressor_offline.html new file mode 100644 index 0000000..6050c8b --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_duplicate/test_compressor_offline.html @@ -0,0 +1,13 @@ +{% load compress %}{% spaceless %} + +{% compress js %} + +{% endcompress %} +{% compress js %} + +{% endcompress %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_error_handling/buggy_extends.html b/django-compressor/compressor/tests/test_templates/test_error_handling/buggy_extends.html new file mode 100644 index 0000000..dede1ce --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_error_handling/buggy_extends.html @@ -0,0 +1,10 @@ +{% extends "buggy_template.html" %} +{% load compress %} + +{% compress css %} + +{% endcompress %} diff --git a/django-compressor/compressor/tests/test_templates/test_error_handling/buggy_template.html b/django-compressor/compressor/tests/test_templates/test_error_handling/buggy_template.html new file mode 100644 index 0000000..1a99dab --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_error_handling/buggy_template.html @@ -0,0 +1,12 @@ +{% load compress %} + +{% compress css %} + +{% endcompress %} + + +{% fail %} diff --git a/django-compressor/compressor/tests/test_templates/test_error_handling/missing_extends.html b/django-compressor/compressor/tests/test_templates/test_error_handling/missing_extends.html new file mode 100644 index 0000000..588ba8a --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_error_handling/missing_extends.html @@ -0,0 +1,10 @@ +{% extends "missing.html" %} +{% load compress %} + +{% compress css %} + +{% endcompress %} diff --git a/django-compressor/compressor/tests/test_templates/test_error_handling/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_error_handling/test_compressor_offline.html new file mode 100644 index 0000000..a0b3c79 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_error_handling/test_compressor_offline.html @@ -0,0 +1,8 @@ +{% load compress %}{% spaceless %} + +{% compress js %} + +{% endcompress %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_error_handling/with_coffeescript.html b/django-compressor/compressor/tests/test_templates/test_error_handling/with_coffeescript.html new file mode 100644 index 0000000..6e56c8a --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_error_handling/with_coffeescript.html @@ -0,0 +1,7 @@ +{% load compress %} + +{% compress js %} + +{% endcompress %} diff --git a/django-compressor/compressor/tests/test_templates/test_inline_non_ascii/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_inline_non_ascii/test_compressor_offline.html new file mode 100644 index 0000000..a4d2bc5 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_inline_non_ascii/test_compressor_offline.html @@ -0,0 +1,7 @@ +{% load compress %}{% spaceless %} + +{% compress js inline %} + +{% endcompress %}{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_static_templatetag/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_static_templatetag/test_compressor_offline.html new file mode 100644 index 0000000..8e17d32 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_static_templatetag/test_compressor_offline.html @@ -0,0 +1,6 @@ +{% load compress static %}{% spaceless %} + +{% compress js %} + + +{% endcompress %}{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_templatetag/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_templatetag/test_compressor_offline.html new file mode 100644 index 0000000..868f188 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_templatetag/test_compressor_offline.html @@ -0,0 +1,7 @@ +{% load compress %}{% spaceless %} + +{% compress js %} + +{% endcompress %}{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates/test_with_context/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_with_context/test_compressor_offline.html new file mode 100644 index 0000000..4970747 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates/test_with_context/test_compressor_offline.html @@ -0,0 +1,7 @@ +{% load compress %}{% spaceless %} + +{% compress js %} + +{% endcompress %}{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/basic/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/basic/test_compressor_offline.html new file mode 100644 index 0000000..6e89ed2 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/basic/test_compressor_offline.html @@ -0,0 +1,8 @@ +{% spaceless %} + +{% compress js %} + +{% endcompress %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super/base.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super/base.html new file mode 100644 index 0000000..e9ca3ad --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super/base.html @@ -0,0 +1,15 @@ +{% spaceless %} +{% block js %} + +{% endblock %} + +{% block css %} + +{% endblock %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super/test_compressor_offline.html new file mode 100644 index 0000000..e1fabd8 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super/test_compressor_offline.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block js %}{% spaceless %} + {% compress js %} + {{ super() }} + + {% endcompress %} +{% endspaceless %}{% endblock %} + +{% block css %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/base.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/base.html new file mode 100644 index 0000000..e9ca3ad --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/base.html @@ -0,0 +1,15 @@ +{% spaceless %} +{% block js %} + +{% endblock %} + +{% block css %} + +{% endblock %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/test_compressor_offline.html new file mode 100644 index 0000000..328ccb9 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/test_compressor_offline.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block js %}{% spaceless %} + {% compress js %} + + {% endcompress %} + + {% compress js %} + {{ super() }} + + {% endcompress %} +{% endspaceless %}{% endblock %} + +{% block css %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base.html new file mode 100644 index 0000000..c9ee6cc --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base.html @@ -0,0 +1,15 @@ +{% spaceless %} +{% block js %} + +{% endblock %} + +{% block css %} + +{% endblock %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base2.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base2.html new file mode 100644 index 0000000..b0b2fef --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base2.html @@ -0,0 +1,3 @@ +{% extends "base.html" %} + +{% block css %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/test_compressor_offline.html new file mode 100644 index 0000000..accd76d --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/test_compressor_offline.html @@ -0,0 +1,10 @@ +{% extends "base2.html" %} + +{% block js %}{% spaceless %} + {% compress js %} + {{ super() }} + + {% endcompress %} +{% endspaceless %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base.html new file mode 100644 index 0000000..c9ee6cc --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base.html @@ -0,0 +1,15 @@ +{% spaceless %} +{% block js %} + +{% endblock %} + +{% block css %} + +{% endblock %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base2.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base2.html new file mode 100644 index 0000000..b0b2fef --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base2.html @@ -0,0 +1,3 @@ +{% extends "base.html" %} + +{% block css %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/test_compressor_offline.html new file mode 100644 index 0000000..accd76d --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/test_compressor_offline.html @@ -0,0 +1,10 @@ +{% extends "base2.html" %} + +{% block js %}{% spaceless %} + {% compress js %} + {{ super() }} + + {% endcompress %} +{% endspaceless %}{% endblock %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_coffin/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_coffin/test_compressor_offline.html new file mode 100644 index 0000000..511ddd0 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_coffin/test_compressor_offline.html @@ -0,0 +1,11 @@ +{%- load compress -%} +{% spaceless %} + {% compress js%} + + {% with "js/one.js" as name -%} + + {%- endwith %} + {% endcompress %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_complex/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_complex/test_compressor_offline.html new file mode 100644 index 0000000..4707182 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_complex/test_compressor_offline.html @@ -0,0 +1,24 @@ +{% spaceless %} + +{% if condition %} + {% compress js%} + + {% with names=[] -%} + {%- do names.append("js/one.js") -%} + {%- do names.append("js/nonasc.js") -%} + {% for name in names -%} + + {%- endfor %} + {%- endwith %} + {% endcompress %} +{% endif %} +{% if not condition -%} + {% compress js %} + + {% endcompress %} +{%- else -%} + {% compress js %} + + {% endcompress %} +{%- endif %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_condition/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_condition/test_compressor_offline.html new file mode 100644 index 0000000..bd1adb8 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_condition/test_compressor_offline.html @@ -0,0 +1,7 @@ +{% spaceless %} + +{% if condition %} + {% compress js%} + + {% endcompress %} +{% endif %}{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_extends.html b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_extends.html new file mode 100644 index 0000000..72513f7 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_extends.html @@ -0,0 +1,9 @@ +{% extends "buggy_template.html" %} + +{% compress css %} + +{% endcompress %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_template.html b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_template.html new file mode 100644 index 0000000..a01b899 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_template.html @@ -0,0 +1,10 @@ +{% compress css %} + +{% endcompress %} + + +{% fail %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/missing_extends.html b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/missing_extends.html new file mode 100644 index 0000000..dc76034 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/missing_extends.html @@ -0,0 +1,9 @@ +{% extends "missing.html" %} + +{% compress css %} + +{% endcompress %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/test_compressor_offline.html new file mode 100644 index 0000000..3ecffa5 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/test_compressor_offline.html @@ -0,0 +1,8 @@ +{% spaceless %} + +{% compress js %} + +{% endcompress %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/with_coffeescript.html b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/with_coffeescript.html new file mode 100644 index 0000000..8a53e44 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/with_coffeescript.html @@ -0,0 +1,5 @@ +{% compress js %} + +{% endcompress %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_inline_non_ascii/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_inline_non_ascii/test_compressor_offline.html new file mode 100644 index 0000000..c03b191 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_inline_non_ascii/test_compressor_offline.html @@ -0,0 +1,7 @@ +{% spaceless %} + +{% compress js, inline %} + +{% endcompress %}{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_jingo/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_jingo/test_compressor_offline.html new file mode 100644 index 0000000..d79c797 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_jingo/test_compressor_offline.html @@ -0,0 +1,11 @@ +{% spaceless %} + {% compress js%} + + {% with name="js/one.js" -%} + + {%- endwith %} + {% endcompress %} +{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_static_templatetag/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_static_templatetag/test_compressor_offline.html new file mode 100644 index 0000000..ed7238c --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_static_templatetag/test_compressor_offline.html @@ -0,0 +1,6 @@ +{% spaceless %} + +{% compress js %} + + +{% endcompress %}{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_templatetag/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_templatetag/test_compressor_offline.html new file mode 100644 index 0000000..31c5d17 --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_templatetag/test_compressor_offline.html @@ -0,0 +1,7 @@ +{% spaceless %} + +{% compress js %} + +{% endcompress %}{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_with_context/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_with_context/test_compressor_offline.html new file mode 100644 index 0000000..2289a5f --- /dev/null +++ b/django-compressor/compressor/tests/test_templates_jinja2/test_with_context/test_compressor_offline.html @@ -0,0 +1,7 @@ +{% spaceless %} + +{% compress js %} + +{% endcompress %}{% endspaceless %} diff --git a/django-compressor/compressor/tests/test_templatetags.py b/django-compressor/compressor/tests/test_templatetags.py new file mode 100644 index 0000000..db0d1b7 --- /dev/null +++ b/django-compressor/compressor/tests/test_templatetags.py @@ -0,0 +1,256 @@ +from __future__ import with_statement, unicode_literals + +import os +import sys + +from mock import Mock + +from django.template import Template, Context, TemplateSyntaxError +from django.test import TestCase +from django.test.utils import override_settings + +from compressor.conf import settings +from compressor.signals import post_compress +from compressor.tests.test_base import css_tag, test_dir + + +def render(template_string, context_dict=None): + """ + A shortcut for testing template output. + """ + if context_dict is None: + context_dict = {} + c = Context(context_dict) + t = Template(template_string) + return t.render(c).strip() + + +class TemplatetagTestCase(TestCase): + def setUp(self): + self.old_enabled = settings.COMPRESS_ENABLED + settings.COMPRESS_ENABLED = True + self.context = {'STATIC_URL': settings.COMPRESS_URL} + + def tearDown(self): + settings.COMPRESS_ENABLED = self.old_enabled + + def test_empty_tag(self): + template = """{% load compress %}{% compress js %}{% block js %} + {% endblock %}{% endcompress %}""" + self.assertEqual('', render(template, self.context)) + + def test_css_tag(self): + template = """{% load compress %}{% compress css %} + + + +{% endcompress %}""" + out = css_tag("/static/CACHE/css/e41ba2cc6982.css") + self.assertEqual(out, render(template, self.context)) + + def test_uppercase_rel(self): + template = """{% load compress %}{% compress css %} + + + +{% endcompress %}""" + out = css_tag("/static/CACHE/css/e41ba2cc6982.css") + self.assertEqual(out, render(template, self.context)) + + def test_nonascii_css_tag(self): + template = """{% load compress %}{% compress css %} + + + {% endcompress %} + """ + out = css_tag("/static/CACHE/css/799f6defe43c.css") + self.assertEqual(out, render(template, self.context)) + + def test_js_tag(self): + template = """{% load compress %}{% compress js %} + + + {% endcompress %} + """ + out = '' + self.assertEqual(out, render(template, self.context)) + + def test_nonascii_js_tag(self): + template = """{% load compress %}{% compress js %} + + + {% endcompress %} + """ + out = '' + self.assertEqual(out, render(template, self.context)) + + def test_nonascii_latin1_js_tag(self): + template = """{% load compress %}{% compress js %} + + + {% endcompress %} + """ + out = '' + self.assertEqual(out, render(template, self.context)) + + def test_compress_tag_with_illegal_arguments(self): + template = """{% load compress %}{% compress pony %} + + {% endcompress %}""" + self.assertRaises(TemplateSyntaxError, render, template, {}) + + @override_settings(COMPRESS_DEBUG_TOGGLE='togglecompress') + def test_debug_toggle(self): + template = """{% load compress %}{% compress js %} + + + {% endcompress %} + """ + + class MockDebugRequest(object): + GET = {settings.COMPRESS_DEBUG_TOGGLE: 'true'} + + context = dict(self.context, request=MockDebugRequest()) + out = """ + """ + self.assertEqual(out, render(template, context)) + + def test_named_compress_tag(self): + template = """{% load compress %}{% compress js inline foo %} + + {% endcompress %} + """ + + def listener(sender, **kwargs): + pass + callback = Mock(wraps=listener) + post_compress.connect(callback) + render(template) + args, kwargs = callback.call_args + context = kwargs['context'] + self.assertEqual('foo', context['compressed']['name']) + + +class PrecompilerTemplatetagTestCase(TestCase): + def setUp(self): + self.old_enabled = settings.COMPRESS_ENABLED + self.old_precompilers = settings.COMPRESS_PRECOMPILERS + + precompiler = os.path.join(test_dir, 'precompiler.py') + python = sys.executable + + settings.COMPRESS_ENABLED = True + settings.COMPRESS_PRECOMPILERS = ( + ('text/coffeescript', '%s %s' % (python, precompiler)), + ('text/less', '%s %s' % (python, precompiler)), + ) + self.context = {'STATIC_URL': settings.COMPRESS_URL} + + def tearDown(self): + settings.COMPRESS_ENABLED = self.old_enabled + settings.COMPRESS_PRECOMPILERS = self.old_precompilers + + def test_compress_coffeescript_tag(self): + template = """{% load compress %}{% compress js %} + + {% endcompress %}""" + out = script(src="/static/CACHE/js/e920d58f166d.js") + self.assertEqual(out, render(template, self.context)) + + def test_compress_coffeescript_tag_and_javascript_tag(self): + template = """{% load compress %}{% compress js %} + + + {% endcompress %}""" + out = script(src="/static/CACHE/js/ef6b32a54575.js") + self.assertEqual(out, render(template, self.context)) + + @override_settings(COMPRESS_ENABLED=False) + def test_coffeescript_and_js_tag_with_compress_enabled_equals_false(self): + template = """{% load compress %}{% compress js %} + + + {% endcompress %}""" + out = (script('# this is a comment.\n') + '\n' + + script('# this too is a comment.')) + self.assertEqual(out, render(template, self.context)) + + @override_settings(COMPRESS_ENABLED=False) + def test_compress_coffeescript_tag_compress_enabled_is_false(self): + template = """{% load compress %}{% compress js %} + + {% endcompress %}""" + out = script("# this is a comment.\n") + self.assertEqual(out, render(template, self.context)) + + @override_settings(COMPRESS_ENABLED=False) + def test_compress_coffeescript_file_tag_compress_enabled_is_false(self): + template = """ + {% load compress %}{% compress js %} + + {% endcompress %}""" + + out = script(src="/static/CACHE/js/one.95cfb869eead.js") + self.assertEqual(out, render(template, self.context)) + + @override_settings(COMPRESS_ENABLED=False) + def test_multiple_file_order_conserved(self): + template = """ + {% load compress %}{% compress js %} + + + + {% endcompress %}""" + + out = '\n'.join([script(src="/static/CACHE/js/one.95cfb869eead.js"), + script(scripttype="", src="/static/js/one.js"), + script(src="/static/CACHE/js/one.81a2cd965815.js")]) + + self.assertEqual(out, render(template, self.context)) + + @override_settings(COMPRESS_ENABLED=False) + def test_css_multiple_files_disabled_compression(self): + assert(settings.COMPRESS_PRECOMPILERS) + template = """ + {% load compress %}{% compress css %} + + + {% endcompress %}""" + + out = ''.join(['', + '']) + + self.assertEqual(out, render(template, self.context)) + + @override_settings(COMPRESS_ENABLED=False) + def test_css_multiple_files_mixed_precompile_disabled_compression(self): + assert(settings.COMPRESS_PRECOMPILERS) + template = """ + {% load compress %}{% compress css %} + + + + {% endcompress %}""" + + out = ''.join(['', + '', + '']) + self.assertEqual(out, render(template, self.context)) + + +def script(content="", src="", scripttype="text/javascript"): + """ + returns a unicode text html script element. + + >>> script('#this is a comment', scripttype="text/applescript") + '' + """ + out_script = '' % content diff --git a/django-compressor/compressor/utils/__init__.py b/django-compressor/compressor/utils/__init__.py new file mode 100644 index 0000000..1c3479b --- /dev/null +++ b/django-compressor/compressor/utils/__init__.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +import os + +from django.utils import six + +from compressor.exceptions import FilterError + + +def get_class(class_string, exception=FilterError): + """ + Convert a string version of a function name to the callable object. + """ + if not hasattr(class_string, '__bases__'): + try: + class_string = str(class_string) + mod_name, class_name = get_mod_func(class_string) + if class_name: + return getattr(__import__(mod_name, {}, {}, [str('')]), class_name) + except (ImportError, AttributeError): + raise exception('Failed to import %s' % class_string) + + raise exception("Invalid class path '%s'" % class_string) + + +def get_mod_func(callback): + """ + Converts 'django.views.news.stories.story_detail' to + ('django.views.news.stories', 'story_detail') + """ + try: + dot = callback.rindex('.') + except ValueError: + return callback, '' + return callback[:dot], callback[dot + 1:] + + +def get_pathext(default_pathext=None): + """ + Returns the path extensions from environment or a default + """ + if default_pathext is None: + default_pathext = os.pathsep.join(['.COM', '.EXE', '.BAT', '.CMD']) + return os.environ.get('PATHEXT', default_pathext) + + +def find_command(cmd, paths=None, pathext=None): + """ + Searches the PATH for the given command and returns its path + """ + if paths is None: + paths = os.environ.get('PATH', '').split(os.pathsep) + if isinstance(paths, six.string_types): + paths = [paths] + # check if there are funny path extensions for executables, e.g. Windows + if pathext is None: + pathext = get_pathext() + pathext = [ext for ext in pathext.lower().split(os.pathsep)] + # don't use extensions if the command ends with one of them + if os.path.splitext(cmd)[1].lower() in pathext: + pathext = [''] + # check if we find the command on PATH + for path in paths: + # try without extension first + cmd_path = os.path.join(path, cmd) + for ext in pathext: + # then including the extension + cmd_path_ext = cmd_path + ext + if os.path.isfile(cmd_path_ext): + return cmd_path_ext + if os.path.isfile(cmd_path): + return cmd_path + return None diff --git a/django-compressor/compressor/utils/decorators.py b/django-compressor/compressor/utils/decorators.py new file mode 100644 index 0000000..f96c929 --- /dev/null +++ b/django-compressor/compressor/utils/decorators.py @@ -0,0 +1,64 @@ +class cached_property(object): + """Property descriptor that caches the return value + of the get function. + + *Examples* + + .. code-block:: python + + @cached_property + def connection(self): + return Connection() + + @connection.setter # Prepares stored value + def connection(self, value): + if value is None: + raise TypeError("Connection must be a connection") + return value + + @connection.deleter + def connection(self, value): + # Additional action to do at del(self.attr) + if value is not None: + print("Connection %r deleted" % (value, )) + """ + def __init__(self, fget=None, fset=None, fdel=None, doc=None): + self.__get = fget + self.__set = fset + self.__del = fdel + self.__doc__ = doc or fget.__doc__ + self.__name__ = fget.__name__ + self.__module__ = fget.__module__ + + def __get__(self, obj, type=None): + if obj is None: + return self + try: + return obj.__dict__[self.__name__] + except KeyError: + value = obj.__dict__[self.__name__] = self.__get(obj) + return value + + def __set__(self, obj, value): + if obj is None: + return self + if self.__set is not None: + value = self.__set(obj, value) + obj.__dict__[self.__name__] = value + + def __delete__(self, obj): + if obj is None: + return self + try: + value = obj.__dict__.pop(self.__name__) + except KeyError: + pass + else: + if self.__del is not None: + self.__del(obj, value) + + def setter(self, fset): + return self.__class__(self.__get, fset, self.__del) + + def deleter(self, fdel): + return self.__class__(self.__get, self.__set, fdel) diff --git a/django-compressor/compressor/utils/staticfiles.py b/django-compressor/compressor/utils/staticfiles.py new file mode 100644 index 0000000..28026f2 --- /dev/null +++ b/django-compressor/compressor/utils/staticfiles.py @@ -0,0 +1,27 @@ +from __future__ import absolute_import, unicode_literals + +from django.core.exceptions import ImproperlyConfigured + +from compressor.conf import settings + +INSTALLED = ("staticfiles" in settings.INSTALLED_APPS or + "django.contrib.staticfiles" in settings.INSTALLED_APPS) + +if INSTALLED: + if "django.contrib.staticfiles" in settings.INSTALLED_APPS: + from django.contrib.staticfiles import finders + else: + try: + from staticfiles import finders # noqa + except ImportError: + # Old (pre 1.0) and incompatible version of staticfiles + INSTALLED = False + + if (INSTALLED and "compressor.finders.CompressorFinder" + not in settings.STATICFILES_FINDERS): + raise ImproperlyConfigured( + "When using Django Compressor together with staticfiles, " + "please add 'compressor.finders.CompressorFinder' to the " + "STATICFILES_FINDERS setting.") +else: + finders = None # noqa diff --git a/django-compressor/compressor/utils/stringformat.py b/django-compressor/compressor/utils/stringformat.py new file mode 100644 index 0000000..9311e78 --- /dev/null +++ b/django-compressor/compressor/utils/stringformat.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- +"""Advanced string formatting for Python >= 2.4. + +An implementation of the advanced string formatting (PEP 3101). + +Author: Florent Xicluna +""" + +from __future__ import unicode_literals + +import re + +from django.utils import six + +_format_str_re = re.compile( + r'((?=^])?)' # alignment + r'([-+ ]?)' # sign + r'(#?)' r'(\d*)' r'(,?)' # base prefix, minimal width, thousands sep + r'((?:\.\d+)?)' # precision + r'(.?)$' # type +) +_field_part_re = re.compile( + r'(?:(\[)|\.|^)' # start or '.' or '[' + r'((?(1)[^]]*|[^.[]*))' # part + r'(?(1)(?:\]|$)([^.[]+)?)' # ']' and invalid tail +) + +_format_str_sub = _format_str_re.sub + + +def _is_integer(value): + return hasattr(value, '__index__') + + +def _strformat(value, format_spec=""): + """Internal string formatter. + + It implements the Format Specification Mini-Language. + """ + m = _format_spec_re.match(str(format_spec)) + if not m: + raise ValueError('Invalid conversion specification') + align, sign, prefix, width, comma, precision, conversion = m.groups() + is_numeric = hasattr(value, '__float__') + is_integer = is_numeric and _is_integer(value) + if prefix and not is_integer: + raise ValueError('Alternate form (#) not allowed in %s format ' + 'specifier' % (is_numeric and 'float' or 'string')) + if is_numeric and conversion == 'n': + # Default to 'd' for ints and 'g' for floats + conversion = is_integer and 'd' or 'g' + elif sign: + if not is_numeric: + raise ValueError("Sign not allowed in string format specifier") + if conversion == 'c': + raise ValueError("Sign not allowed with integer " + "format specifier 'c'") + if comma: + # TODO: thousand separator + pass + try: + if ((is_numeric and conversion == 's') or (not is_integer and conversion in set('cdoxX'))): + raise ValueError + if conversion == 'c': + conversion = 's' + value = chr(value % 256) + rv = ('%' + prefix + precision + (conversion or 's')) % (value,) + except ValueError: + raise ValueError("Unknown format code %r for object of type %r" % + (conversion, value.__class__.__name__)) + if sign not in '-' and value >= 0: + # sign in (' ', '+') + rv = sign + rv + if width: + zero = (width[0] == '0') + width = int(width) + else: + zero = False + width = 0 + # Fastpath when alignment is not required + if width <= len(rv): + if not is_numeric and (align == '=' or (zero and not align)): + raise ValueError("'=' alignment not allowed in string format " + "specifier") + return rv + fill, align = align[:-1], align[-1:] + if not fill: + fill = zero and '0' or ' ' + if align == '^': + padding = width - len(rv) + # tweak the formatting if the padding is odd + if padding % 2: + rv += fill + rv = rv.center(width, fill) + elif align == '=' or (zero and not align): + if not is_numeric: + raise ValueError("'=' alignment not allowed in string format " + "specifier") + if value < 0 or sign not in '-': + rv = rv[0] + rv[1:].rjust(width - 1, fill) + else: + rv = rv.rjust(width, fill) + elif align in ('>', '=') or (is_numeric and not align): + # numeric value right aligned by default + rv = rv.rjust(width, fill) + else: + rv = rv.ljust(width, fill) + return rv + + +def _format_field(value, parts, conv, spec, want_bytes=False): + """Format a replacement field.""" + for k, part, _ in parts: + if k: + if part.isdigit(): + value = value[int(part)] + else: + value = value[part] + else: + value = getattr(value, part) + if conv: + value = ((conv == 'r') and '%r' or '%s') % (value,) + if hasattr(value, '__format__'): + value = value.__format__(spec) + elif hasattr(value, 'strftime') and spec: + value = value.strftime(str(spec)) + else: + value = _strformat(value, spec) + if want_bytes and isinstance(value, six.text_type): + return str(value) + return value + + +class FormattableString(object): + """Class which implements method format(). + + The method format() behaves like str.format() in python 2.6+. + + >>> FormattableString('{a:5}').format(a=42) + ... # Same as '{a:5}'.format(a=42) + ' 42' + + """ + + __slots__ = '_index', '_kwords', '_nested', '_string', 'format_string' + + def __init__(self, format_string): + self._index = 0 + self._kwords = {} + self._nested = {} + + self.format_string = format_string + self._string = _format_str_sub(self._prepare, format_string) + + def __eq__(self, other): + if isinstance(other, FormattableString): + return self.format_string == other.format_string + # Compare equal with the original string. + return self.format_string == other + + def _prepare(self, match): + # Called for each replacement field. + part = match.group(0) + if part[0] == part[-1]: + # '{{' or '}}' + assert part == part[0] * len(part) + return part[:len(part) // 2] + repl = part[1:-1] + field, _, format_spec = repl.partition(':') + literal, sep, conversion = field.partition('!') + if sep and not conversion: + raise ValueError("end of format while looking for " + "conversion specifier") + if len(conversion) > 1: + raise ValueError("expected ':' after format specifier") + if conversion not in 'rsa': + raise ValueError("Unknown conversion specifier %s" % + str(conversion)) + name_parts = _field_part_re.findall(literal) + if literal[:1] in '.[': + # Auto-numbering + if self._index is None: + raise ValueError("cannot switch from manual field " + "specification to automatic field numbering") + name = str(self._index) + self._index += 1 + if not literal: + del name_parts[0] + else: + name = name_parts.pop(0)[1] + if name.isdigit() and self._index is not None: + # Manual specification + if self._index: + raise ValueError("cannot switch from automatic field " + "numbering to manual field specification") + self._index = None + empty_attribute = False + for k, v, tail in name_parts: + if not v: + empty_attribute = True + if tail: + raise ValueError("Only '.' or '[' may follow ']' " + "in format field specifier") + if name_parts and k == '[' and not literal[-1] == ']': + raise ValueError("Missing ']' in format string") + if empty_attribute: + raise ValueError("Empty attribute in format string") + if '{' in format_spec: + format_spec = _format_sub_re.sub(self._prepare, format_spec) + rv = (name_parts, conversion, format_spec) + self._nested.setdefault(name, []).append(rv) + else: + rv = (name_parts, conversion, format_spec) + self._kwords.setdefault(name, []).append(rv) + return r'%%(%s)s' % id(rv) + + def format(self, *args, **kwargs): + """Same as str.format() and unicode.format() in Python 2.6+.""" + if args: + kwargs.update(dict((str(i), value) + for (i, value) in enumerate(args))) + # Encode arguments to ASCII, if format string is bytes + want_bytes = isinstance(self._string, str) + params = {} + for name, items in self._kwords.items(): + value = kwargs[name] + for item in items: + parts, conv, spec = item + params[str(id(item))] = _format_field(value, parts, conv, spec, + want_bytes) + for name, items in self._nested.items(): + value = kwargs[name] + for item in items: + parts, conv, spec = item + spec = spec % params + params[str(id(item))] = _format_field(value, parts, conv, spec, + want_bytes) + return self._string % params + + +def selftest(): + import datetime + F = FormattableString + + assert F("{0:{width}.{precision}s}").format('hello world', + width=8, precision=5) == 'hello ' + + d = datetime.date(2010, 9, 7) + assert F("The year is {0.year}").format(d) == "The year is 2010" + assert F("Tested on {0:%Y-%m-%d}").format(d) == "Tested on 2010-09-07" + print('Test successful') + +if __name__ == '__main__': + selftest() diff --git a/django-compressor/django_compressor.egg-info/PKG-INFO b/django-compressor/django_compressor.egg-info/PKG-INFO new file mode 100644 index 0000000..19e5fea --- /dev/null +++ b/django-compressor/django_compressor.egg-info/PKG-INFO @@ -0,0 +1,104 @@ +Metadata-Version: 1.1 +Name: django-compressor +Version: 1.4 +Summary: Compresses linked and inline JavaScript or CSS into single cached files. +Home-page: http://django-compressor.readthedocs.org/en/latest/ +Author: Jannis Leidel +Author-email: jannis@leidel.info +License: MIT +Description: Django Compressor + ================= + + .. image:: https://coveralls.io/repos/django-compressor/django-compressor/badge.png?branch=develop + :target: https://coveralls.io/r/django-compressor/django-compressor?branch=develop + + .. image:: https://pypip.in/v/django_compressor/badge.png + :target: https://pypi.python.org/pypi/django_compressor + + .. image:: https://pypip.in/d/django_compressor/badge.png + :target: https://pypi.python.org/pypi/django_compressor + + .. image:: https://secure.travis-ci.org/django-compressor/django-compressor.png?branch=develop + :alt: Build Status + :target: http://travis-ci.org/django-compressor/django-compressor + + Django Compressor combines and compresses linked and inline Javascript + or CSS in a Django template into cacheable static files by using the + ``compress`` template tag. + + HTML in between ``{% compress js/css %}`` and ``{% endcompress %}`` is + parsed and searched for CSS or JS. These styles and scripts are subsequently + processed with optional, configurable compilers and filters. + + The default filter for CSS rewrites paths to static files to be absolute + and adds a cache busting timestamp. For Javascript the default filter + compresses it using ``jsmin``. + + As the final result the template tag outputs a `` + + The new ``compressor/js_file.html``:: + + + +- Removed old templates named ``compressor/css.html`` and + ``compressor/js.html`` that were originally left for backwards + compatibility. If you've overridden them, just rename them to + ``compressor/css_file.html`` or ``compressor/js_file.html`` and + make sure you've accounted for the backwards incompatible change + of the template context mentioned above. + +- Reverted an unfortunate change to the YUI filter that prepended + ``'java -jar'`` to the binary name, which doesn't alway work, e.g. + if the YUI compressor is shipped as a script like + ``/usr/bin/yui-compressor``. + +- Changed the sender parameter of the :func:`~compressor.signals.post_compress` + signal to be either :class:`compressor.css.CssCompressor` or + :class:`compressor.js.JsCompressor` for easier customization. + +- Correctly handle offline compressing files that are found in ``{% if %}`` + template blocks. + +- Renamed the second option for the ``COMPRESS_CSS_HASHING_METHOD`` setting + from ``'hash'`` to ``'content'`` to better describe what it does. The old + name is also supported, as well as the default being ``'mtime'``. + +- Fixed CssAbsoluteFilter, ``src`` attributes in includes now get transformed. + +- Added a new hook to allow developers to completely bypass offline + compression in CompressorNode subclasses: ``is_offline_compression_enabled``. + +- Dropped versiontools from required dependencies again. + +v1.1.2 +------ + +- Fixed an installation issue related to versiontools. + +v1.1.1 +------ + +- Fixed a stupid ImportError bug introduced in 1.1. + +- Fixed Jinja2 docs of since ``JINJA2_EXTENSIONS`` expects + a class, not a module. + +- Fixed a Windows bug with regard to file resolving with + staticfiles finders. + +- Stopped a potential memory leak when memoizing the rendered + output. + +- Fixed the integration between staticfiles (e.g. in Django <= 1.3.1) + and compressor which prevents the collectstatic management command + to work. + + .. warning:: + + Make sure to **remove** the ``path`` method of your custom + :ref:`remote storage ` class! + +v1.1 +---- + +- Made offline compression completely independent from cache (by writing a + manifest.json file). + + You can now easily run the :ref:`compress ` management + command locally and transfer the :attr:`~django.conf.settings.COMPRESS_ROOT` + dir to your server. + +- Updated installation instructions to properly mention all dependencies, + even those internally used. + +- Fixed a bug introduced in 1.0 which would prevent the proper deactivation + of the compression in production. + +- Added a Jinja2_ :doc:`contrib extension `. + +- Made sure the rel attribute of link tags can be mixed case. + +- Avoid overwriting context variables needed for compressor to work. + +- Stopped the compress management command to require model validation. + +- Added missing imports and fixed a few :pep:`8` issues. + +.. _Jinja2: http://jinja.pocoo.org/2/ + +v1.0.1 +------ + +- Fixed regression in ``compressor.utils.staticfiles`` compatibility + module. + +v1.0 +---- + +- **BACKWARDS-INCOMPATIBLE** Stopped swallowing exceptions raised by + rendering the template tag in production (``DEBUG = False``). This + has the potential to breaking lots of apps but on the other hand + will help find bugs. + +- **BACKWARDS-INCOMPATIBLE** The default function to create the cache + key stopped containing the server hostname. Instead the cache key + now only has the form ``'django_compressor.'``. + + To revert to the previous way simply set the ``COMPRESS_CACHE_KEY_FUNCTION`` + to ``'compressor.cache.socket_cachekey'``. + +- **BACKWARDS-INCOMPATIBLE** Renamed ambigously named + ``COMPRESS_DATA_URI_MAX_SIZE`` setting to ``COMPRESS_DATA_URI_MAX_SIZE``. + It's the maximum size the ``compressor.filters.datauri.DataUriFilter`` + filter will embed files as data: URIs. + +- Added ``COMPRESS_CSS_HASHING_METHOD`` setting with the options ``'mtime'`` + (default) and ``'hash'`` for the ``CssAbsoluteFilter`` filter. The latter + uses the content of the file to calculate the cache-busting hash. + +- Added support for ``{{ block.super }}`` to ``compress`` management command. + +- Dropped Django 1.1.X support. + +- Fixed compiler filters on Windows. + +- Handle new-style cached template loaders in the compress management command. + +- Documented included filters. + +- Added `Slim It`_ filter. + +- Added new CallbackOutputFilter to ease the implementation of Python-based + callback filters that only need to pass the content to a callable. + +- Make use of `django-appconf`_ for settings handling and `versiontools`_ + for versions. + +- Uses the current context when rendering the render templates. + +- Added :func:`post_compress` signal. + +.. _`Slim It`: http://slimit.org/ +.. _`django-appconf`: http://django-appconf.rtfd.org/ +.. _`versiontools`: http://pypi.python.org/pypi/versiontools + +v0.9.2 +------ + +- Fixed stdin handling of precompiler filter. + +v0.9.1 +------ + +- Fixed encoding related issue. + +- Minor cleanups. + +v0.9 +---- + +- Fixed the precompiler support to also use the full file path instead of a + temporarily created file. + +- Enabled test coverage. + +- Refactored caching and other utility code. + +- Switched from SHA1 to MD5 for hash generation to lower the computational impact. + +v0.8 +---- + +- Replace naive jsmin.py with rJSmin (http://opensource.perlig.de/rjsmin/) + and fixed a few problems with JavaScript comments. + +- Fixed converting relative URLs in CSS files when running in debug mode. + +.. note:: + + If you relied on the ``split_contents`` method of ``Compressor`` classes, + please make sure a fourth item is returned in the iterable that denotes + the base name of the file that is compressed. + +v0.7.1 +------ + +- Fixed import error when using the standalone django-staticfiles app. + +v0.7 +---- + +- Created new parser, HtmlParser, based on the stdlib HTMLParser module. + +- Added a new default AutoSelectParser, which picks the LxmlParser if lxml + is available and falls back to HtmlParser. + +- Use unittest2 for testing goodness. + +- Fixed YUI JavaScript filter argument handling. + +- Updated bundled jsmin to use version by Dave St.Germain that was refactored for speed. + +v0.6.4 +------ + +- Fixed Closure filter argument handling. + +v0.6.3 +------ + +- Fixed options mangling in CompilerFilter initialization. + +- Fixed tox configuration. + +- Extended documentation and README. + +- In the compress command ignore hidden files when looking for templates. + +- Restructured utilities and added staticfiles compat layer. + +- Restructered parsers and added a html5lib based parser. + +v0.6.2 +------ + +- Minor bugfixes that caused the compression not working reliably in + development mode (e.g. updated files didn't trigger a new compression). + +v0.6.1 +------ + +- Fixed staticfiles support to also use its finder API to find files during + developement -- when the static files haven't been collected in + ``STATIC_ROOT``. + +- Fixed regression with the ``COMPRESS`` setting, pre-compilation and + staticfiles. + +v0.6 +---- + +Major improvements and a lot of bugfixes, some of which are: + +- New precompilation support, which allows compilation of files and + hunks with easily configurable compilers before calling the actual + output filters. See the + :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS` for more details. + +- New staticfiles support. With the introduction of the staticfiles app + to Django 1.3, compressor officially supports finding the files to + compress using the app's finder API. Have a look at the documentation + about :ref:`remote storages ` in case you want to use + those together with compressor. + +- New ``compress`` management command which allows pre-running of what the + compress template tag does. See the + :ref:`pre-compression ` docs for more information. + +- Various perfomance improvements by better caching and mtime cheking. + +- Deprecated ``COMPRESS_LESSC_BINARY`` setting because it's now + superseded by the :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS` + setting. Just make sure to use the correct mimetype when linking to less + files or adding inline code and add the following to your settings:: + + COMPRESS_PRECOMPILERS = ( + ('text/less', 'lessc {infile} {outfile}'), + ) + +- Added cssmin_ filter (``compressor.filters.CSSMinFilter``) based on + Zachary Voase's Python port of the YUI CSS compression algorithm. + +- Reimplemented the dog-piling prevention. + +- Make sure the CssAbsoluteFilter works for relative paths. + +- Added inline render mode. See :ref:`usage ` docs. + +- Added ``mtime_cache`` management command to add and/or remove all mtimes + from the cache. + +- Moved docs to Read The Docs: http://django-compressor.readthedocs.org/en/latest/ + +- Added optional ``compressor.storage.GzipCompressorFileStorage`` storage + backend that gzips of the saved files automatically for easier deployment. + +- Reimplemented a few filters on top of the new + ``compressor.filters.base.CompilerFilter`` to be a bit more DRY. + +- Added tox based test configuration, testing on Django 1.1-1.3 and Python + 2.5-2.7. + +.. _cssmin: http://pypi.python.org/pypi/cssmin/ + diff --git a/django-compressor/docs/conf.py b/django-compressor/docs/conf.py new file mode 100644 index 0000000..34552c3 --- /dev/null +++ b/django-compressor/docs/conf.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +# +# django-compressor documentation build configuration file, created by +# sphinx-quickstart on Fri Jan 21 11:47:42 2011. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('..')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +# extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.txt' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Django Compressor' +copyright = u'2014, Django Compressor authors' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +try: + from compressor import __version__ + # The short X.Y version. + version = '.'.join(__version__.split('.')[:2]) + # The full version, including alpha/beta/rc tags. + release = __version__ +except ImportError: + version = release = 'dev' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'murphy' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# html_theme = 'default' +RTD_NEW_THEME = True + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = ['_theme'] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'django-compressordoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'django-compressor.tex', u'Django Compressor Documentation', + u'Django Compressor authors', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'django-compressor', u'Django Compressor Documentation', + [u'Django Compressor authors'], 1) +] diff --git a/django-compressor/docs/contributing.txt b/django-compressor/docs/contributing.txt new file mode 100644 index 0000000..225a1ae --- /dev/null +++ b/django-compressor/docs/contributing.txt @@ -0,0 +1,174 @@ +Contributing +============ + +Like every open-source project, Django Compressor is always looking for +motivated individuals to contribute to it's source code. +However, to ensure the highest code quality and keep the repository nice and +tidy, everybody has to follow a few rules (nothing major, I promise :) ) + +Community +--------- + +People interested in developing for the Django Compressor should head +over to #django-compressor on the `freenode`_ IRC network for help and to +discuss the development. + +You may also be interested in following `@jezdez`_ on Twitter. + +In a nutshell +------------- + +Here's what the contribution process looks like, in a bullet-points fashion, +and only for the stuff we host on github: + +#. Django Compressor is hosted on `github`_, at https://github.com/django-compressor/django-compressor +#. The best method to contribute back is to create a github account, then fork + the project. You can use this fork as if it was your own project, and should + push your changes to it. +#. When you feel your code is good enough for inclusion, "send us a `pull + request`_", by using the nice github web interface. + +Contributing Code +----------------- + +Getting the source code +^^^^^^^^^^^^^^^^^^^^^^^ + +If you're interested in developing a new feature for Compressor, it is +recommended that you first discuss it on IRC not to do any work that will +not get merged in anyway. + +- Code will be reviewed and tested by at least one core developer, preferably + by several. Other community members are welcome to give feedback. +- Code *must* be tested. Your pull request should include unit-tests (that + cover the piece of code you're submitting, obviously) +- Documentation should reflect your changes if relevant. There is nothing + worse than invalid documentation. +- Usually, if unit tests are written, pass, and your change is relevant, then + it'll be merged. + +Since it's hosted on github, Django Compressor uses `git`_ as a version +control system. + +The `github help`_ is very well written and will get you started on using git +and github in a jiffy. It is an invaluable resource for newbies and old timers +alike. + +Syntax and conventions +^^^^^^^^^^^^^^^^^^^^^^ + +We try to conform to `PEP8`_ as much as possible. A few highlights: + +- Indentation should be exactly 4 spaces. Not 2, not 6, not 8. **4**. Also, + tabs are evil. +- We try (loosely) to keep the line length at 79 characters. Generally the + rule is "it should look good in a terminal-base editor" (eg vim), but we + try not be [Godwin's law] about it. + +Process +^^^^^^^ + +This is how you fix a bug or add a feature: + +#. `Fork`_ us on github. +#. Checkout your fork. +#. Hack hack hack, test test test, commit commit commit, test again. +#. Push to your fork. +#. Open a pull request. + +Tests +^^^^^ + +Having a wide and comprehensive library of unit-tests and integration tests is +of exceeding importance. Contributing tests is widely regarded as a very +prestigious contribution (you're making everybody's future work much easier by +doing so). Good karma for you. Cookie points. Maybe even a beer if we meet in +person :) + +Generally tests should be: + +- Unitary (as much as possible). I.E. should test as much as possible only one + function/method/class. That's the very definition of unit tests. + +- Integration tests are interesting too obviously, but require more time to + maintain since they have a higher probability of breaking. + +- Short running. No hard numbers here, but if your one test doubles the time + it takes for everybody to run them, it's probably an indication that you're + doing it wrong. + +In a similar way to code, pull requests will be reviewed before pulling +(obviously), and we encourage discussion via code review (everybody learns +something this way) or IRC discussions. + +Running the tests +^^^^^^^^^^^^^^^^^ + +To run the tests simply fork django_compressor, make the changes and open +a pull request. The Travis_ bot will automatically run the tests of your +branch/fork (see the `pull request announcment`_ for more info) and add a +comment about the test results to the pull requests. Alternatively you +can also login at Travis and enable your fork to run there, too. See the +`Travis documentation`_ to read about how to do that. + +Alternatively, create a virtualenv and activate it, then install the +requirements **in the virtualenv**:: + + $ virtualenv compressor_test + $ source compressor_test/bin/activate + (compressor_test) $ make testenv + +Then run ``make test`` to run the tests. Please note that this only tests +django_compressor in the Python version you've created the virtualenv with +not all the versions that are required to be supported. + +Contributing Documentation +-------------------------- + +Perhaps considered "boring" by hard-core coders, documentation is sometimes +even more important than code! This is what brings fresh blood to a project, +and serves as a reference for old timers. On top of this, documentation is +the one area where less technical people can help most - you just need to +write a semi-decent English. People need to understand you. + +Documentation should be: + +- We use `Sphinx`_/`restructuredText`_. So obviously this is the format you + should use :) File extensions should be ``.txt``. + +- Written in English. We can discuss how it would bring more people to the + project to have a Klingon translation or anything, but that's a problem we + will ask ourselves when we already have a good documentation in English. + +- Accessible. You should assume the reader to be moderately familiar with + Python and Django, but not anything else. Link to documentation of libraries + you use, for example, even if they are "obvious" to you. A brief + description of what it does is also welcome. + +Pulling of documentation is pretty fast and painless. Usually somebody goes +over your text and merges it, since there are no "breaks" and that github +parses rst files automagically it's really convenient to work with. + +Also, contributing to the documentation will earn you great respect from the +core developers. You get good karma just like a test contributor, but you get +double cookie points. Seriously. You rock. + +.. note:: + + This very document is based on the contributing docs of the + `django CMS`_ project. Many thanks for allowing us to steal it! + +.. _Fork: http://github.com/django-compressor/django-compressor +.. _Travis: http://travis-ci.org/ +.. _`pull request announcment`: http://about.travis-ci.org/blog/announcing-pull-request-support/ +.. _`Travis documentation`: http://about.travis-ci.org/docs/ +.. _Sphinx: http://sphinx.pocoo.org/ +.. _PEP8: http://www.python.org/dev/peps/pep-0008/ +.. _github : http://www.github.com +.. _github help : http://help.github.com +.. _freenode : http://freenode.net/ +.. _@jezdez : https://twitter.com/jezdez +.. _pull request : http://help.github.com/send-pull-requests/ +.. _git : http://git-scm.com/ +.. _restructuredText: http://docutils.sourceforge.net/docs/ref/rst/introduction.html +.. _`django CMS`: http://www.django-cms.org/ diff --git a/django-compressor/docs/django-sekizai.txt b/django-compressor/docs/django-sekizai.txt new file mode 100644 index 0000000..6fd80c9 --- /dev/null +++ b/django-compressor/docs/django-sekizai.txt @@ -0,0 +1,24 @@ +.. _django-sekizai_support: + +django-sekizai Support +====================== + +Django Compressor comes with support for _django-sekizai via an extension. +_django-sekizai provides the ability to include template code, from within +any block, to a parent block. It is primarily used to include js/css from +included templates to the master template. + +It requires _django-sekizai to installed. Refer to the _django-sekizai _docs +for how to use ``render_block`` + +Usage +----- + +.. code-block:: django + + {% load sekizai_tags %} + {% render_block "" postprocessor "compressor.contrib.sekizai.compress" %} + + +.. _django-sekizai: https://github.com/ojii/django-sekizai +.. _docs: http://django-sekizai.readthedocs.org/en/latest/ diff --git a/django-compressor/docs/index.txt b/django-compressor/docs/index.txt new file mode 100644 index 0000000..f1adfa4 --- /dev/null +++ b/django-compressor/docs/index.txt @@ -0,0 +1,47 @@ +================= +Django Compressor +================= + +Compresses linked and inline JavaScript or CSS into a single cached file. + +Why another static file combiner for Django? +============================================ + +Short version: None of them did exactly what I needed. + +Long version: + +**JS/CSS belong in the templates** + Every static combiner for Django I've seen makes you configure + your static files in your ``settings.py``. While that works, it doesn't make + sense. Static files are for display. And it's not even an option if your + settings are in completely different repositories and use different deploy + processes from the templates that depend on them. + +**Flexibility** + Django Compressor doesn't care if different pages use different combinations + of statics. It doesn't care if you use inline scripts or styles. It doesn't + get in the way. + +**Automatic regeneration and cache-foreverable generated output** + Statics are never stale and browsers can be told to cache the output forever. + +**Full test suite** + I has one. + +Contents +======== + +.. toctree:: + :maxdepth: 2 + + quickstart + usage + scenarios + settings + remote-storages + behind-the-scenes + jinja2 + django-sekizai + contributing + changelog diff --git a/django-compressor/docs/jinja2.txt b/django-compressor/docs/jinja2.txt new file mode 100644 index 0000000..134b0b8 --- /dev/null +++ b/django-compressor/docs/jinja2.txt @@ -0,0 +1,175 @@ +Jinja2 In-Request Support +========================= + +Django Compressor comes with support for Jinja2_ via an extension. + +Plain Jinja2 +------------ + +In order to use Django Compressor's Jinja2 extension we would need to pass +``compressor.contrib.jinja2ext.CompressorExtension`` into environment:: + + import jinja2 + from compressor.contrib.jinja2ext import CompressorExtension + + env = jinja2.Environment(extensions=[CompressorExtension]) + +From now on, you can use same code you'd normally use within Django templates:: + + from django.conf import settings + template = env.from_string('\n'.join([ + '{% compress css %}', + '', + '{% endcompress %}', + ])) + template.render({'STATIC_URL': settings.STATIC_URL}) + +For coffin users +---------------- + +Coffin_ makes it very easy to include additional Jinja2_ extensions as it +only requires to add extension to ``JINJA2_EXTENSIONS`` at main settings +module:: + + JINJA2_EXTENSIONS = [ + 'compressor.contrib.jinja2ext.CompressorExtension', + ] + +And that's it - our extension is loaded and ready to be used. + + +Jinja2 Offline Compression Support +================================== +You'd need to configure ``COMPRESS_JINJA2_GET_ENVIRONMENT`` so that +Compressor can retrieve the Jinja2 environment for rendering. +This can be a lamda or function that returns a Jinja2 environment. + +Usage +----- +Run the following compress command along with an ``-engine`` parameter. The +parameter can be either jinja2 or django (default). For example, +"./manage.py compress -engine jinja2". + +Using both Django and Jinja2 templates +-------------------------------------- +There may be a chance that the Jinja2 parser is used to parse Django templates +if you have a mixture of Django and Jinja2 templates in the same location(s). +This should not be a problem since the Jinja2 parser will likely raise a +template syntax error, causing Compressor to skip the errorneous +template safely. (Vice versa for Django parser). + +A typical usage could be : + +- "./manage.py compress" for processing Django templates first, skipping + Jinja2 templates. +- "./manage.py compress -engine jinja2" for processing Jinja2 templates, + skipping Django templates. + +However, it is still recommended that you do not mix Django and Jinja2 +templates in the same project. + +Limitations +----------- +- Does not support ``{% import %}`` and similar blocks within + ``{% compress %}`` blocks. +- Does not support ``{{super()}}``. +- All other filters, globals and language constructs such as + ``{% if %}``, ``{% with %}`` and ``{% for %}`` are tested and + should run fine. + +Jinja2 templates location +------------------------- +IMPORTANT: For Compressor to discover the templates for offline compression, +there must be a template loader that implements the ``get_template_sources`` +method, and is in the ``TEMPLATE_LOADERS`` setting. + +If you're using Jinja2, you're likely to have a Jinja2 template loader in the +``TEMPLATE_LOADERS`` setting, otherwise Django won't know how to load Jinja2 +templates. You could use Jingo_ or your own custom loader. Coffin_ works +differently by providing a custom rendering method instead of a custom loader. + +Unfortunately, Jingo_ does not implement such a method in its loader; +Coffin_ does not seem to have a template loader in the first place. +Read on to understand how to make Compressor work nicely with Jingo_ +and Coffin_. + +By default, if you don't override the ``TEMPLATE_LOADERS`` setting, +it will include the app directories loader that searches for templates under +the ``templates`` directory in each app. If the app directories loader is in use +and your Jinja2 templates are in the ``/templates`` directories, +Compressor will be able to find the Jinja2 templates. + +However, if you have Jinja2 templates in other location(s), you could include +the filesystem loader (``django.template.loaders.filesystem.Loader``) in the +``TEMPLATE_LOADERS`` setting and specify the custom location in the +``TEMPLATE_DIRS`` setting. + +For Jingo users +--------------- +You should configure ``TEMPLATE_LOADERS`` as such:: + + TEMPLATE_LOADERS = ( + 'jingo.Loader', + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ) + + def COMPRESS_JINJA2_GET_ENVIRONMENT(): + # TODO: ensure the CompressorExtension is installed with Jingo via + # Jingo's JINJA_CONFIG setting. + # Additional globals, filters, tests, + # and extensions used within {%compress%} blocks must be configured + # with Jingo. + from jingo import env + + return env + +This will enable the Jingo_ loader to load Jinja2 templates and the other +loaders to report the templates location(s). + +For Coffin users +---------------- +You might want to configure ``TEMPLATE_LOADERS`` as such:: + + TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ) + + def COMPRESS_JINJA2_GET_ENVIRONMENT(): + # TODO: ensure the CompressorExtension is installed with Coffin + # as described in the "In-Request Support" section above. + # Additional globals, filters, tests, + # and extensions used within {%compress%} blocks must be configured + # with Coffin. + from coffin.common import env + + return env + +Again, if you have the Jinja2 templates in the app template directories, you're +done here. Otherwise, specify the location in ``TEMPLATE_DIRS``. + +Using your custom loader +------------------------ +You should configure ``TEMPLATE_LOADERS`` as such:: + + TEMPLATE_LOADERS = ( + 'your_app.Loader', + ... other loaders (optional) ... + ) + +You could implement the `get_template_sources` method in your loader or make +use of the Django's builtin loaders to report the Jinja2 template location(s). + +Python 3 Support +---------------- +Jingo with Jinja2 are tested and work on Python 2.6, 2.7, and 3.3. +Coffin with Jinja2 are tested and work on Python 2.6 and 2.7 only. +Jinja2 alone (with custom loader) are tested and work on Python 2.6, 2.7 and +3.3 only. + + +.. _Jinja2: http://jinja.pocoo.org/docs/ +.. _Coffin: http://pypi.python.org/pypi/Coffin +.. _Jingo: https://jingo.readthedocs.org/en/latest/ + diff --git a/django-compressor/docs/make.bat b/django-compressor/docs/make.bat new file mode 100644 index 0000000..aa5c189 --- /dev/null +++ b/django-compressor/docs/make.bat @@ -0,0 +1,170 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-compressor.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-compressor.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/django-compressor/docs/quickstart.txt b/django-compressor/docs/quickstart.txt new file mode 100644 index 0000000..4acfab2 --- /dev/null +++ b/django-compressor/docs/quickstart.txt @@ -0,0 +1,100 @@ +Quickstart +========== + +Installation +------------ + +* Install Django Compressor with your favorite Python package manager:: + + pip install django_compressor + +* Add ``'compressor'`` to your ``INSTALLED_APPS`` setting:: + + INSTALLED_APPS = ( + # other apps + "compressor", + ) + +* See the list of :ref:`settings` to modify Django Compressor's + default behaviour and make adjustments for your website. + +* In case you use Django's staticfiles_ contrib app (or its standalone + counterpart django-staticfiles_) you have to add Django Compressor's file + finder to the ``STATICFILES_FINDERS`` setting, for example with + ``django.contrib.staticfiles``: + + .. code-block:: python + + STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + # other finders.. + 'compressor.finders.CompressorFinder', + ) + +* Define :attr:`COMPRESS_ROOT ` in settings + if you don't have already ``STATIC_ROOT`` or if you want it in a different + folder. + +.. _staticfiles: http://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/ +.. _django-staticfiles: http://pypi.python.org/pypi/django-staticfiles + +.. _dependencies: + +Dependencies +------------ + +Required +^^^^^^^^ + +In case you're installing Django Compressor differently +(e.g. from the Git repo), make sure to install the following +dependencies. + +- django-appconf_ + + Used internally to handle Django's settings, this is + automatically installed when following the above + installation instructions. + + pip install django-appconf + +Optional +^^^^^^^^ + +- BeautifulSoup_ + + For the :attr:`parser ` + ``compressor.parser.BeautifulSoupParser`` and + ``compressor.parser.LxmlParser``:: + + pip install "BeautifulSoup<4.0" + +- lxml_ + + For the :attr:`parser ` + ``compressor.parser.LxmlParser``, also requires libxml2_:: + + STATIC_DEPS=true pip install lxml + +- html5lib_ + + For the :attr:`parser ` + ``compressor.parser.Html5LibParser``:: + + pip install html5lib + +- `Slim It`_ + + For the :ref:`Slim It filter ` + ``compressor.filters.jsmin.SlimItFilter``:: + + pip install slimit + +.. _BeautifulSoup: http://www.crummy.com/software/BeautifulSoup/ +.. _lxml: http://codespeak.net/lxml/ +.. _libxml2: http://xmlsoft.org/ +.. _html5lib: http://code.google.com/p/html5lib/ +.. _`Slim It`: http://slimit.org/ +.. _django-appconf: http://pypi.python.org/pypi/django-appconf/ +.. _versiontools: http://pypi.python.org/pypi/versiontools/ diff --git a/django-compressor/docs/remote-storages.txt b/django-compressor/docs/remote-storages.txt new file mode 100644 index 0000000..91e7c2e --- /dev/null +++ b/django-compressor/docs/remote-storages.txt @@ -0,0 +1,91 @@ +.. _remote_storages: + +Remote storages +--------------- + +In some cases it's useful to use a CDN_ for serving static files such as +those generated by Django Compressor. Due to the way Django Compressor +processes files, it requires the files to be processed (in the +``{% compress %}`` block) to be available in a local file system cache. + +Django Compressor provides hooks to automatically have compressed files +pushed to a remote storage backend. Simply set the storage backend +that saves the result to a remote service (see +:attr:`~django.conf.settings.COMPRESS_STORAGE`). + +django-storages +^^^^^^^^^^^^^^^ + +So assuming your CDN is `Amazon S3`_, you can use the boto_ storage backend +from the 3rd party app `django-storages`_. Some required settings are:: + + AWS_ACCESS_KEY_ID = 'XXXXXXXXXXXXXXXXXXXXX' + AWS_SECRET_ACCESS_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' + AWS_STORAGE_BUCKET_NAME = 'compressor-test' + +Next, you need to specify the new CDN base URL and update the URLs to the +files in your templates which you want to compress:: + + COMPRESS_URL = "http://compressor-test.s3.amazonaws.com/" + +.. note:: + + For staticfiles just set ``STATIC_URL = COMPRESS_URL`` + +The storage backend to save the compressed files needs to be changed, too:: + + COMPRESS_STORAGE = 'storages.backends.s3boto.S3BotoStorage' + +Using staticfiles +^^^^^^^^^^^^^^^^^ + +If you are using Django's staticfiles_ contrib app or the standalone +app django-staticfiles_, you'll need to use a temporary filesystem cache +for Django Compressor to know which files to compress. Since staticfiles +provides a management command to collect static files from various +locations which uses a storage backend, this is where both apps can be +integrated. + +#. Make sure the :attr:`~django.conf.settings.COMPRESS_ROOT` and STATIC_ROOT_ + settings are equal since both apps need to look at the same directories + when to do their job. + +#. You need to create a subclass of the remote storage backend you want + to use; below is an example of the boto S3 storage backend from + django-storages_:: + + from django.core.files.storage import get_storage_class + from storages.backends.s3boto import S3BotoStorage + + class CachedS3BotoStorage(S3BotoStorage): + """ + S3 storage backend that saves the files locally, too. + """ + def __init__(self, *args, **kwargs): + super(CachedS3BotoStorage, self).__init__(*args, **kwargs) + self.local_storage = get_storage_class( + "compressor.storage.CompressorFileStorage")() + + def save(self, name, content): + name = super(CachedS3BotoStorage, self).save(name, content) + self.local_storage._save(name, content) + return name + +#. Set your :attr:`~django.conf.settings.COMPRESS_STORAGE` and STATICFILES_STORAGE_ + settings to the dotted path of your custom cached storage backend, e.g. + ``'mysite.storage.CachedS3BotoStorage'``. + +#. To have Django correctly render the URLs to your static files, set the + STATIC_URL_ setting to the same value as + :attr:`~django.conf.settings.COMPRESS_URL` (e.g. + ``"http://compressor-test.s3.amazonaws.com/"``). + +.. _CDN: http://en.wikipedia.org/wiki/Content_delivery_network +.. _Amazon S3: https://s3.amazonaws.com/ +.. _boto: http://boto.cloudhackers.com/ +.. _django-storages: http://code.welldev.org/django-storages/ +.. _django-staticfiles: http://github.com/jezdez/django-staticfiles/ +.. _staticfiles: http://docs.djangoproject.com/en/dev/howto/static-files/ +.. _STATIC_ROOT: http://docs.djangoproject.com/en/dev/ref/settings/#static-root +.. _STATIC_URL: http://docs.djangoproject.com/en/dev/ref/settings/#static-url +.. _STATICFILES_STORAGE: http://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-storage diff --git a/django-compressor/docs/scenarios.txt b/django-compressor/docs/scenarios.txt new file mode 100644 index 0000000..5f86fd7 --- /dev/null +++ b/django-compressor/docs/scenarios.txt @@ -0,0 +1,67 @@ +.. _scenarios: + +Common Deployment Scenarios +=========================== + +This document presents the most typical scenarios in which Django Compressor +can be configured, and should help you decide which method you may want to +use for your stack. + +In-Request Compression +---------------------- + +This is the default method of compression. Where-in Django Compressor will +go through the steps outlined in :ref:`behind_the_scenes`. You will find +in-request compression beneficial if: + +* Using a single server setup, where the application and static files are on + the same machine. + +* Prefer a simple configuration. By default, there is no configuration + required. + +Caveats +------- + +* If deploying to a multi-server setup and using + :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS`, each binary is + required to be installed on each application server. + +* Application servers may not have permissions to write to your static + directories. For example, if deploying to a CDN (e.g. Amazon S3) + +Offline Compression +------------------- + +This method decouples the compression outside of the request +(see :ref:`behind_the_Scenes`) and can prove beneficial in the speed, +and in many scenarios, the maintainability of your deployment. +You will find offline compression beneficial if: + +* Using a multi-server setup. A common scenario for this may be multiple + application servers and a single static file server (CDN included). + With offline compression, you typically run ``manage.py compress`` + on a single utility server, meaning you only maintain + :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS` binaries in one + location. + +* You store compressed files on a CDN. + +Caveats +------- + +* If your templates have complex logic in how template inheritance is done + (e.g. ``{% extends context_variable %}``), then this becomes a problem, + as offline compression will not have the context, unless you set it in + :attr:`~django.conf.settings.COMPRESS_OFFLINE_CONTEXT` + +* Due to the way the manifest file is used, while deploying across a + multi-server setup, your application may use old templates with a new + manifest, possibly rendering your pages incoherent. The current suggested + solution for this is to change the + :attr:`~django.conf.settings.COMPRESS_OFFLINE_MANIFEST` path for each new + version of your code. This will ensure that the old code uses old + compressed output, and the new one appropriately as well. + +Every setup is unique, and your scenario may differ slightly. Choose what +is the most sane to maintain for your situation. diff --git a/django-compressor/docs/settings.txt b/django-compressor/docs/settings.txt new file mode 100644 index 0000000..0d3fd72 --- /dev/null +++ b/django-compressor/docs/settings.txt @@ -0,0 +1,465 @@ +.. _settings: + +Settings +======== + +.. currentmodule:: django.conf.settings + +Django Compressor has a number of settings that control its behavior. +They've been given sensible defaults. + +Base settings +------------- + +.. attribute:: COMPRESS_ENABLED + + :default: the opposite of ``DEBUG`` + + Boolean that decides if compression will happen. To test compression + when ``DEBUG`` is ``True`` ``COMPRESS_ENABLED`` must also be set to + ``True``. + + When ``COMPRESS_ENABLED`` is ``False`` the input will be rendered without + any compression except for code with a mimetype matching one listed in the + :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS` setting. These + matching files are still passed to the precompiler before rendering. + + An example for some javascript and coffeescript. + + .. code-block:: django + + {% load compress %} + + {% compress js %} + + + +.. attribute:: COMPRESS_URL + + :Default: ``STATIC_URL`` + + Controls the URL that linked files will be read from and compressed files + will be written to. + +.. attribute:: COMPRESS_ROOT + + :Default: ``STATIC_ROOT`` + + Controls the absolute file path that linked static will be read from and + compressed static will be written to when using the default + :attr:`~django.conf.settings.COMPRESS_STORAGE` + ``compressor.storage.CompressorFileStorage``. + +.. attribute:: COMPRESS_OUTPUT_DIR + + :Default: ``'CACHE'`` + + Controls the directory inside :attr:`~django.conf.settings.COMPRESS_ROOT` + that compressed files will be written to. + +Backend settings +---------------- + +.. attribute:: COMPRESS_CSS_FILTERS + + :default: ``['compressor.filters.css_default.CssAbsoluteFilter']`` + + A list of filters that will be applied to CSS. + + Possible options are (including their settings): + + - ``compressor.filters.css_default.CssAbsoluteFilter`` + + A filter that normalizes the URLs used in ``url()`` CSS statements. + + .. attribute:: COMPRESS_CSS_HASHING_METHOD + + The method to use when calculating the hash to append to + processed URLs. Either ``'mtime'`` (default) or ``'content'``. + Use the latter in case you're using multiple server to serve your + static files. + + - ``compressor.filters.csstidy.CSSTidyFilter`` + + A filter that passes the CSS content to the CSSTidy_ tool. + + .. attribute:: COMPRESS_CSSTIDY_BINARY + + The CSSTidy binary filesystem path. + + .. attribute:: COMPRESS_CSSTIDY_ARGUMENTS + + The arguments passed to CSSTidy. + + - ``compressor.filters.datauri.CssDataUriFilter`` + + A filter for embedding media as `data: URIs`_ in the CSS. + + .. attribute:: COMPRESS_DATA_URI_MAX_SIZE + + Only files that are smaller than this in bytes value will be embedded. + + - ``compressor.filters.yui.YUICSSFilter`` + + A filter that passes the CSS content to the `YUI compressor`_. + + .. attribute:: COMPRESS_YUI_BINARY + + The YUI compressor filesystem path. Make sure to also prepend this + setting with ``java -jar`` if you use that kind of distribution. + + .. attribute:: COMPRESS_YUI_CSS_ARGUMENTS + + The arguments passed to the compressor. + + - ``compressor.filters.yuglify.YUglifyCSSFilter`` + + A filter that passes the CSS content to the `yUglify compressor`_. + + .. attribute:: COMPRESS_YUGLIFY_BINARY + + The yUglify compressor filesystem path. + + .. attribute:: COMPRESS_YUGLIFY_CSS_ARGUMENTS + + The arguments passed to the compressor. Defaults to --terminal. + + - ``compressor.filters.cssmin.CSSMinFilter`` + + A filter that uses Zachary Voase's Python port of the YUI CSS compression + algorithm cssmin_. + + .. _CSSTidy: http://csstidy.sourceforge.net/ + .. _`data: URIs`: http://en.wikipedia.org/wiki/Data_URI_scheme + .. _cssmin: http://pypi.python.org/pypi/cssmin/ + + - ``compressor.filters.template.TemplateFilter`` + + A filter that renders the CSS content with Django templating system. + + .. attribute:: COMPRESS_TEMPLATE_FILTER_CONTEXT + + The context to render your css files with. + + +.. _compress_js_filters: + +.. attribute:: COMPRESS_JS_FILTERS + + :Default: ``['compressor.filters.jsmin.JSMinFilter']`` + + A list of filters that will be applied to javascript. + + Possible options are: + + - ``compressor.filters.jsmin.JSMinFilter`` + + A filter that uses the jsmin implementation rJSmin_ to compress + JavaScript code. + + .. _slimit_filter: + + - ``compressor.filters.jsmin.SlimItFilter`` + + A filter that uses the jsmin implementation `Slim It`_ to compress + JavaScript code. + + - ``compressor.filters.closure.ClosureCompilerFilter`` + + A filter that uses `Google Closure compiler`_. + + .. attribute:: COMPRESS_CLOSURE_COMPILER_BINARY + + The Closure compiler filesystem path. Make sure to also prepend + this setting with ``java -jar`` if you use that kind of distribution. + + .. attribute:: COMPRESS_CLOSURE_COMPILER_ARGUMENTS + + The arguments passed to the compiler. + + - ``compressor.filters.yui.YUIJSFilter`` + + A filter that passes the JavaScript code to the `YUI compressor`_. + + .. attribute:: COMPRESS_YUI_BINARY + + The YUI compressor filesystem path. + + .. attribute:: COMPRESS_YUI_JS_ARGUMENTS + + The arguments passed to the compressor. + + - ``compressor.filters.yuglify.YUglifyJSFilter`` + + A filter that passes the JavaScript code to the `yUglify compressor`_. + + .. attribute:: COMPRESS_YUGLIFY_BINARY + + The yUglify compressor filesystem path. + + .. attribute:: COMPRESS_YUGLIFY_JS_ARGUMENTS + + The arguments passed to the compressor. + + - ``compressor.filters.template.TemplateFilter`` + + A filter that renders the JavaScript code with Django templating system. + + .. attribute:: COMPRESS_TEMPLATE_FILTER_CONTEXT + + The context to render your JavaScript code with. + + .. _rJSmin: http://opensource.perlig.de/rjsmin/ + .. _`Google Closure compiler`: http://code.google.com/closure/compiler/ + .. _`YUI compressor`: http://developer.yahoo.com/yui/compressor/ + .. _`yUglify compressor`: https://github.com/yui/yuglify + .. _`Slim It`: http://slimit.org/ + +.. attribute:: COMPRESS_PRECOMPILERS + + :Default: ``()`` + + An iterable of two-tuples whose first item is the mimetype of the files or + hunks you want to compile with the command or filter specified as the second + item: + + #. mimetype + The mimetype of the file or inline code that should be compiled. + + #. command_or_filter + The command to call on each of the files. Modern Python string + formatting will be provided for the two placeholders ``{infile}`` and + ``{outfile}`` whose existence in the command string also triggers the + actual creation of those temporary files. If not given in the command + string, Django Compressor will use ``stdin`` and ``stdout`` respectively + instead. + + Alternatively, you may provide the fully qualified class name of a + filter you wish to use as a precompiler. + + Example:: + + COMPRESS_PRECOMPILERS = ( + ('text/coffeescript', 'coffee --compile --stdio'), + ('text/less', 'lessc {infile} {outfile}'), + ('text/x-sass', 'sass {infile} {outfile}'), + ('text/x-scss', 'sass --scss {infile} {outfile}'), + ('text/stylus', 'stylus < {infile} > {outfile}'), + ('text/foobar', 'path.to.MyPrecompilerFilter'), + ) + + .. note:: + Depending on the implementation, some precompilers might not support + outputting to something else than ``stdout``, so you'll need to omit the + ``{outfile}`` parameter when working with those. For instance, if you + are using the Ruby version of lessc, you'll need to set up the + precompiler like this:: + + ('text/less', 'lessc {infile}'), + + With that setting (and CoffeeScript_ installed), you could add the following + code to your templates: + + .. code-block:: django + + {% load compress %} + + {% compress js %} + + {% endcompress %} + + This would give you something like this:: + + + + The same works for less_, too: + + .. code-block:: django + + {% load compress %} + + {% compress css %} + + + {% endcompress %} + + Which would be rendered something like:: + + + + .. _less: http://lesscss.org/ + .. _CoffeeScript: http://jashkenas.github.com/coffee-script/ + +.. attribute:: COMPRESS_STORAGE + + :Default: ``'compressor.storage.CompressorFileStorage'`` + + The dotted path to a Django Storage backend to be used to save the + compressed files. + + Django Compressor ships with one additional storage backend: + + * ``'compressor.storage.GzipCompressorFileStorage'`` + + A subclass of the default storage backend, which will additionally + create ``*.gz`` files of each of the compressed files. + +.. attribute:: COMPRESS_PARSER + + :Default: ``'compressor.parser.AutoSelectParser'`` + + The backend to use when parsing the JavaScript or Stylesheet files. The + ``AutoSelectParser`` picks the ``lxml`` based parser when available, and falls + back to ``HtmlParser`` if ``lxml`` is not available. + + ``LxmlParser`` is the fastest available parser, but ``HtmlParser`` is not much + slower. ``AutoSelectParser`` adds a slight overhead, but in most cases it + won't be necessary to change the default parser. + + The other two included parsers are considerably slower and should only be + used if absolutely necessary. + + .. warning:: + + In some cases the ``compressor.parser.HtmlParser`` parser isn't able to + parse invalid HTML in JavaScript or CSS content. As a workaround you + should use one of the more forgiving parsers, e.g. the + ``BeautifulSoupParser``. + + The backends included in Django Compressor: + + - ``compressor.parser.AutoSelectParser`` + - ``compressor.parser.LxmlParser`` + - ``compressor.parser.HtmlParser`` + - ``compressor.parser.BeautifulSoupParser`` + - ``compressor.parser.Html5LibParser`` + + See :ref:`dependencies` for more info about the packages you need + for each parser. + +Caching settings +---------------- + +.. attribute:: COMPRESS_CACHE_BACKEND + + :Default: ``CACHES["default"]`` or ``CACHE_BACKEND`` + + The backend to use for caching, in case you want to use a different cache + backend for Django Compressor. + + If you have set the ``CACHES`` setting (new in Django 1.3), + ``COMPRESS_CACHE_BACKEND`` defaults to ``"default"``, which is the alias for + the default cache backend. You can set it to a different alias that you have + configured in your ``CACHES`` setting. + + If you have not set ``CACHES`` and are using the old ``CACHE_BACKEND`` + setting, ``COMPRESS_CACHE_BACKEND`` defaults to the ``CACHE_BACKEND`` setting. + +.. attribute:: COMPRESS_REBUILD_TIMEOUT + + :Default: ``2592000`` (30 days in seconds) + + The period of time after which the compressed files are rebuilt even if + no file changes are detected. + +.. attribute:: COMPRESS_MINT_DELAY + + :Default: ``30`` (seconds) + + The upper bound on how long any compression should take to run. Prevents + dog piling, should be a lot smaller than + :attr:`~django.conf.settings.COMPRESS_REBUILD_TIMEOUT`. + +.. attribute:: COMPRESS_MTIME_DELAY + + :Default: ``10`` + + The amount of time (in seconds) to cache the modification timestamp of a + file. Should be smaller than + :attr:`~django.conf.settings.COMPRESS_REBUILD_TIMEOUT` and + :attr:`~django.conf.settings.COMPRESS_MINT_DELAY`. + +.. attribute:: COMPRESS_DEBUG_TOGGLE + + :Default: None + + The name of the GET variable that toggles the debug mode and prevents Django + Compressor from performing the actual compression. Only useful for debugging. + + .. warning:: + + Don't use this option in production! + + An easy convention is to only set it depending on the ``DEBUG`` setting:: + + if DEBUG: + COMPRESS_DEBUG_TOGGLE = 'whatever' + + .. note:: + + This only works for pages that are rendered using the RequestContext_ + and the ``django.core.context_processors.request`` context processor. + + .. _RequestContext: http://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.RequestContext + +.. attribute:: COMPRESS_CACHE_KEY_FUNCTION + + :Default: ``'compressor.cache.simple_cachekey'`` + + The function to use when generating the cache key. The function must take + one argument which is the partial key based on the source's hex digest. + It must return the full key as a string. + +Offline settings +---------------- + +.. attribute:: COMPRESS_OFFLINE + + :Default: ``False`` + + Boolean that decides if compression should also be done outside of the + request/response loop -- independent from user requests. This allows to + pre-compress CSS and JavaScript files and works just like the automatic + compression with the ``{% compress %}`` tag. + +.. attribute:: COMPRESS_OFFLINE_TIMEOUT + + :Default: ``31536000`` (1 year in seconds) + + The period of time with which the ``compress`` management command stores + the pre-compressed the contents of ``{% compress %}`` template tags in + the cache. + +.. attribute:: COMPRESS_OFFLINE_CONTEXT + + :Default: ``{'STATIC_URL': settings.STATIC_URL}`` + + The context to be used by the ``compress`` management command when rendering + the contents of ``{% compress %}`` template tags and saving the result in the + offline cache. + + If available, the ``STATIC_URL`` setting is also added to the context. + +.. attribute:: COMPRESS_OFFLINE_MANIFEST + + :Default: ``manifest.json`` + + The name of the file to be used for saving the names of the files + compressed offline. diff --git a/django-compressor/docs/usage.txt b/django-compressor/docs/usage.txt new file mode 100644 index 0000000..3e18a8f --- /dev/null +++ b/django-compressor/docs/usage.txt @@ -0,0 +1,234 @@ +.. _usage: + +Usage +===== + +.. code-block:: django + + {% load compress %} + {% compress [ [block_name]] %} + + {% endcompress %} + +Examples +-------- + +.. code-block:: django + + {% load compress %} + + {% compress css %} + + + + {% endcompress %} + +Which would be rendered something like: + +.. code-block:: django + + + +or: + +.. code-block:: django + + {% load compress %} + + {% compress js %} + + + {% endcompress %} + +Which would be rendered something like: + +.. code-block:: django + + + +.. note:: + + Remember that django-compressor will try to :ref:`group ouputs by media `. + +Linked files **must** be accessible via +:attr:`~django.conf.settings.COMPRESS_URL`. + +If the :attr:`~django.conf.settings.COMPRESS_ENABLED` setting is ``False`` +(defaults to the opposite of DEBUG) the ``compress`` template tag does nothing +and simply returns exactly what it was given. + +.. note:: + + If you've configured any + :attr:`precompilers ` + setting :attr:`~django.conf.settings.COMPRESS_ENABLED` to ``False`` won't + affect the processing of those files. Only the + :attr:`CSS ` and + :attr:`JavaScript filters ` + will be disabled. + +If both DEBUG and :attr:`~django.conf.settings.COMPRESS_ENABLED` are set to +``True``, incompressible files (off-site or non existent) will throw an +exception. If DEBUG is ``False`` these files will be silently stripped. + +.. warning:: + + For production sites it is **strongly recommended** to use a real cache + backend such as memcached_ to speed up the checks of compressed files. + Make sure you set your Django cache backend appropriately (also see + :attr:`~django.conf.settings.COMPRESS_CACHE_BACKEND` and + Django's `caching documentation`_). + +The compress template tag supports a second argument specifying the output +mode and defaults to saving the result in a file. Alternatively you can +pass '``inline``' to the template tag to return the content directly to the +rendered page, e.g.: + +.. code-block:: django + + {% load compress %} + + {% compress js inline %} + + + {% endcompress %} + +would be rendered something like:: + + + +The compress template tag also supports a third argument for naming the output +of that particular compress tag. This is then added to the context so you can +access it in the `post_compress signal `. + +.. _memcached: http://memcached.org/ +.. _caching documentation: http://docs.djangoproject.com/en/1.2/topics/cache/#memcached + +.. _pre-compression: + +Pre-compression +--------------- + +Django Compressor comes with an optional ``compress`` management command to +run the compression outside of the request/response loop -- independent +from user requests. This allows to pre-compress CSS and JavaScript files and +works just like the automatic compression with the ``{% compress %}`` tag. + +To compress the files "offline" and update the offline cache you have +to use the ``compress`` management command, ideally during deployment. +Also make sure to enable the :attr:`django.conf.settings.COMPRESS_OFFLINE` +setting. In case you don't use the ``compress`` management command, Django +Compressor will automatically fallback to the automatic compression using +the template tag. + +The command parses all templates that can be found with the template +loader (as specified in the TEMPLATE_LOADERS_ setting) and looks for +``{% compress %}`` blocks. It then will use the context as defined in +:attr:`django.conf.settings.COMPRESS_OFFLINE_CONTEXT` to render its +content. So if you use any variables inside the ``{% compress %}`` blocks, +make sure to list all values you require in ``COMPRESS_OFFLINE_CONTEXT``. +It's similar to a template context and should be used if a variable is used +in the blocks, e.g.: + +.. code-block:: django + + {% load compress %} + {% compress js %} + + {% endcompress %} + +Since this template requires a variable (``path_to_files``) you need to +specify this in your settings before using the ``compress`` management +command:: + + COMPRESS_OFFLINE_CONTEXT = { + 'path_to_files': '/static/js/', + } + +If not specified, the ``COMPRESS_OFFLINE_CONTEXT`` will by default contain +the commonly used setting to refer to saved files ``STATIC_URL``. + +The result of running the ``compress`` management command will be cached +in a file called ``manifest.json`` using the :attr:`configured storage +` to be able to be transfered from your developement +computer to the server easily. + +.. _TEMPLATE_LOADERS: http://docs.djangoproject.com/en/dev/ref/settings/#template-loaders + +.. _signals: + +Signals +------- + +.. function:: compressor.signals.post_compress(sender, type, mode, context) + +Django Compressor includes a ``post_compress`` signal that enables you to +listen for changes to your compressed CSS/JS. This is useful, for example, if +you need the exact filenames for use in an HTML5 manifest file. The signal +sends the following arguments: + +``sender`` + Either :class:`compressor.css.CssCompressor` or + :class:`compressor.js.JsCompressor`. + + .. versionchanged:: 1.2 + + The sender is now one of the supported Compressor classes for + easier limitation to only one of them, previously it was a string + named ``'django-compressor'``. + +``type`` + Either "``js``" or "``css``". + +``mode`` + Either "``file``" or "``inline``". + +``context`` + The context dictionary used to render the output of the compress template + tag. + + If ``mode`` is "``file``" the dictionary named ``compressed`` in the + context will contain a "``url``" key that maps to the relative URL for + the compressed asset. + + If ``type`` is "``css``", the dictionary named ``compressed`` in the + context will additionally contain a "``media``" key with a value of + ``None`` if no media attribute is specified on the link/style tag and + equal to that attribute if one is specified. + + Additionally, ``context['compressed']['name']`` will be the third + positional argument to the template tag, if provided. + +.. note:: + + When compressing CSS, the ``post_compress`` signal will be called once for + every different media attribute on the tags within the ``{% compress %}`` + tag in question. + +.. _css_notes: + +CSS Notes +--------- + +All relative ``url()`` bits specified in linked CSS files are automatically +converted to absolute URLs while being processed. Any local absolute URLs (those +starting with a ``'/'``) are left alone. + +Stylesheets that are ``@import``'d are not compressed into the main file. +They are left alone. + +If the media attribute is set on