Giter VIP home page Giter VIP logo

django-compressor's Introduction

Django Compressor

image

image

Django Compressor processes, combines and minifies linked and inline Javascript or CSS in a Django template into cacheable static files.

It supports compilers such as coffeescript, LESS and SASS and is extensible by custom processing steps.

How it works

In your templates, all HTML code between the tags {% 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. Both Javascript and CSS files are by default concatenated and minified.

As the final step the template tag outputs a <script> or <link> tag pointing to the optimized file. Alternatively it can also inline the resulting content into the original template directly.

Since the file name is dependent on the content, these files can be given a far future expiration date without worrying about stale browser caches.

For increased performance, the concatenation and compressing process can also be run once manually outside of the request/response cycle by using the Django management command manage.py compress.

Configurability & Extensibility

Django Compressor is highly configurable and extensible. The HTML parsing is done using lxml or if it's not available Python's built-in HTMLParser by default. As an alternative Django Compressor provides a BeautifulSoup and a html5lib based parser, as well as an abstract base class that makes it easy to write a custom parser.

Django Compressor also comes with built-in support for YUI CSS and JS compressor, yUglify CSS and JS compressor, Google's Closure Compiler, a Python port of Douglas Crockford's JSmin, a Python port of the YUI CSS Compressor csscompressor and a filter to convert (some) images into data URIs.

If your setup requires a different compressor or other post-processing tool it will be fairly easy to implement a custom filter. Simply extend from one of the available base classes.

More documentation about the usage and settings of Django Compressor can be found on django-compressor.readthedocs.org.

The source code for Django Compressor can be found and contributed to on github.com/django-compressor/django-compressor. There you can also file tickets.

The in-development version of Django Compressor can be installed with pip install git+https://github.com/django-compressor/django-compressor.git

django-compressor's People

Contributors

agriffis avatar albertyw avatar alvra avatar browniebroke avatar carljm avatar carltongibson avatar cbjadwani avatar cuu508 avatar dependabot[bot] avatar diox avatar dziegler avatar emperorcezar avatar iknite avatar jaap3 avatar jezdez avatar karyon avatar kudlatyamroth avatar lukaszb avatar matthewwithanm avatar mintchaos avatar philippbosch avatar pindia avatar rafales avatar ramast avatar rfleschenberg avatar scop avatar sdfsdhgjkbmnmxc avatar shemigon avatar spookylukey avatar ulope avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-compressor's Issues

URLs generated for compressed files on remote storage should use COMPRESS_URL

STATIC_URL = "http://cdn.example.com"
COMPRESS_URL = STATIC_URL
AWS_STORAGE_BUCKET_NAME = "static-example"

In this case the compressed files use a base URL which is based off of storage.url() rather than using COMPRESS_URL. In this case the URL generated for the compressed files starts with http://static-example.s3.aws.amazon.com rather than http://cdn.example.com

I think this line

url = self.storage.url(new_filepath)

needs to be changed

in base.py

class Compressor(object):
    ....
    def output_file(self, mode, content, forced=False):
        """
        The output method that saves the content to a file and renders
        the appropriate template with the file's URL.
        """
        new_filepath = self.filepath(content)
        if not self.storage.exists(new_filepath) or forced:
            self.storage.save(new_filepath, ContentFile(content))
        url = self.storage.url(new_filepath)
        return self.render_output(mode, {"url": url})

Data URI:s that work in IE6 and IE7

Nice to see that you've taken over the project! :) Let me move a feature request from the old repo here:

Data URI:s are great, but IE6 and IE7 does not support them. Good thing is, they can be emulated with MHTML, and it actually isn't that tricky:

http://www.phpied.com/the-proper-mhtml-syntax/

This would be an excellent addition to django_compressor, don't you think?

allow filters to be toggled independent from COMPRESS_DEBUG_TOGGLE

If I set COMPRESS_DEBUG_TOGGLE to 'whatever' and uses that in querystring, compress tag will return all the elements within as is. This is a problem for me because the lessc files too are returned unfiltered, which break the whole page.

I'd really like something to toggle the compression independent from the filters, something like FILTER_DEBUG_TOGGLE.

I can put together some patches to do that, if that's OK.

COMPRESS_ROOT default causing problems with staticfiles

Hi Jannis,

Currently compressor's COMPRESS_ROOT setting defaults to STATIC_ROOT. If the file is already present in STATIC_ROOT, running "collectstatic -l" will cause an infinite symbolic link loop. I setup compressor with pretty much default settings, visit the site which causes compressor to generate minified CSS and ran the following:

selwin@selwin:~/dev/spark$ ./manage.py collectstatic -l

You have requested to collect static files at the destination
location as specified in your settings file.

This will overwrite existing files.
Are you sure you want to do this?

Type 'yes' to continue, or 'no' to cancel: yes

<-- snip -->
Linking '/home/selwin/dev/spark/static/CACHE/css/bdec3fe0ab18.css'

413 static files symlinked to '/home/selwin/dev/spark/../spark/static/'.

nginx stopped responding and when I checked compressor generated file, apparently for compressor generated files, staticfiles created a symlink to itself:

selwin@selwin:~/dev/spark$ more static/CACHE/css/bdec3fe0ab18.css
static/CACHE/css/bdec3fe0ab18.css: Too many levels of symbolic links
selwin@selwin:~/dev/spark$ ls -la static/CACHE/css
total 8
drwxr-xr-x 2 selwin selwin 4096 2011-06-09 14:47 .
drwxr-xr-x 3 selwin selwin 4096 2011-06-09 14:47 ..
lrwxrwxrwx 1 selwin selwin   56 2011-06-09 14:47 bdec3fe0ab18.css -> /home/selwin/dev/spark/static/CACHE/css/bdec3fe0ab18.css

Setting COMPRESS_ROOT to another dir solves this, but that means having one extra directory in my project dir.

I thought about modifying CompressorFinder but couldn't find enough documentation to fully understand what it does. If you could point me in the right direction though, I'd be more than happy to help.

cannot import name HTMLParser on case-insensitive file-systems (e.g. Mac)

the file htmlparser.py has this import statement:
from HTMLParser import HTMLParser

since the file itself is called htmlparser and there is a class HTMLParser in it, this import will not work on a case-insensitive file-system

fix:
change:
from HTMLParser import HTMLParser
to:
from ..HTMLParser import HTMLParser

STDIN not being sent to filter processes

I have everything setup properly and working wonderfully, however linking to files isn't working.

it works fine like this

{% compress js %}
<script type="text/coffeescript">
# Functions:
square = (x) -> x * x
console.log square
</script>
{% endcompress %}

My template code (which doesn't work since it's using a url)

{% load compress %}

<html>
    <head>
        <title>{% block title %}{% endblock %}</title>
        {% compress js %}
        <script type="text/coffeescript" charset="utf-8" src="/static/tests.coffee"></script>
        {% endcompress %}
    </head>
    <body>{% block content %}{% endblock %}</body>
</html>

tests.coffee

# Functions:
square = (x) -> x * x
console.log square

resulting code (was run in yuicompressor afterward). If not using coffeescript, there simply would be no files saved to cache, or javascript link to the cached javascript in the html; coffeescript is only an exception because it outputs the wrapper even if nothing is entered.

(function(){}).call(this);

my app settings

# add node.js to path
os.environ['PATH'] += ':%s' % os.path.join(PROJECT_ROOT, 'sys/compressor/compressors')

COMPRESS_PRECOMPILERS = (
    ('text/coffeescript', '%s --compile --stdio' % os.path.join(PROJECT_ROOT, 'sys/compressor/compressors/coffeescript/bin/coffee')),
    ('text/less', '%s {infile} {outfile}' % os.path.join(PROJECT_ROOT, 'sys/compressor/compressors/less/bin/lessc')),
    ('text/x-sass', '%s {infile} {outfile}' % os.path.join(PROJECT_ROOT, 'sys/compressor/compressors/sass/bin/sass')),
    ('text/x-scss', '%s --scss {infile} {outfile}' % os.path.join(PROJECT_ROOT, 'sys/compressor/compressors/sass/bin/sass')),
    ('text/clevercss', 'python %s' % os.path.join(PROJECT_ROOT, 'sys/compressor/compressors/clevercss.py'))
)

# caching backend to use
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
    }
}

