Added support for pluggable storage backends to agnostically handle the actual build location.

This commit is contained in:
Jannis Leidel
2010-02-10 18:42:28 +01:00
parent 7f314c5134
commit 4701bd3a55
4 changed files with 89 additions and 35 deletions

View File

@@ -5,6 +5,9 @@ from django import template
from django.conf import settings as django_settings from django.conf import settings as django_settings
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.core.files.base import ContentFile
from django.core.files.storage import get_storage_class
from compressor.conf import settings from compressor.conf import settings
from compressor import filters from compressor import filters
@@ -42,16 +45,12 @@ class Compressor(object):
raise NotImplementedError('split_contents must be defined in a subclass') raise NotImplementedError('split_contents must be defined in a subclass')
def get_filename(self, url): def get_filename(self, url):
if not url.startswith(settings.MEDIA_URL): if not url.startswith(self.storage.base_url):
raise UncompressableFileError('"%s" is not in COMPRESS_URL ("%s") and can not be compressed' % (url, settings.MEDIA_URL)) raise UncompressableFileError('"%s" is not in COMPRESS_URL ("%s") and can not be compressed' % (url, self.storage.base_url))
# .lstrip used to remove leading slashes because os.path.join basename = url.replace(self.storage.base_url, "", 1)
# counterintuitively takes "/foo/bar" and "/baaz" to produce "/baaz", if not self.storage.exists(basename):
# not the "/foo/bar/baaz" which you might expect: raise UncompressableFileError('"%s" does not exist' % self.storage.path(basename))
basename = url.replace(settings.MEDIA_URL, "", 1).lstrip("/") return self.storage.path(basename)
filename = os.path.join(settings.MEDIA_ROOT, basename)
if not os.path.exists(filename):
raise UncompressableFileError('"%s" does not exist' % (filename,))
return filename
@property @property
def mtimes(self): def mtimes(self):
@@ -64,6 +63,10 @@ class Compressor(object):
cachestr = "".join(cachebits) cachestr = "".join(cachebits)
return "django_compressor.%s" % get_hexdigest(cachestr)[:12] return "django_compressor.%s" % get_hexdigest(cachestr)[:12]
@property
def storage(self):
return get_storage_class(settings.STORAGE)()
@property @property
def hunks(self): def hunks(self):
if getattr(self, '_hunks', ''): if getattr(self, '_hunks', ''):
@@ -119,28 +122,20 @@ class Compressor(object):
@property @property
def new_filepath(self): def new_filepath(self):
filename = "".join([self.hash, self.extension]) filename = "".join([self.hash, self.extension])
filepath = "%s/%s/%s" % (settings.OUTPUT_DIR.strip('/'), self.ouput_prefix, filename) return "/".join((settings.OUTPUT_DIR.strip('/'), self.ouput_prefix, filename))
return filepath
def save_file(self): def save_file(self):
filename = "%s/%s" % (settings.MEDIA_ROOT.rstrip('/'), self.new_filepath) if self.storage.exists(self.new_filepath):
if os.path.exists(filename):
return False return False
dirname = os.path.dirname(filename) self.storage.save(self.new_filepath, ContentFile(self.combined))
if not os.path.exists(dirname):
os.makedirs(dirname)
fd = open(filename, 'wb+')
fd.write(self.combined)
fd.close()
return True return True
def output(self): def output(self):
if not settings.COMPRESS: if not settings.COMPRESS:
return self.content return self.content
url = "%s/%s" % (settings.MEDIA_URL.rstrip('/'), self.new_filepath)
self.save_file() self.save_file()
context = getattr(self, 'extra_context', {}) context = getattr(self, 'extra_context', {})
context['url'] = url context['url'] = self.storage.url(self.new_filepath)
return render_to_string(self.template_name, context) return render_to_string(self.template_name, context)
@@ -149,7 +144,10 @@ class CssCompressor(Compressor):
def __init__(self, content, ouput_prefix="css"): def __init__(self, content, ouput_prefix="css"):
self.extension = ".css" self.extension = ".css"
self.template_name = "compressor/css.html" self.template_name = "compressor/css.html"
self.filters = ['compressor.filters.css_default.CssAbsoluteFilter', 'compressor.filters.css_default.CssMediaFilter'] self.filters = [
'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.css_default.CssMediaFilter',
]
self.filters.extend(settings.COMPRESS_CSS_FILTERS) self.filters.extend(settings.COMPRESS_CSS_FILTERS)
self.type = 'css' self.type = 'css'
super(CssCompressor, self).__init__(content, ouput_prefix) super(CssCompressor, self).__init__(content, ouput_prefix)