COMPRESS = True

COMPRESS_OUTPUT_DIR = 'cache'

COMPRESS_CSSTIDY_BINARY = os.path.join(PROJECT_ROOT, 'sys/compressor/filters/csstidy')
COMPRESS_YUI_BINARY = 'java -jar %s' % os.path.join(PROJECT_ROOT, 'sys/compressor/filters/yuicompressor.jar')

COMPRESS_CSS_FILTERS = ['apps.compressor.filters.csstidy.CSSTidyFilter', 'apps.compressor.filters.yui.YUICSSFilter']
COMPRESS_JS_FILTERS = ['apps.compressor.filters.yui.YUIJSFilter']

Using:
django 1.3
node 0.4.7

compress templatetag throwing error

When using django_compressor with DEBUG set to True and COMPRESS also set to True it throws a Caught TypeError while rendering: decoding Unicode is not supported error if it has url() in the stylesheet to replace. If you have CssAbsoluteFilter in your filters list the content passed to the input method is of str type. Then running return URL_PATTERN.sub(self.url_converter, self.content) the content then gets return as the unicode type.

In the Compressor runs over the hunks method it returns yield unicode(content, charset). When the content is of unicode type and the charset is set to utf-8 the unicode function raises the TypeError

GzipCompressorFileStorage returns URLs ending in .gz

In addition to creating a gzipped version of the compressed files, GzipCompressorFileStorage appends a .gz at the end of URLs.

This behavior is not documented, and I don't understand it.

I want the gzipped version so that the webserver doesn't have to re-compress the file every time it's requested by a client that supports gzip, but I still want the original URLs in the HTML.

Did I miss something? Or is it a bug?

See also http://wiki.nginx.org/HttpGzipStaticModule

Some settings issue

Hello,

I'm not very good at this, please bear with me.

I have a dev and prod system both with Python 2.7, Django 1.3 and compressor 0.7.1.
On the local system, if I do

os.environ['DJANGO_SETTINGS_MODULE'] = "settings"
from compressor.conf import settings

and then check the attributes of settings, almost everything imaginable is there.

But on the prod system, if I do the same, setting's attribute just shows
settings.ABSOLUTE_CSS_URLS
settings.COMPRESS_JS_FILTERS
settings.OUTPUT_DIR
settings.COMPILER_FORMATS
settings.COMPRESS
settings.MEDIA_ROOT
settings.COMPRESS_CSS_FILTERS
settings.MEDIA_URL

If I do collectstatic or visit a page, I'll get no COMPRESS_ROOT or COMPRESS_CACHE_BACKEND attribute.

Any thoughts on what I might have done wrong?

Thanks,
Xiao

Compass filter should process only scss files

This causes an error:

{% compress %}
    <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/styles.scss" />
    <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}js/jquery/plugin/css/plugin.css" />
{% endcompress %}

Because it tries to filter plugin.css throug COMPASS filter.

Filters are being run in DEBUG mode

With the latest changes in the develop branch, filters are getting applied in DEBUG mode, when they shouldn't be.

It works fine before those changes were made.

W3C validation failed: incorrect self-close tag "link"

The "link" self-close generated element is malformed:
<link rel="stylesheet" href="http://static.domain.tld/CACHE/css/64673ae00d55.css" type="text/css">

rather than:
<link rel="stylesheet" href="http://static.domain.tld/CACHE/css/64673ae00d55.css" type="text/css" />

Allow for media URLs with querystrings

A fairly common technique in media is to use querystrings to issue new URLs (which are ignored in static file service) and then issue far-future cache headers. Compressor currently maps COMPRESS_URL to COMPRESS_ROOT literally, so that any querystring is expected to exist on the file system.

It would be good to have an option to ignore querystrings in media URLs.

Here's what I did in client code for now:

from compressor.conf import settings as compressor_settings
from compressor.exceptions import UncompressableFileError
from compressor.base import Compressor
from compressor.css import CssCompressor
from compressor.js import JsCompressor

class FooBaseCompressor(Compressor):
    def get_filename(self, url):
        try:
            base_url = self.storage.base_url
        except AttributeError:
            base_url = settings.COMPRESS_URL
        if not url.startswith(base_url):
            raise UncompressableFileError(
                "'%s' isn't accesible via COMPRESS_URL ('%s') and can't be"
                " compressed" % (url, base_url))
        basename = url.replace(base_url, "", 1)

        # the one custom bit, alas:
        basename = basename.split("?", 1)[0] # drop the querystring, which is used for non-compressed cache-busting.

        filename = os.path.join(compressor_settings.COMPRESS_ROOT, basename)
        if not os.path.exists(filename):
            raise UncompressableFileError("'%s' does not exist" % filename)
        return filename

class FooCssCompressor(FooBaseCompressor, CssCompressor):
    pass
class FooJsCompressor(FooBaseCompressor, JsCompressor):
    pass

Efficient offline generation for multiple sites

I have a project with 100s of domains and related settings files. I'd like to generate compressed files for each of them (offline) upon deploy. It'd be nice if there were a way to map the required settings for use as a single management command to avoid the startup time of django-admin for each settings file. (It's a large project, startup is about 4 seconds on a very capable machine.)

Cache: get_offline_cachekey uses too little information from TextNode for hashing

Too much information is lost from TextNode objects because of smart_str being used for hashing:

def __repr__(self):
    return "<Text Node: '%s'>" % smart_str(self.s[:25], 'ascii',
            errors='replace')

Here's a proposed new implementation of get_offline_cachekey:

def get_offline_cachekey(source):
    to_hexdigest = [smart_str(getattr(s, 's', s)) for s in source]
    return get_cachekey("offline.%s" % get_hexdigest("".join(to_hexdigest)))

Only PRECOMPILE script in DEBUG mode if script content changed

Problem: In DEBUG mode, script(s) is/are continually recompiled by the PRECOMPILER on each page load, whereas in production mode, scripts are only precompiled (then filtered) if the script actually changed any (or at end of default 30 day period).

I propose to ONLY recompile scripts in DEBUG mode when the script content changes, and it needs to be recompiled, just like in production. This way there'll be no recompiling of the script when it's not needed, but it will be when it's needed.

issue with compressor >= 0.6.3 and django 1.2.x / staticfiles 1.0.1

Upgraded a project that previously used django_compressor 0.6b5 to 0.7 and my compressed media broke with a traceback on each request. Traceback:

Traceback (most recent call last):
  File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/django/core/servers/basehttp.py", line 280, in run
    self.result = application(self.environ, self.start_response)
  File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/handlers.py", line 67, in __call__
    return super(StaticFilesHandler, self).__call__(environ, start_response)
  File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/django/core/handlers/wsgi.py", line 248, in __call__
    response = self.get_response(request)
  File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/handlers.py", line 57, in get_response
    return self.serve(request)
  File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/handlers.py", line 50, in serve
    return serve(request, self.file_path(request.path), insecure=True)
  File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/views.py", line 35, in serve
    absolute_path = finders.find(normalized_path)
  File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/finders.py", line 236, in find
    for finder in get_finders():
  File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/finders.py", line 253, in get_finders
    yield get_finder(finder_path)
  File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/django/utils/functional.py", line 124, in wrapper
    result = func(*args)
  File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/finders.py", line 262, in _get_finder
    mod = import_module(module)
  File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/django/utils/importlib.py", line 35, in import_module
    __import__(name)
  File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/compressor/finders.py", line 4, in 
    class CompressorFinder(staticfiles.finders.BaseStorageFinder):
AttributeError: 'NoneType' object has no attribute 'BaseStorageFinder'

Did some digging, removed the try at https://github.com/jezdez/django_compressor/blob/master/compressor/utils/staticfiles.py#L15 and got a template import error from CssCompressor.

Traced the error back to what appears to be namespace issue with the compressor.utils.staticfiles module stepping on staticfiles, which causes an import of staticfiles.conf to fail even when the same import works fine in the same python session. Traceback that shows that is below:

In [1]: from compressor.css import CssCompressor
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)

/Users/luke/projects/project/ in ()


/Users/luke/.virtualenvs/env/lib/python2.6/site-packages/compressor/css.py in ()
----> 2 from compressor.base import Compressor
      3 from compressor.exceptions import UncompressableFileError
      4 
      5 
      6 class CssCompressor(Compressor):

/Users/luke/.virtualenvs/env/lib/python2.6/site-packages/compressor/base.py in ()
     10 from compressor.filters import CompilerFilter
     11 from compressor.storage import default_storage
---> 12 from compressor.utils import get_class, staticfiles
     13 from compressor.utils.cache import cached_property
     14 

/Users/luke/.virtualenvs/env/lib/python2.6/site-packages/compressor/utils/staticfiles.py in ()
     14     else:
     15         from staticfiles import finders
---> 16         from staticfiles.conf import settings
     17 
     18     if INSTALLED and "compressor.finders.CompressorFinder" \

ImportError: No module named conf

In [2]: from staticfiles.conf import settings

In [3]: settings
Out[3]: 

Using compressor while developing with runserver?

It's writtent that in case of staticfiles usage I should add CompressorFinder to the finders setting, and it should do the trick

However compressor does only see the files collected with collectstatic, thus not noticing any new or changed files in app/static directory. In comparison, staticfiles is quite good in working with these files when runserver command is used.

Am I missing something?

Relative URLs in CSS not being converted to absolute URLs

The docs have a note about relative url() values in CSS being converted to absolute URLs while being processed, but this isn't happening in my case. Instead, the URLs are being left alone and the linked-to images aren't being found.

The settings that I've customized are:
COMPRESS_ROOT = os.path.join(PROJECT_ROOT, "..", "site_media", "static")
COMPRESS_URL = STATIC_URL

And I'm using LocMemCache as the cache backend, django version 1.2.4, and django-compressor version 0.5.3.

http://django_compressor.readthedocs.org/en/latest/index.html#css-notes

Let me know if any other info would be helpful.

problem using django_compressor with apache

I'm having some problems getting django_compressor to work when running apache.

I'm using S3 as my remote storage so when I run 'python manage.py compress' and the expected js + css files appear in the correct place in my S3 bucket.