View File

@@ -5,6 +5,7 @@ from django.conf import settings
MEDIA_URL = getattr(settings, 'COMPRESS_URL', settings.MEDIA_URL) MEDIA_URL = getattr(settings, 'COMPRESS_URL', settings.MEDIA_URL)
MEDIA_ROOT = getattr(settings, 'COMPRESS_ROOT', settings.MEDIA_ROOT) MEDIA_ROOT = getattr(settings, 'COMPRESS_ROOT', settings.MEDIA_ROOT)
OUTPUT_DIR = getattr(settings, 'COMPRESS_OUTPUT_DIR', 'CACHE') OUTPUT_DIR = getattr(settings, 'COMPRESS_OUTPUT_DIR', 'CACHE')
STORAGE = getattr(settings, 'COMPRESS_STORAGE', 'compressor.storage.CompressorFileStorage')
COMPRESS = getattr(settings, 'COMPRESS', not settings.DEBUG) COMPRESS = getattr(settings, 'COMPRESS', not settings.DEBUG)
COMPRESS_CSS_FILTERS = getattr(settings, 'COMPRESS_CSS_FILTERS', []) COMPRESS_CSS_FILTERS = getattr(settings, 'COMPRESS_CSS_FILTERS', [])

20
compressor/storage.py Normal file
View File

@@ -0,0 +1,20 @@
from django.core.files.storage import FileSystemStorage
from django.core.files.storage import get_storage_class
from compressor.conf import settings
class CompressorFileStorage(FileSystemStorage):
"""
Standard file system storage for files handled by django-compressor.
The defaults for ``location`` and ``base_url`` are ``COMPRESS_ROOT`` and
``COMPRESS_URL``.
"""
def __init__(self, location=None, base_url=None, *args, **kwargs):
if location is None:
location = settings.MEDIA_ROOT
if base_url is None:
base_url = settings.MEDIA_URL
super(CompressorFileStorage, self).__init__(location, base_url,
*args, **kwargs)

View File