However when the page renders from the apache+mod_wsgi server the line linking to the compressed js/css does not appear at all (the whole block that i wrapped in {% compress %} is missing.

What's very odd is that when i run the django dev server (with the same exact settings) the expected compresssed js+css files do exist.

Any suggestions about where I should look to debug this? I'd love to use django_compressor in production

-Evan

COMPRESS = True doesn't enable compress

In the documentation for enabling compress, you have listed that the setting to enable is:

COMPRESS = True

that didn't work for me. I had to use:

COMPRESS_ENABLED = True

I think this is just a typo in your docs; I thought I'd let you know.

COMPRESS_URL, COMPRESS_ROOT ... static vs. media

I´m not entirely sure but I supposed that the defaults for COMPRESS_URL and COMPRESS_ROOT are STATIC_URL and STATIC_ROOT (instead of MEDIA_URL and MEDIA_ROOT).

any reasons for this?

thanks,
patrick

HTML characters breaks HtmlParser

Trying to compress a line like this one:

var html = "<//div>"

Raise this error:

Error while initializing HtmlParser: bad end tag: u'<//div>', at line 28, column 47

Seems to happen with any html closing tag.

This is likely an HtlmParser error and maybe should it be removed from the parsers, BeautifulSoup works fine.

Using this setting:

COMPRESS_PARSER = 'compressor.parser.AutoSelectParser'

fails silently, but using 'HtmlParser' shows the error.

NOTE: installing lxml is a pain in the ass :(

COMPRESS_REBUILD_TIMEOUT not in settings module

When loading template tag "compress", i get an error that indicates compressor's settings object is not being filled by the initial configure.

These are the only settings which are loaded by compressor.

['ABSOLUTE_CSS_URLS', 'COMPILER_FORMATS', 'COMPRESS', 'COMPRESS_CSS_FILTERS', 'COMPRESS_JS_FILTERS', 'ImproperlyConfigured', 'MEDIA_ROOT', 'MEDIA_URL', 'OUTPUT_DIR', '__builtins__', '__doc__', '__file__', '__name__', '__package__', 'settings'] 

I have no idea why it would load these but choke loading the other options.

Allow LESS css files for compression that will be used on the client

If you want to allow the less.js to parse the LESS css files on the client you need to set the rel attribute to stylesheet/less. Doing this makes django-compressor skip the files as it looks for rel="stylesheet". It would be great to have a CSS_COMPRESSOR that we could configure that would assume the files are LESS and compress them and out put one link element with the rel set to stylesheet/less so that less.js will parse the compressed file.

Support for multiple media subdomains

I've actually got this working with very minor customisations to django_compressor. My approach is to use a custom context processor to set MEDIA_URL (which I use everywhere for loading JS/CSS) to an appropriate subdomain e.g. http://media. (or potentially http://media{0,1,2,...}). This forces django_compressor to create a new cache key for this content.

Next was to ensure django_compressor passed the current context through via the template tag. This was essential to perform custom manipulation of django_compressor's URL e.g. for handling SSL/non-SSL, for which I needed the current request.

I also had to modify get_filename to be slightly more liberal and strip the protocol+hostname before checking if a URL is compressible.

It would be nice to have a more "core" way to do this, rather than relying on the request context variable being available, for example. I may submit a pull request if I get time, otherwise feel free to implement!

The new COMPASS filter when COMPRESS = False

I'm testing the new cool COMPASS filter, and it's great.

The problem comes when I disable the compression for debugging purposes. The scss files are not compiled.

I've been playing with COMPRESS_PRECOMPILERS, but I don't understand when it should be used.

I have:

COMPRESS_PRECOMPILERS = (
    ('text/x-sass', 'sass {infile} {outfile}'),
    ('text/x-scss', 'sass --scss {infile} {outfile}'),
)

I supposed that the precompilers will check if any of the files matching the mimetypes, and compile them, but leaving all the other files untouched. But I was wrong.

Well, what i want to do is to show the css and js the same as when COMPRESS = False, but compiling the scss files.

What is the best approach to solve this problem?

Do you think is a good idea to add a new setting like: COMPILE_ALWAYS similar to COMPRESS_PRECOMPILERS?

An example of the desired behaviour with COMPRESS = FALSE

Template:

{% compress js %}
    <script type="text"javascript" src="{{ STATIC_URL }}/js/file1.js" ></script>
{% endcompress %}
{% compress css %}
    <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/stylesheets.css" />
    <!-- Note that this is a SCSS file to be processed by COMPASS FILTER -->
    <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/other_styles.scss" />
{% endcompress %}

Result wanted:

<script type="text"javascript" src="/static/js/file1.js" ></script>
<link rel="stylesheet" type="text/css" href="/static/css/stylesheets.css" />
<link rel="stylesheet" type="text/css" href="/static/CACHE/3b1f6363b80d.css" />

Regards,

Adrian

Offline generation

There have been a few requests for offline generation in the old repo's issue tracker (mintchaos#34 and mintchaos#56).

I've had the need for offline generation too and have built an implementation that works for my use case (and should work for most needs IMHO): https://github.com/ulope/django_compressor/tree/offline

There are a few limitations however:

  • Currently no docs (although I would be happy to provide some in case you are interested in having this feature merged)
  • Only new-style (i.e. class-based) template Loaders that provide a 'get_template_sources()' method are supported (this implies django >= 1.2).
  • From the stock django loaders only the "filesystem" and "app_directories" loaders currently fit the above restrictions. That means that compress blocks can only be found in templates loadable by these loaders.

370% performance improvement patch

As part of diagnosing a performance issue with our site, I discovered that a large portion of our page execution (74% in fact) was spent in django_compressor (jsmin.py, to be precise).

The core reason is that to determine if we already have a cached copy of the combined files, we have to run compressor.hash(), which in turn runs all the filters (which are slow). In the case of sites that have sizeable javascript, this becomes the dominant portion of page execution.

I have implemented a (crude) patch in a fork here: https://github.com/fennb/django_compressor that solves the problem by hashing on pre-filtered content, rather than post-filtered.

As a general approach, this seems to work, but I haven't done anything like see if this passes unit tests/etc. I ran into problems with compressor.concat() being a generator so secondary calls caused it to return '' (a separate bug?) so I used cached_property to memoize the output.

The results are fairly dramatic:
https://img.skitch.com/20110331-tg8wfpdk899s7ep1xghx1rwmy4.jpg

Also see profiling output here:
http://dl.dropbox.com/u/269071/temp/compressor_profile_orig.txt
http://dl.dropbox.com/u/269071/temp/compressor_profile_modified.txt

Interested in your thoughts. Thanks :)

CompressorFileStorage' object has no attribute 'modified_time'

Hi,
I setup a server today (Django 1.2.4), and i'm using both django_staicfiles (v1.0b2) and django_compressor (dev). I'm also using django_storages (dev) with S3 backend storage for MEDIA only. All STATIC files are served through apache, while MEDIA files are served with Amazon S3.
When collecting STATIC files the following error is raised :
$ python manage.py collectstatic --noinput
Traceback (most recent call last):
File "manage.py", line 11, in
execute_manager(settings)
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/django/core/management/init.py", line 438, in execute_manager
utility.execute()
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/django/core/management/init.py", line 379, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/django/core/management/base.py", line 191, in run_from_argv
self.execute(_args, *_options.dict)
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/django/core/management/base.py", line 220, in execute
output = self.handle(_args, _options)
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/django/core/management/base.py", line 351, in handle
return self.handle_noargs(
_options)
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/staticfiles/management/commands/collectstatic.py", line 89, in handle_noargs
self.copy_file(path, prefixed_path, storage, *_options)
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/staticfiles/management/commands/collectstatic.py", line 184, in copy_file
if not self.delete_file(path, prefixed_path, source_storage, **options):
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/staticfiles/management/commands/collectstatic.py", line 125, in delete_file
source_last_modified = source_storage.modified_time(path)
AttributeError: 'CompressorFileStorage' object has no attribute 'modified_time'

What am i doing wrong ? Or is there a bug somewhere ?
Cheers,

Allow debugging in prod (old feature ?DEBUG=1)

In an old version of compressor (0.6a3, I think), CompressorNode had this gross but useful hack:

    request = context.get('request')
    ...
    if request and request.REQUEST.get('DEBUG'):
        return content

That is, if the request has a querystring DEBUG, return the uncompressed content. This was useful for debugging in prod, and it doesn't exist in latest (0.6b5).

I can see why that hack was removed (or not included in this line of development), but since it is a useful feature, perhaps we can introduce a setting:

 CONTEXT_DEBUG_VARIABLE = None
 # e.g. "request.GET.NO_COMPRESS"

CompressorNode could then resolve the variable and return uncompressed if it resolved to something truthy.

Thoughts?

Shared cache keys

We're running a web app on a small virtual machine cluster behind a proxy. Each web app shares the compress output directory and we'd like for all apps to share the same compressed CSS and JS files. Because get_cachekey seems to use the server hostname when generating the cache key, we're ending up with a compressed resource for each app instance.

def get_cachekey(key):
    return ("django_compressor.%s.%s" % (socket.gethostname(), key))

Can you suggest a patch that would allow our individual app instances to all share the same compressed resources? Possibly adding a settings flag to toggle the use of the hostname in the cache key, if that is indeed the only issue.

I'll do some hacking on this myself and see if I can come up with an elegant solution, but was wondering if you had any bright ideas.

Thanks!

CssAbsoluteFilter fails when using django-static

CssAbsoluteFilter does not replace url(...) in CSS files when the CSS is not present in the compressed root but in some other directory.

I believe the problem is around

css_default.py file

if not filename or not filename.startswith(self.root):
return self.content

When MEDIA_ROOT is pointing to a non existing dir, compressor fails silently

When MEDIA_ROOT is pointing to a non existing dir (or maybe a directory that cannot be created because of wrong permissions) ), compressor fails silently. It writes the entry in the cache but it does not create a file (for obvious reasons) and does not output the link in the template

This happened with python 2.5 on a slackware install with django 1.3 , apache2 and mod_wsgi

I will try to create a patch for this.

Compressed filename doesn't depend on included source

There's a feature regression in commit 15ae5ae (wich fixed bug #18), after replacing
new_filepath = self.filepath(content) with new_filepath = self.filepath(self.content) filepath of output file no longer depends on contents of processed files. It's beacause self.content contains html and content was css(js) minified source.

Since I might want to have COMPRESS = True on my development and after my js-files are changed (but not their names), compressor won't create compressed js files again, because there is already such file from previous run.

jsmin filter fails to produce correct output on complex script

The jsmin filter fails to produce the correct output on a complex script (included below), a line break is inserted in the wrong place. The same javascript is compressed correctly with the older jsmin filter included in the django-compressor 0.5.3 (mintchaos?).

The line of code causing the issue is the '/.../' comment at the end of the script after the var assignment. If I remove the comment the compressor generates a working compressed javascript.

Diff between generated working and broken javascripts

$ LC_ALL=en_US diff -c widget-working.js widget-broken.js 
*** widget-working.js   2011-05-03 11:09:41.553279754 +0200
--- widget-broken.js    2011-05-03 10:25:59.120443254 +0200
***************
*** 46,50 ****
  var addToolbarHtmlClickEventListener=function(){jQuery(document.html).click(function(event){jQuery('#kth-toolbar ul li ul').hide();event.stopPropagation();});}
  var mouseLeaveTimer;var isApiDataLoaded;function renderKthToolbar(){renderInitialToolbar();initializeToolbar("#kth-toolbar","#showkth-toolbar");addToolbarDeactivateClickEventListener("#hidekth-toolbar","#kth-toolbar","#showkth-toolbar");addToolbarActivateClickEventListener("#kth-toolbar","#showkth-toolbar");addToolbarHtmlClickEventListener();jQuery(".kth-toolbar-menu").live('mouseleave',function(){mouseLeaveTimer=window.setTimeout(function(){hideAllMenus();mouseLeaveTimer=null;},500);});jQuery(".kth-toolbar-menu").live('mouseenter',function(){if(mouseLeaveTimer){window.clearTimeout(mouseLeaveTimer);}});isApiDataLoaded=false;window.setTimeout(function(){if(!isApiDataLoaded){renderErrorToolbar();}},10000);jQuery.ajax({url:"http://fjo.ite.kth.se:8081/toolbar/api/1.0/"+KthToolbarConfig.language,dataType:"jsonp",jsonpCallback:"KthToolbarJSONPLoader",success:function(toolbarapi_data){isApiDataLoaded=true;if(toolbarapi_data.status=="OK"){renderPersonalizedToolbar(toolbarapi_data);}else if(toolbarapi_data.status=="anonymous"){renderAnonymousToolbar(toolbarapi_data);}else{renderErrorToolbar();}}
  });}
! jQuery(document).ready(function(){renderKthToolbar();});}
! KthToolbar();
\ No newline at end of file
--- 46,49 ----
  var addToolbarHtmlClickEventListener=function(){jQuery(document.html).click(function(event){jQuery('#kth-toolbar ul li ul').hide();event.stopPropagation();});}
  var mouseLeaveTimer;var isApiDataLoaded;function renderKthToolbar(){renderInitialToolbar();initializeToolbar("#kth-toolbar","#showkth-toolbar");addToolbarDeactivateClickEventListener("#hidekth-toolbar","#kth-toolbar","#showkth-toolbar");addToolbarActivateClickEventListener("#kth-toolbar","#showkth-toolbar");addToolbarHtmlClickEventListener();jQuery(".kth-toolbar-menu").live('mouseleave',function(){mouseLeaveTimer=window.setTimeout(function(){hideAllMenus();mouseLeaveTimer=null;},500);});jQuery(".kth-toolbar-menu").live('mouseenter',function(){if(mouseLeaveTimer){window.clearTimeout(mouseLeaveTimer);}});isApiDataLoaded=false;window.setTimeout(function(){if(!isApiDataLoaded){renderErrorToolbar();}},10000);jQuery.ajax({url:"http://fjo.ite.kth.se:8081/toolbar/api/1.0/"+KthToolbarConfig.language,dataType:"jsonp",jsonpCallback:"KthToolbarJSONPLoader",success:function(toolbarapi_data){isApiDataLoaded=true;if(toolbarapi_data.status=="OK"){renderPersonalizedToolbar(toolbarapi_data);}else if(toolbarapi_data.status=="anonymous"){renderAnonymousToolbar(toolbarapi_data);}else{renderErrorToolbar();}}
  });}