@@ -1,9 +1,12 @@
import os, re import os, re
import gzip
from django.template import Template, Context from django.template import Template, Context
from django.test import TestCase from django.test import TestCase
from compressor import CssCompressor, JsCompressor from compressor import CssCompressor, JsCompressor
from compressor.conf import settings from compressor.conf import settings
from compressor.storage import CompressorFileStorage
from django.conf import settings as django_settings from django.conf import settings as django_settings
from BeautifulSoup import BeautifulSoup from BeautifulSoup import BeautifulSoup
@@ -148,11 +151,7 @@ class CssMediaTestCase(TestCase):
out = u'@media screen {body { background:#990; }}\n@media print {p { border:5px solid green;}}\n@media all {body { color:#fff; }}' out = u'@media screen {body { background:#990; }}\n@media print {p { border:5px solid green;}}\n@media all {body { color:#fff; }}'
self.assertEqual(out, self.cssNode.combined) self.assertEqual(out, self.cssNode.combined)
class TemplatetagTestCase(TestCase): def render(template_string, context_dict=None):
def setUp(self):
settings.COMPRESS = True
def render(self, template_string, context_dict=None):
"""A shortcut for testing template output.""" """A shortcut for testing template output."""
if context_dict is None: if context_dict is None:
context_dict = {} context_dict = {}
@@ -161,6 +160,10 @@ class TemplatetagTestCase(TestCase):
t = Template(template_string) t = Template(template_string)
return t.render(c).strip() return t.render(c).strip()
class TemplatetagTestCase(TestCase):
def setUp(self):
settings.COMPRESS = True
def test_css_tag(self): def test_css_tag(self):
template = u"""{% load compress %}{% compress css %} template = u"""{% load compress %}{% compress css %}
<link rel="stylesheet" href="{{ MEDIA_URL }}css/one.css" type="text/css" charset="utf-8"> <link rel="stylesheet" href="{{ MEDIA_URL }}css/one.css" type="text/css" charset="utf-8">
@@ -170,7 +173,7 @@ class TemplatetagTestCase(TestCase):
""" """
context = { 'MEDIA_URL': settings.MEDIA_URL } context = { 'MEDIA_URL': settings.MEDIA_URL }
out = u'<link rel="stylesheet" href="/media/CACHE/css/f7c661b7a124.css" type="text/css" media="all" charset="utf-8">' out = u'<link rel="stylesheet" href="/media/CACHE/css/f7c661b7a124.css" type="text/css" media="all" charset="utf-8">'
self.assertEqual(out, self.render(template, context)) self.assertEqual(out, render(template, context))
def test_nonascii_css_tag(self): def test_nonascii_css_tag(self):
template = u"""{% load compress %}{% compress css %} template = u"""{% load compress %}{% compress css %}
@@ -180,7 +183,7 @@ class TemplatetagTestCase(TestCase):
""" """
context = { 'MEDIA_URL': settings.MEDIA_URL } context = { 'MEDIA_URL': settings.MEDIA_URL }
out = '<link rel="stylesheet" href="/media/CACHE/css/68da639dbb24.css" type="text/css" media="all" charset="utf-8">' out = '<link rel="stylesheet" href="/media/CACHE/css/68da639dbb24.css" type="text/css" media="all" charset="utf-8">'
self.assertEqual(out, self.render(template, context)) self.assertEqual(out, render(template, context))
def test_js_tag(self): def test_js_tag(self):
template = u"""{% load compress %}{% compress js %} template = u"""{% load compress %}{% compress js %}
@@ -190,5 +193,37 @@ class TemplatetagTestCase(TestCase):
""" """
context = { 'MEDIA_URL': settings.MEDIA_URL } context = { 'MEDIA_URL': settings.MEDIA_URL }
out = u'<script type="text/javascript" src="/media/CACHE/js/3f33b9146e12.js" charset="utf-8"></script>' out = u'<script type="text/javascript" src="/media/CACHE/js/3f33b9146e12.js" charset="utf-8"></script>'
self.assertEqual(out, self.render(template, context)) self.assertEqual(out, render(template, context))
class TestStorage(CompressorFileStorage):
"""
Test compressor storage that gzips storage files
"""
def url(self, name):
return u'%s.gz' % super(TestStorage, self).url(name)
def save(self, filename, content):
filename = super(TestStorage, self).save(filename, content)
out = gzip.open(u'%s.gz' % self.path(filename), 'wb')
out.writelines(open(self.path(filename), 'rb'))
out.close()
class StorageTestCase(TestCase):
def setUp(self):
self._storage = settings.STORAGE
settings.STORAGE = 'core.tests.TestStorage'
settings.COMPRESS = True
def tearDown(self):
settings.STORAGE = self._storage
def test_css_tag_with_storage(self):
template = u"""{% load compress %}{% compress css %}
<link rel="stylesheet" href="{{ MEDIA_URL }}css/one.css" type="text/css" charset="utf-8">
<style type="text/css">p { border:5px solid white;}</style>
<link rel="stylesheet" href="{{ MEDIA_URL }}css/two.css" type="text/css" charset="utf-8">
{% endcompress %}
"""
context = { 'MEDIA_URL': settings.MEDIA_URL }
out = u'<link rel="stylesheet" href="/media/CACHE/css/5b231a62e9a6.css.gz" type="text/css" media="all" charset="utf-8">'
self.assertEqual(out, render(template, context))