! jQuery(document).ready(function(){renderKthToolbar();});}KthToolbar();
\ No newline at end of file

The uncompressed source for the script.

if (!KthToolbarConfig) {
    var KthToolbarConfig = {};
}
if (!KthToolbarConfig.frontUrl) {
    KthToolbarConfig.frontUrl = "http://fjo.ite.kth.se:8081";
}
if (!KthToolbarConfig.loginUrl) {
    KthToolbarConfig.loginUrl = "http://fjo.ite.kth.se:8081/accounts/login/?next=" + escape(location.href);
} else {
    if (KthToolbarConfig.loginUrl.charAt(0) == '?') {
    KthToolbarConfig.loginUrl = "http://fjo.ite.kth.se:8081/accounts/login/?next=" + escape(location.href + KthToolbarConfig.loginUrl);
    } else {
    KthToolbarConfig.loginUrl = "http://fjo.ite.kth.se:8081/accounts/login/?next=" + escape(KthToolbarConfig.loginUrl);
    }
}
if (!KthToolbarConfig.loginMethod) {
    KthToolbarConfig.loginMethod = 'POST';
}
if (!KthToolbarConfig.locale) {
    KthToolbarConfig.locale = "sv_SE";
}
KthToolbarConfig.language = KthToolbarConfig.locale.split('_')[0];



if (KthToolbar && console && console.warn) {
    console.warn("KthToolbar already defined on page!");
}


var KthToolbar = function() {
    jQuery("head").append("<link rel='stylesheet' type='text/css' href='" + KthToolbarConfig.frontUrl + "/static/toolbar/toolbar-screen.css'>" +
                          "<link rel='stylesheet' type='text/css' href='" + KthToolbarConfig.frontUrl + "/static/toolbar/reset-context-min.css'>" +
                          "<!--[if IE 7]><link rel='stylesheet' type='text/css' href='" + KthToolbarConfig.frontUrl + "/static/toolbar/ie7-toolbar-screen.css'><![endif]-->");

    var lang = {
        "Startsida för www.kth.se": { en: "Start page for www.kth.se" },
        "Dölj": { en: "Hide" },
        "Dölj toolbaren": { en: "Hide toolbar" },
        "Mina sidor": { en: "My pages" },
        "Visa": { en: "Show" },
        "Logga in": { en: "Log in" },
        "Laddar personlig information": { en: "Loading personalized information" },
        "Kunde inte ladda personlig information (klicka för att försöka igen)": { en: "Failed to load customized information (click to retry)" }
    };   

    function translate(text) {
        function trans(sv) {    
            var translations = lang[sv];
            if (!translations) {
                return sv;
            }    
            var translation = translations[KthToolbarConfig.language];
            if (!translation) {
                return sv;
            }
            return translation;
        }

        return text.replace(/#\{(.*?)\}/g, function(str, p1) {
            return trans(p1);
        });
    }

    /**
    * Create the initial toolbar that is displayed until more information i known
    */
    function renderInitialToolbar() {
        var html = "<div class='yui3-cssreset'><div id='kth-toolbar' class='yui3-cssreset'><ul class='kth-toolbar-items'><li class='kth-toolbar-left kth-toolbar-home'><a href='http://www.kth.se' title='#{Startsida för www.kth.se}'><img src='" + KthToolbarConfig.frontUrl + "/static/toolbar/kth-logo-24-24.png' width='24' height='24'></a></li><li id='kth-toolbar-profile' class='kth-toolbar-left'><span style='color: #cccccc'>#{Laddar personlig information}...</span></li><li id='kth-toolbar-linksets-insertion-point' style='display: none'></li><li id='hidekth-toolbar' title='#{Dölj toolbaren}'>#{Dölj}</li><li id='kth-toolbar-webmail' class='kth-toolbar-right kth-toolbar-button'><a href='https://webmail.kth.se'>#{Webmail}</a></li><li id='kth-toolbar-my-pages' class='kth-toolbar-right kth-toolbar-button'><a id='kth-toolbar-mypages-link'>#{Mina sidor}</a></li></ul></div><div id='showkth-toolbar'>#{Visa}</div></div>";
        jQuery('body').append(translate(html));
        jQuery('#kth-toolbar-mypages-link').attr('href', 'https://www.kth.se/student/minasidor/?l=' + KthToolbarConfig.locale);
    }

   /**
    * adjust toolbar for (async deduced) error situation so personal/loggedin cannot be known
    */
    function renderErrorToolbar() {
        jQuery('#kth-toolbar #kth-toolbar-profile').html(translate("<a href='http://fjo.ite.kth.se:8081/accounts/login/?next=" + escape(location.href) + "'>#{Kunde inte ladda personlig information (klicka för att försöka igen)}</a>"));
    }

   /**
    * adjust toolbar for (async deduced) not logged in
    */
    function renderAnonymousToolbar(toolbarapi_data) {
        jQuery('#kth-toolbar #kth-toolbar-profile').html(translate("<form action='" + KthToolbarConfig.loginUrl + "' method='" + KthToolbarConfig.loginMethod + "'><input type='submit' name='kth-toolbar-login-button' value='#{Logga in}' class='kth-toolbar-login' /></form>"));
    }

   /**
    * adjust toolbar for (async fetched) personal information
    */
    function renderPersonalizedToolbar(toolbarapi_data) {
        /* NOTE! name and avatar_html is leaked into the global name space on purpose. */
        name = '';
        avatar_html = '';

        if (toolbarapi_data.avatar) {
                avatar_html = '<span class="kth-toolbar-small-profile-picture"><img src="' + toolbarapi_data.avatar.url + '" alt="' + toolbarapi_data.avatar.alt + '" width="' + toolbarapi_data.avatar.width + '" height="' + toolbarapi_data.avatar.height + '"></span>';
        }
        name = toolbarapi_data.name.firstname + ' ' + toolbarapi_data.name.lastname;

        jQuery('#kth-toolbar #kth-toolbar-profile').html('<a href="' + toolbarapi_data.homeurl + '" class="kth-toolbar-profile-link">'
            + avatar_html +
            '<span class="kth-toolbar-label">' + name + '</span></a>');

        jQuery("#kth-toolbar-insert-username").html(name);


        jQuery.each(toolbarapi_data.linksets, function(index, linkset) {
            if (linkset.links.length > 0) {
                var html = "<li id='" + index + "' class='kth-toolbar-left kth-toolbar-menu " + linkset.type + "'><ul id='kth-toolbar-toolbar-menu" + index + "' class='kth-toolbar-menuitems'>";
                jQuery.each(linkset.links, function(index, link) {
                    html += "<li class='kth-toolbar-item'>";
                    html += "<a href='" + link.url + "' class='kth-toolbar-label'>" + link.name + "</a>";
                    if (link.extra) {
                        html += "<a class='kth-toolbar-extra' href='" + link.url + "'>" + link.extra + "</a>";
                    }
                    html += "</li>";
                });
                html += "</ul><span class='kth-toolbar-button title'>" + linkset.name + "</span></li>";
                jQuery("#kth-toolbar-linksets-insertion-point").before(html);

                addMenuActivatorClickEventListener("#kth-toolbar-toolbar-menu" + index, "#" + index);
            }
        });    
    }

    /**
    * Toolbar functions
    */
    function slideElementVertically(element, from, to, animationSpeed) {
        jQuery(element).css({height: from}).animate({ height: to }, animationSpeed);
    }

    /**
    * Shows the toolbar by sliding it up.
    *
    * @param toolbarId the actual toolbar element.
    */
    function showToolbar(toolbarId, animationSpeed) {
        slideElementVertically(toolbarId, 0, 34, animationSpeed);
    }

    /**
    * Hides the toolbar by sliding it down.
    *
    * @param toolbarId the actual toolbar element.
    */
    function hideToolbar(toolbarId) {
        slideElementVertically(toolbarId, 34, 0, 'slow');
    }

    /**
    * Shows the "Show toolbar" tab by sliding it up.
    *
    * @param tabId the element for the "Show toolbar".
    */
    function showTab(tabId) {
        slideElementVertically(tabId, -5, 24, 'slow');
    }

    /**
    * Hides the "Show toolbar" tab by sliding it down.
    *
    * @param tabId the element for the "Show toolbar".
    */
    function hideTab(tabId) {
        slideElementVertically(tabId, 24, -5, 'slow');
    }

    /**
    * Checks if the toolbar is activated (visible).
    *
    * @param toolbarId the actual toolbar element.
    * @return true if the toolbar is activated, else false.
    */
    function isActivated(toolbarId) {
        if(jQuery(toolbarId).height() > 0) {
            return true;
        } else {
            return false;
        }
    }

    /**
    * Hides all the menus in the toolbar.
    */
    function hideAllMenus() {
        jQuery('#kth-toolbar ul li ul').hide();
        jQuery('#kth-toolbar .kth-toolbar-items .kth-toolbar-left.kth-toolbar-menu.kth-toolbar-active').removeClass('kth-toolbar-active');
    }

    /**
    * Gets the toolbar visibility state value from the kthToolbar cookie.
    *
    * @param name the name of the cookie to retrieve the toolbar state from.
    * @return the visibility state of the toolbar (true/false) .
    */
    function getToolbarCookie(name) {
        if (document.cookie.length > 0) {
            start = document.cookie.indexOf(name + "=");
            if (start != -1) {
                start = start + name.length+1;
                end = document.cookie.indexOf(";",start);
                if (end == -1) {
                    end = document.cookie.length;
                }
                return unescape(document.cookie.substring(start,end));
            }
        }
        return "";
    }

    /**
    * Sets a kthToolbar cookie and prepare it with the toolbar state
    * and expire time.
    *
    * @param name the name of the cookie
    * @param value the value/state (true or false)
    * @param expiredays number of days until the cookie should expire.
    */
    function setToolbarCookies(name, value, expiredays) {
        var exdate=new Date();
        exdate.setDate(exdate.getDate() + expiredays);
        var domain="kth.se";
        var path="/";
        document.cookie = name + "=" +escape( value ) +
            ( ( expiredays ) ? ";expires=" + exdate.toUTCString() : "" ) +
            ( ( path ) ? ";path=" + path : "" ) +
            ( ( domain ) ? ";domain=" + domain : "" );
    }

    /**
    * Activates the toolbar and deactivates the "Show toolbar" tab.
    *
    * @param toolbarId the actual toolbar element.
    * @param tabId the element for the "Show toolbar".
    */
    function activateToolbar(toolbarId, tabId) {
        hideTab(tabId);
        showToolbar(toolbarId, 600);
    }

    /**
    * Deactivates the toolbar, hide all menus, and activates
    * the "Show toolbar" tab.
    *
    * @param toolbarId the actual toolbar element.
    * @param tabId the element for the "Show toolbar".
    */
    function deActivateToolbar(toolbarId, tabId) {
        hideAllMenus();
        hideToolbar(toolbarId);
        showTab(tabId);
    }

    /**
    * Calculates the menu position based on the number of
    * elements in the menu and the height of the toolbar.
    * 
    * The browser specific code sucks, but is the best I 
    * can figure out thus far. /fjo 20110103
    *
    * @param menuId element
    * @return
    */
    function calculateMenuPosition(menuId) {
        var maxValue = 404;
        var items = jQuery(menuId).children().size();
        var itemHeight = 25;
        var adjustment = 4;

        if (jQuery.browser.msie) {
            var pos = (items * itemHeight) + adjustment;
        } else {
            var pos = jQuery(menuId).height() + adjustment;
        }

        if (pos < maxValue) {
            return pos;
        } else {
            return maxValue;
        }
    }

    /**
    * Initializes the toolbar by checking the cookie for the toolar state.
    * If no cookie is set, show the toolar and hide the "Show toolbar" tab.
    *
    * @param toolbarId the actual toolbar element.
    * @param tabId the element for the "Show toolbar".
    */
    function initializeToolbar(toolbarId, tabId) {
        var isVisible = getToolbarCookie("kthToolbar");
        if (isVisible=="") {
            jQuery(tabId).toggle();
            showToolbar(toolbarId, 0);
        } else if (isVisible == "true") {
            jQuery(toolbarId).css({height: 34});
            jQuery(tabId).toggle();
        } else if (isVisible == "false") {
            jQuery(tabId).css({height: 20});
        }
    }
    /**
    * Deactivates the toolbar and shows the "Show toolbar" tab.
    * This function also sets a cookie to remember the toolbar state.
    *
    * @param activatorId the activator element for the toolbar.
    * @param toolbarId the actual toolbar element.
    * @param tabId the element for the "Show toolbar".
    */
    function addToolbarDeactivateClickEventListener(activatorId, toolbarId, tabId) {
        jQuery(activatorId).click(function (event) {
            if(isActivated(toolbarId)) {
                deActivateToolbar(toolbarId, tabId);
                setToolbarCookies("kthToolbar", "false", 7);
            }
            event.stopPropagation();
        });
    }
    /**
    * Activates the toolbar and hides the "Show toolbar" tab.
    * This function also sets a cookie to remember the toolbar state.
    *
    * @param toolbarId the actual toolbar element.
    * @param tabId the element for the "Show toolbar".
    */
    function addToolbarActivateClickEventListener(toolbarId, tabId) {
        jQuery(tabId).click(function (event) {
            activateToolbar(toolbarId, tabId);
            setToolbarCookies("kthToolbar", "true", 7);
            event.stopPropagation();
        });
    }

    /**
    * Check if the given menu is showing.
    *
    * @param menuId the element containing the menu.
    * @return true if the menu is showing, else false.
    */
    function showingMenu(menuId) {
        var display = jQuery(menuId).css('display');
        if (display != 'none') {
            return true;
        } else {
            return false;
        }
    }

    /**
    * Toggle the given menu element and calculates
    * the position of the expanded menu.
    *
    * @param menuId the element of the active menu.
    */
    function toggleMenu(menuId) {
        var pos = calculateMenuPosition(menuId);
        jQuery(menuId).css('top', -pos);
        jQuery(menuId).toggle();
    }

    /**
    * Toggle the menus depending on their states.
    *
    * @param menuId the element that contains the menu.
    * @param event the click event on the menu activator element.
    */
    function toggleMenus(menuId, event, activator) {
        var toggle = true;
        if(showingMenu(menuId)) {
            toggle = false;
        }
        hideAllMenus();
        if(toggle) {
            toggleMenu(menuId);
            jQuery(activator).addClass('kth-toolbar-active');
        }
        event.stopPropagation();
    }

    /**
    * Adds a click event listener for the given menu button on the toolbar.
    */
    function addMenuActivatorClickEventListener(menuId, activator) {
        jQuery(activator).click(function (event) {
            toggleMenus(menuId, event, activator);
        });
    }

    /**
    * Close the menus if the HTML element is clicked.
    */
    var addToolbarHtmlClickEventListener = function() {
        jQuery(document.html).click(function (event) {
            jQuery('#kth-toolbar ul li ul').hide();
            event.stopPropagation();
        });
    }


    var mouseLeaveTimer;
    var isApiDataLoaded;

    /**
    * Main bootstrapt function that creates the toolbar and fills it,
    * intented to be called once the page is loaded
    */
    function renderKthToolbar() {
        renderInitialToolbar();
        initializeToolbar("#kth-toolbar", "#showkth-toolbar");
        addToolbarDeactivateClickEventListener("#hidekth-toolbar", "#kth-toolbar", "#showkth-toolbar");
        addToolbarActivateClickEventListener("#kth-toolbar", "#showkth-toolbar");
        addToolbarHtmlClickEventListener();

        jQuery(".kth-toolbar-menu").live('mouseleave', function() {
            mouseLeaveTimer = window.setTimeout(function() {
               hideAllMenus();
               mouseLeaveTimer = null;
            }, 500);
        });
        jQuery(".kth-toolbar-menu").live('mouseenter', function() {
            if (mouseLeaveTimer) {
                window.clearTimeout(mouseLeaveTimer);
            }
        });

        isApiDataLoaded = false;
        window.setTimeout(function() {
           if (!isApiDataLoaded) {
                renderErrorToolbar();
           }
        }, 10000);

        jQuery.ajax({
          url: "http://fjo.ite.kth.se:8081/toolbar/api/1.0/" + KthToolbarConfig.language,
          dataType: "jsonp",
          jsonpCallback: "KthToolbarJSONPLoader",
          success: function(toolbarapi_data) {
            isApiDataLoaded = true;
            if (toolbarapi_data.status == "OK") {
                renderPersonalizedToolbar(toolbarapi_data);
            } else if (toolbarapi_data.status == "anonymous") {
                renderAnonymousToolbar(toolbarapi_data);
            } else {
                renderErrorToolbar();
            }
          }
          /*timeout, error and complete do not work when we use jsonp in 1.4 */
        });
    }  

    /**
    * Activate when document is loaded.
    */
    jQuery(document).ready(function () {
        renderKthToolbar();
    });
}; /*end KthToolbar*/

KthToolbar();

Compressor generates new directories recursively

I have the following settings in settings.py:

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
STATIC_URL = MEDIA_URL + 'static/'
STATIC_ROOT = os.path.join(MEDIA_ROOT, 'static')
STATICFILES_DIRS = ('%s/static/' % BASE_DIR,)
STATICFILES_FINDERS = (
     'django.contrib.staticfiles.finders.FileSystemFinder',
     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
     'django.contrib.staticfiles.finders.DefaultStorageFinder',
     'compressor.finders.CompressorFinder',
) 

It seems Django compressor is over time generating new directories under /media/static/static/static/... with the contents of the immediately preceding directory.

gather, concatenate and *then* pass to filters?

Hi! Is there anyway to collect and concatenate the CSS or JS in a particular block and then pass it through a filter chain? In particular, I'd like to use pyScss to process my SCSS. I've created a pySCSSFilter extending FilterBase, and I'd really like to do something like:

{% compress css %}
<link rel="stylesheet" type="text/css" media="screen" href="{{ MEDIA_URL }}css/mixins.scss" />
<link rel="stylesheet" type="text/css" media="screen" href="{{ MEDIA_URL }}css/base.scss" />
<link rel="stylesheet" type="text/css" media="screen" href="{{ MEDIA_URL }}css/other.scss" />
{% endcompress %}

but of course doesn't won't work as my files are processed in isolation from one another.

Is there a way?

Cheers!

New jsmin chokes on some javascript comments

Since 37cb543 (I haven't done a bisect but it's between 0.6.2 and 0.7.1 and I don't see any other changes in js filters) django_compressor's jsmin filter chokes on some very specific javascript code. Here is a testcase:

var a = 'bar';
switch(a) {
    case 'foo':// foo //
    break;
}
alert(a);

Here is the jsmin output:

var a='bar';switch(a){case'foo'://foobreak;}
alert(a);

Note the end of the comment, which was kept. While it's a rather ugly way of writing comments (and it might even be invalid, didn't check the spec) but it used to work before. Is it fixable ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.