mirror of
https://github.com/Hopiu/django-imagekit.git
synced 2026-03-18 06:10:24 +00:00
Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31eab7666a |
55 changed files with 327 additions and 880 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -4,14 +4,10 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.tox
|
.tox
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
MANIFEST
|
MANIFEST
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
/tests/media/*
|
/tests/media/*
|
||||||
!/tests/media/reference.png
|
!/tests/media/lenna.png
|
||||||
/venv
|
/venv
|
||||||
/venv3
|
|
||||||
/.env
|
/.env
|
||||||
/tags
|
|
||||||
|
|
|
||||||
34
.travis.yml
34
.travis.yml
|
|
@ -1,35 +1,7 @@
|
||||||
sudo: false
|
|
||||||
|
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- "3.8"
|
- 2.7
|
||||||
- "3.7"
|
install: pip install tox --use-mirrors
|
||||||
- "3.6"
|
script: tox
|
||||||
- "3.5"
|
|
||||||
env:
|
|
||||||
- DJANGO="master"
|
|
||||||
- DJANGO="30"
|
|
||||||
- DJANGO="22"
|
|
||||||
- DJANGO="21"
|
|
||||||
- DJANGO="21"
|
|
||||||
- DJANGO="20"
|
|
||||||
- DJANGO="111"
|
|
||||||
|
|
||||||
install:
|
|
||||||
- pip install tox
|
|
||||||
|
|
||||||
script:
|
|
||||||
- tox -e py$(python -c 'import sys;print("".join(map(str, sys.version_info[:2])))')-django${DJANGO}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
fast_finish: true
|
|
||||||
allow_failures:
|
|
||||||
- env: DJANGO="master"
|
|
||||||
exclude:
|
|
||||||
- python: "3.5"
|
|
||||||
env: DJANGO="30"
|
|
||||||
- python: "3.5"
|
|
||||||
env: DJANGO="master"
|
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
irc: "irc.freenode.org#imagekit"
|
irc: "irc.freenode.org#imagekit"
|
||||||
|
|
|
||||||
2
AUTHORS
2
AUTHORS
|
|
@ -28,7 +28,6 @@ Contributors
|
||||||
* `Jannis Leidel`_
|
* `Jannis Leidel`_
|
||||||
* `Sean Bell`_
|
* `Sean Bell`_
|
||||||
* `Saul Shanabrook`_
|
* `Saul Shanabrook`_
|
||||||
* `Venelin Stoykov`_
|
|
||||||
|
|
||||||
.. _Justin Driscoll: http://github.com/jdriscoll
|
.. _Justin Driscoll: http://github.com/jdriscoll
|
||||||
.. _HZDG: http://hzdg.com
|
.. _HZDG: http://hzdg.com
|
||||||
|
|
@ -50,4 +49,3 @@ Contributors
|
||||||
.. _Jannis Leidel: https://github.com/jezdez
|
.. _Jannis Leidel: https://github.com/jezdez
|
||||||
.. _Sean Bell: https://github.com/seanbell
|
.. _Sean Bell: https://github.com/seanbell
|
||||||
.. _Saul Shanabrook: https://github.com/saulshanabrook
|
.. _Saul Shanabrook: https://github.com/saulshanabrook
|
||||||
.. _Venelin Stoykov: https://github.com/vstoykov
|
|
||||||
|
|
|
||||||
17
MANIFEST.in
17
MANIFEST.in
|
|
@ -1,18 +1,5 @@
|
||||||
include AUTHORS
|
include AUTHORS
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include README.rst
|
include README.rst
|
||||||
include testrunner.py
|
recursive-include docs *
|
||||||
include setup.cfg
|
recursive-include imagekit/templates *
|
||||||
include tests/*.py
|
|
||||||
include tests/assets/Lenna.png
|
|
||||||
include tests/assets/lenna-*.jpg
|
|
||||||
include tests/media/lenna.png
|
|
||||||
prune tests/media/CACHE
|
|
||||||
prune tests/media/b
|
|
||||||
prune tests/media/photos
|
|
||||||
include docs/Makefile
|
|
||||||
include docs/conf.py
|
|
||||||
include docs/make.bat
|
|
||||||
include docs/*.rst
|
|
||||||
recursive-include docs/_themes LICENSE README.rst flask_theme_support.py theme.conf *.css_t *.css *.html
|
|
||||||
recursive-include imagekit/templates *.html
|
|
||||||
|
|
|
||||||
75
README.rst
75
README.rst
|
|
@ -1,6 +1,6 @@
|
||||||
|Build Status|_
|
|Build Status|_
|
||||||
|
|
||||||
.. |Build Status| image:: https://travis-ci.org/matthewwithanm/django-imagekit.svg?branch=develop
|
.. |Build Status| image:: https://travis-ci.org/matthewwithanm/django-imagekit.png?branch=develop
|
||||||
.. _Build Status: https://travis-ci.org/matthewwithanm/django-imagekit
|
.. _Build Status: https://travis-ci.org/matthewwithanm/django-imagekit
|
||||||
|
|
||||||
ImageKit is a Django app for processing images. Need a thumbnail? A
|
ImageKit is a Django app for processing images. Need a thumbnail? A
|
||||||
|
|
@ -8,15 +8,10 @@ black-and-white version of a user-uploaded image? ImageKit will make them for
|
||||||
you. If you need to programatically generate one image from another, you need
|
you. If you need to programatically generate one image from another, you need
|
||||||
ImageKit.
|
ImageKit.
|
||||||
|
|
||||||
ImageKit comes with a bunch of image processors for common tasks like resizing
|
|
||||||
and cropping, but you can also create your own. For an idea of what's possible,
|
|
||||||
check out the `Instakit`__ project.
|
|
||||||
|
|
||||||
**For the complete documentation on the latest stable version of ImageKit, see**
|
**For the complete documentation on the latest stable version of ImageKit, see**
|
||||||
`ImageKit on RTD`_.
|
`ImageKit on RTD`_.
|
||||||
|
|
||||||
.. _`ImageKit on RTD`: http://django-imagekit.readthedocs.org
|
.. _`ImageKit on RTD`: http://django-imagekit.readthedocs.org
|
||||||
__ https://github.com/fish2000/instakit
|
|
||||||
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
|
|
@ -39,7 +34,6 @@ Installation
|
||||||
Usage Overview
|
Usage Overview
|
||||||
==============
|
==============
|
||||||
|
|
||||||
.. _specs:
|
|
||||||
|
|
||||||
Specs
|
Specs
|
||||||
-----
|
-----
|
||||||
|
|
@ -71,8 +65,8 @@ your model class:
|
||||||
options={'quality': 60})
|
options={'quality': 60})
|
||||||
|
|
||||||
profile = Profile.objects.all()[0]
|
profile = Profile.objects.all()[0]
|
||||||
print(profile.avatar_thumbnail.url) # > /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg
|
print profile.avatar_thumbnail.url # > /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg
|
||||||
print(profile.avatar_thumbnail.width) # > 100
|
print profile.avatar_thumbnail.width # > 100
|
||||||
|
|
||||||
As you can probably tell, ImageSpecFields work a lot like Django's
|
As you can probably tell, ImageSpecFields work a lot like Django's
|
||||||
ImageFields. The difference is that they're automatically generated by
|
ImageFields. The difference is that they're automatically generated by
|
||||||
|
|
@ -89,7 +83,6 @@ class:
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from imagekit.models import ProcessedImageField
|
from imagekit.models import ProcessedImageField
|
||||||
from imagekit.processors import ResizeToFill
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
class Profile(models.Model):
|
||||||
avatar_thumbnail = ProcessedImageField(upload_to='avatars',
|
avatar_thumbnail = ProcessedImageField(upload_to='avatars',
|
||||||
|
|
@ -98,8 +91,8 @@ class:
|
||||||
options={'quality': 60})
|
options={'quality': 60})
|
||||||
|
|
||||||
profile = Profile.objects.all()[0]
|
profile = Profile.objects.all()[0]
|
||||||
print(profile.avatar_thumbnail.url) # > /media/avatars/MY-avatar.jpg
|
print profile.avatar_thumbnail.url # > /media/avatars/MY-avatar.jpg
|
||||||
print(profile.avatar_thumbnail.width) # > 100
|
print profile.avatar_thumbnail.width # > 100
|
||||||
|
|
||||||
This is pretty similar to our previous example. We don't need to specify a
|
This is pretty similar to our previous example. We don't need to specify a
|
||||||
"source" any more since we're not processing another image field, but we do need
|
"source" any more since we're not processing another image field, but we do need
|
||||||
|
|
@ -138,29 +131,24 @@ particularly when the processing being done depends on user input.
|
||||||
format = 'JPEG'
|
format = 'JPEG'
|
||||||
options = {'quality': 60}
|
options = {'quality': 60}
|
||||||
|
|
||||||
It's probably not surprising that this class is capable of processing an image
|
It's probaby not surprising that this class is capable of processing an image
|
||||||
in the exact same way as our ImageSpecField above. However, unlike with the
|
in the exact same way as our ImageSpecField above. However, unlike with the
|
||||||
image spec model field, this class doesn't define what source the spec is acting
|
image spec model field, this class doesn't define what source the spec is acting
|
||||||
on, or what should be done with the result; that's up to you:
|
on, or what should be done with the result; that's up to you:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
source_file = open('/path/to/myimage.jpg', 'rb')
|
source_file = open('/path/to/myimage.jpg')
|
||||||
image_generator = Thumbnail(source=source_file)
|
image_generator = Thumbnail(source=source_file)
|
||||||
result = image_generator.generate()
|
result = image_generator.generate()
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
You don't have to use ``open``! You can use whatever File-like object you
|
|
||||||
want—including a model's ``ImageField``.
|
|
||||||
|
|
||||||
The result of calling ``generate()`` on an image spec is a file-like object
|
The result of calling ``generate()`` on an image spec is a file-like object
|
||||||
containing our resized image, with which you can do whatever you want. For
|
containing our resized image, with which you can do whatever you want. For
|
||||||
example, if you wanted to save it to disk:
|
example, if you wanted to save it to disk:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
dest = open('/path/to/dest.jpg', 'wb')
|
dest = open('/path/to/dest.jpg', 'w')
|
||||||
dest.write(result.read())
|
dest.write(result.read())
|
||||||
dest.close()
|
dest.close()
|
||||||
|
|
||||||
|
|
@ -230,7 +218,7 @@ that's what we need to pass to use our thumbnail spec:
|
||||||
|
|
||||||
{% load imagekit %}
|
{% load imagekit %}
|
||||||
|
|
||||||
{% generateimage 'myapp:thumbnail' source=source_file %}
|
{% generateimage 'myapp:thumbnail' source=source_image %}
|
||||||
|
|
||||||
This will output the following HTML:
|
This will output the following HTML:
|
||||||
|
|
||||||
|
|
@ -245,7 +233,7 @@ keyword args using two dashes:
|
||||||
|
|
||||||
{% load imagekit %}
|
{% load imagekit %}
|
||||||
|
|
||||||
{% generateimage 'myapp:thumbnail' source=source_file -- alt="A picture of Me" id="mypicture" %}
|
{% generateimage 'myapp:thumbnail' source=source_image -- alt="A picture of Me" id="mypicture" %}
|
||||||
|
|
||||||
Not generating HTML image tags? No problem. The tag also functions as an
|
Not generating HTML image tags? No problem. The tag also functions as an
|
||||||
assignment tag, providing access to the underlying file object:
|
assignment tag, providing access to the underlying file object:
|
||||||
|
|
@ -254,7 +242,7 @@ assignment tag, providing access to the underlying file object:
|
||||||
|
|
||||||
{% load imagekit %}
|
{% load imagekit %}
|
||||||
|
|
||||||
{% generateimage 'myapp:thumbnail' source=source_file as th %}
|
{% generateimage 'myapp:thumbnail' source=source_image as th %}
|
||||||
<a href="{{ th.url }}">Click to download a cool {{ th.width }} x {{ th.height }} image!</a>
|
<a href="{{ th.url }}">Click to download a cool {{ th.width }} x {{ th.height }} image!</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -268,7 +256,7 @@ template tag:
|
||||||
|
|
||||||
{% load imagekit %}
|
{% load imagekit %}
|
||||||
|
|
||||||
{% thumbnail '100x50' source_file %}
|
{% thumbnail '100x50' source_image %}
|
||||||
|
|
||||||
Like the generateimage tag, the thumbnail tag outputs an <img> tag:
|
Like the generateimage tag, the thumbnail tag outputs an <img> tag:
|
||||||
|
|
||||||
|
|
@ -289,15 +277,15 @@ with the id "imagekit:thumbnail" which, by default, is
|
||||||
Second, we're passing two positional arguments (the dimensions and the source
|
Second, we're passing two positional arguments (the dimensions and the source
|
||||||
image) as opposed to the keyword arguments we used with the generateimage tag.
|
image) as opposed to the keyword arguments we used with the generateimage tag.
|
||||||
|
|
||||||
Like with the generateimage tag, you can also specify additional HTML attributes
|
Like with the generatethumbnail tag, you can also specify additional HTML
|
||||||
for the thumbnail tag, or use it as an assignment tag:
|
attributes for the thumbnail tag, or use it as an assignment tag:
|
||||||
|
|
||||||
.. code-block:: html
|
.. code-block:: html
|
||||||
|
|
||||||
{% load imagekit %}
|
{% load imagekit %}
|
||||||
|
|
||||||
{% thumbnail '100x50' source_file -- alt="A picture of Me" id="mypicture" %}
|
{% thumbnail '100x50' source_image -- alt="A picture of Me" id="mypicture" %}
|
||||||
{% thumbnail '100x50' source_file as th %}
|
{% thumbnail '100x50' source_image as th %}
|
||||||
|
|
||||||
|
|
||||||
Using Specs in Forms
|
Using Specs in Forms
|
||||||
|
|
@ -407,37 +395,6 @@ Django admin classes:
|
||||||
|
|
||||||
admin.site.register(Photo, PhotoAdmin)
|
admin.site.register(Photo, PhotoAdmin)
|
||||||
|
|
||||||
To use specs defined outside of models:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from imagekit.admin import AdminThumbnail
|
|
||||||
from imagekit import ImageSpec
|
|
||||||
from imagekit.processors import ResizeToFill
|
|
||||||
from imagekit.cachefiles import ImageCacheFile
|
|
||||||
|
|
||||||
from .models import Photo
|
|
||||||
|
|
||||||
class AdminThumbnailSpec(ImageSpec):
|
|
||||||
processors = [ResizeToFill(100, 30)]
|
|
||||||
format = 'JPEG'
|
|
||||||
options = {'quality': 60 }
|
|
||||||
|
|
||||||
def cached_admin_thumb(instance):
|
|
||||||
# `image` is the name of the image field on the model
|
|
||||||
cached = ImageCacheFile(AdminThumbnailSpec(instance.image))
|
|
||||||
# only generates the first time, subsequent calls use cache
|
|
||||||
cached.generate()
|
|
||||||
return cached
|
|
||||||
|
|
||||||
class PhotoAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('__str__', 'admin_thumbnail')
|
|
||||||
admin_thumbnail = AdminThumbnail(image_field=cached_admin_thumb)
|
|
||||||
|
|
||||||
admin.site.register(Photo, PhotoAdmin)
|
|
||||||
|
|
||||||
|
|
||||||
AdminThumbnail can even use a custom template. For more information, see
|
AdminThumbnail can even use a custom template. For more information, see
|
||||||
``imagekit.admin.AdminThumbnail``.
|
``imagekit.admin.AdminThumbnail``.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ A simple example of a custom source group class is as follows:
|
||||||
def files(self):
|
def files(self):
|
||||||
os.chdir(self.dir)
|
os.chdir(self.dir)
|
||||||
for name in glob.glob('*.jpg'):
|
for name in glob.glob('*.jpg'):
|
||||||
yield open(name, 'rb')
|
yield open(name)
|
||||||
|
|
||||||
Instances of this class could then be registered with one or more spec id:
|
Instances of this class could then be registered with one or more spec id:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ Caching
|
||||||
|
|
||||||
|
|
||||||
Default Backend Workflow
|
Default Backend Workflow
|
||||||
========================
|
================
|
||||||
|
|
||||||
|
|
||||||
``ImageSpec``
|
``ImageSpec``
|
||||||
|
|
@ -29,8 +29,6 @@ objects, but they've got a little trick up their sleeve: they represent files
|
||||||
that may not actually exist!
|
that may not actually exist!
|
||||||
|
|
||||||
|
|
||||||
.. _cache-file-strategy:
|
|
||||||
|
|
||||||
Cache File Strategy
|
Cache File Strategy
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|
@ -57,8 +55,6 @@ The default strategy only defines the first two of these, as follows:
|
||||||
file.generate()
|
file.generate()
|
||||||
|
|
||||||
|
|
||||||
.. _cache-file-backend:
|
|
||||||
|
|
||||||
Cache File Backend
|
Cache File Backend
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
@ -75,7 +71,7 @@ The default works like this:
|
||||||
* If not, caches that information for 5 seconds
|
* If not, caches that information for 5 seconds
|
||||||
* If it does, caches that information in the ``IMAGEKIT_CACHE_BACKEND``
|
* If it does, caches that information in the ``IMAGEKIT_CACHE_BACKEND``
|
||||||
|
|
||||||
If file doesn't exist, generates it immediately and synchronously
|
If file doesn't exsit, generates it immediately and synchronously
|
||||||
|
|
||||||
|
|
||||||
That pretty much covers the architecture of the caching layer, and its default
|
That pretty much covers the architecture of the caching layer, and its default
|
||||||
|
|
@ -104,21 +100,13 @@ ImageKit. Each has its own pros and cons.
|
||||||
Caching Data About Generated Files
|
Caching Data About Generated Files
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
Generally, once a file is generated, you will never be removing it, so by
|
The easiest, and most significant improvement you can make to improve the
|
||||||
default ImageKit will use default cache to cache the state of generated
|
performance of your site is to have ImageKit cache the state of your generated
|
||||||
files "forever" (or only 5 minutes when ``DEBUG = True``).
|
files. The default cache file backend will already do this (if ``DEBUG`` is
|
||||||
|
``True``), using your default Django cache backend, but you can make it way
|
||||||
The time for which ImageKit will cache state is configured with
|
better by setting ``IMAGEKIT_CACHE_BACKEND``. Generally, once a file is
|
||||||
``IMAGEKIT_CACHE_TIMEOUT``. If set to ``None`` this means "never expire"
|
generated, you will never be removing it; therefore, if you can, you should set
|
||||||
(default when ``DEBUG = False``). You can reduce this timeout if you want
|
``IMAGEKIT_CACHE_BACKEND`` to a cache backend that will cache forever.
|
||||||
or set it to some numeric value in seconds if your cache backend behaves
|
|
||||||
differently and for example do not cache values if timeout is ``None``.
|
|
||||||
|
|
||||||
If you clear your cache durring deployment or some other reason probably
|
|
||||||
you do not want to lose the cache for generated images especcialy if you
|
|
||||||
are using some slow remote storage (like Amazon S3). Then you can configure
|
|
||||||
seprate cache (for example redis) in your ``CACHES`` config and tell ImageKit
|
|
||||||
to use it instead of the default cache by setting ``IMAGEKIT_CACHE_BACKEND``.
|
|
||||||
|
|
||||||
|
|
||||||
Pre-Generating Images
|
Pre-Generating Images
|
||||||
|
|
@ -144,7 +132,7 @@ As mentioned above, image generation is normally done synchronously. through
|
||||||
the default cache file backend. However, you can also take advantage of
|
the default cache file backend. However, you can also take advantage of
|
||||||
deferred generation. In order to do this, you'll need to do two things:
|
deferred generation. In order to do this, you'll need to do two things:
|
||||||
|
|
||||||
1) install `celery`__ (or `django-celery`__ if you are bound to Celery<3.1)
|
1) install `django-celery`__
|
||||||
2) tell ImageKit to use the async cachefile backend.
|
2) tell ImageKit to use the async cachefile backend.
|
||||||
To do this for all specs, set the ``IMAGEKIT_DEFAULT_CACHEFILE_BACKEND`` in
|
To do this for all specs, set the ``IMAGEKIT_DEFAULT_CACHEFILE_BACKEND`` in
|
||||||
your settings
|
your settings
|
||||||
|
|
@ -176,28 +164,8 @@ Or, in Python:
|
||||||
else:
|
else:
|
||||||
url = '/path/to/placeholder.jpg'
|
url = '/path/to/placeholder.jpg'
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
If you are using an "async" backend in combination with the "optimistic"
|
|
||||||
cache file strategy (see `Removing Safeguards`_ below), checking for
|
|
||||||
thruthiness as described above will not work. The "optimistic" backend is
|
|
||||||
very optimistic so to say, and removes the check. Create and use the
|
|
||||||
following strategy to a) have images only created on save, and b) retain
|
|
||||||
the ability to check whether the images have already been created::
|
|
||||||
|
|
||||||
class ImagekitOnSaveStrategy(object):
|
|
||||||
def on_source_saved(self, file):
|
|
||||||
file.generate()
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
If you use custom storage backend for some specs,
|
|
||||||
(storage passed to the field different than configured one)
|
|
||||||
it's required the storage to be pickleable
|
|
||||||
|
|
||||||
|
|
||||||
__ https://pypi.python.org/pypi/django-celery
|
__ https://pypi.python.org/pypi/django-celery
|
||||||
__ http://www.celeryproject.org
|
|
||||||
|
|
||||||
|
|
||||||
Removing Safeguards
|
Removing Safeguards
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ execfile(os.path.join(os.path.dirname(__file__), '..', 'imagekit',
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = re.match(r'\d+\.\d+', pkgmeta['__version__']).group()
|
version = re.match('\d+\.\d+', pkgmeta['__version__']).group()
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = pkgmeta['__version__']
|
release = pkgmeta['__version__']
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ Settings
|
||||||
|
|
||||||
The qualified class name of a Django storage backend to use to save the
|
The qualified class name of a Django storage backend to use to save the
|
||||||
cached images. If no value is provided for ``IMAGEKIT_DEFAULT_FILE_STORAGE``,
|
cached images. If no value is provided for ``IMAGEKIT_DEFAULT_FILE_STORAGE``,
|
||||||
and none is specified by the spec definition, `your default file storage`__
|
and none is specified by the spec definition, the storage of the source file
|
||||||
will be used.
|
will be used.
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -44,24 +44,11 @@ Settings
|
||||||
|
|
||||||
.. attribute:: IMAGEKIT_CACHE_BACKEND
|
.. attribute:: IMAGEKIT_CACHE_BACKEND
|
||||||
|
|
||||||
:default: ``'default'``
|
:default: If ``DEBUG`` is ``True``, ``'django.core.cache.backends.dummy.DummyCache'``.
|
||||||
|
Otherwise, ``'default'``.
|
||||||
|
|
||||||
The Django cache backend alias to retrieve the shared cache instance defined
|
The Django cache backend to be used to store information like the state of
|
||||||
in your settings, as described in the `Django cache section`_.
|
cached images (i.e. validated or not).
|
||||||
|
|
||||||
The cache is then used to store information like the state of cached
|
|
||||||
images (i.e. validated or not).
|
|
||||||
|
|
||||||
.. _`Django cache section`: https://docs.djangoproject.com/en/1.8/topics/cache/#accessing-the-cache
|
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: IMAGEKIT_CACHE_TIMEOUT
|
|
||||||
|
|
||||||
:default: ``None``
|
|
||||||
|
|
||||||
Use when you need to override the timeout used to cache file state.
|
|
||||||
By default it is "cache forever".
|
|
||||||
It's highly recommended that you use a very high timeout.
|
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: IMAGEKIT_CACHE_PREFIX
|
.. attribute:: IMAGEKIT_CACHE_PREFIX
|
||||||
|
|
@ -85,6 +72,3 @@ Settings
|
||||||
A function responsible for generating file names for cache files that
|
A function responsible for generating file names for cache files that
|
||||||
correspond to image specs. Since you will likely want to base the name of
|
correspond to image specs. Since you will likely want to base the name of
|
||||||
your cache files on the name of the source, this extra setting is provided.
|
your cache files on the name of the source, this extra setting is provided.
|
||||||
|
|
||||||
|
|
||||||
__ https://docs.djangoproject.com/en/dev/ref/settings/#default-file-storage
|
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,12 @@ IK3 provides analogous settings for cache file backends and strategies:
|
||||||
IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'path.to.MyCacheFileBackend'
|
IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'path.to.MyCacheFileBackend'
|
||||||
IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'path.to.MyCacheFileStrategy'
|
IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'path.to.MyCacheFileStrategy'
|
||||||
|
|
||||||
See the documentation on :ref:`cache file backends <cache-file-backend>` and :ref:`cache file strategies <cache-file-strategy>`
|
See the documentation on `cache file backends`_ and `cache file strategies`_
|
||||||
for more details.
|
for more details.
|
||||||
|
|
||||||
|
.. _`cache file backends`:
|
||||||
|
.. _`cache file strategies`:
|
||||||
|
|
||||||
|
|
||||||
Conditional model ``processors``
|
Conditional model ``processors``
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
@ -90,7 +93,9 @@ In IK2, an ``ImageSpecField`` could take a ``processors`` callable instead of
|
||||||
an iterable, which allowed processing decisions to made based on other
|
an iterable, which allowed processing decisions to made based on other
|
||||||
properties of the model. IK3 does away with this feature for consistency's sake
|
properties of the model. IK3 does away with this feature for consistency's sake
|
||||||
(if one kwarg could be callable, why not all?), but provides a much more robust
|
(if one kwarg could be callable, why not all?), but provides a much more robust
|
||||||
solution: the custom ``spec``. See the :doc:`advanced usage <advanced_usage>` documentation for more.
|
solution: the custom ``spec``. See the `advanced usage`_ documentation for more.
|
||||||
|
|
||||||
|
.. _`advanced usage`:
|
||||||
|
|
||||||
|
|
||||||
Conditonal ``cache_to`` file names
|
Conditonal ``cache_to`` file names
|
||||||
|
|
@ -104,7 +109,9 @@ There is a way to achieve custom file names by overriding your spec's
|
||||||
``cachefile_name``, but it is not recommended, as the spec's default
|
``cachefile_name``, but it is not recommended, as the spec's default
|
||||||
behavior is to hash the combination of ``source``, ``processors``, ``format``,
|
behavior is to hash the combination of ``source``, ``processors``, ``format``,
|
||||||
and other spec options to ensure that changes to the spec always result in
|
and other spec options to ensure that changes to the spec always result in
|
||||||
unique file names. See the documentation on :ref:`specs` for more.
|
unique file names. See the documentation on `specs`_ for more.
|
||||||
|
|
||||||
|
.. _`specs`:
|
||||||
|
|
||||||
|
|
||||||
Processors have moved to PILKit
|
Processors have moved to PILKit
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
|
from . import importers
|
||||||
from . import conf
|
from . import conf
|
||||||
from . import generatorlibrary
|
from . import generatorlibrary
|
||||||
from .specs import ImageSpec
|
from .specs import ImageSpec
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
from copy import copy
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.core.files.images import ImageFile
|
from django.core.files.images import ImageFile
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from django.utils.encoding import smart_str
|
|
||||||
from ..files import BaseIKFile
|
from ..files import BaseIKFile
|
||||||
from ..registry import generator_registry
|
from ..registry import generator_registry
|
||||||
from ..signals import content_required, existence_required
|
from ..signals import content_required, existence_required
|
||||||
|
|
@ -122,54 +120,14 @@ class ImageCacheFile(BaseIKFile, ImageFile):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def __bool__(self):
|
def __nonzero__(self):
|
||||||
if not self.name:
|
if not self.name:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Dispatch the existence_required signal before checking to see if the
|
# Dispatch the existence_required signal before checking to see if the
|
||||||
# file exists. This gives the strategy a chance to create the file.
|
# file exists. This gives the strategy a chance to create the file.
|
||||||
existence_required.send(sender=self, file=self)
|
existence_required.send(sender=self, file=self)
|
||||||
|
return self.cachefile_backend.exists(self)
|
||||||
try:
|
|
||||||
check = self.cachefile_strategy.should_verify_existence(self)
|
|
||||||
except AttributeError:
|
|
||||||
# All synchronous backends should have created the file as part of
|
|
||||||
# `existence_required` if they wanted to.
|
|
||||||
check = getattr(self.cachefile_backend, 'is_async', False)
|
|
||||||
return self.cachefile_backend.exists(self) if check else True
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
state = copy(self.__dict__)
|
|
||||||
|
|
||||||
# file is hidden link to "file" attribute
|
|
||||||
state.pop('_file', None)
|
|
||||||
|
|
||||||
# remove storage from state as some non-FileSystemStorage can't be
|
|
||||||
# pickled
|
|
||||||
settings_storage = get_singleton(
|
|
||||||
settings.IMAGEKIT_DEFAULT_FILE_STORAGE,
|
|
||||||
'file storage backend'
|
|
||||||
)
|
|
||||||
if state['storage'] == settings_storage:
|
|
||||||
state.pop('storage')
|
|
||||||
return state
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
|
||||||
if 'storage' not in state:
|
|
||||||
state['storage'] = get_singleton(
|
|
||||||
settings.IMAGEKIT_DEFAULT_FILE_STORAGE,
|
|
||||||
'file storage backend'
|
|
||||||
)
|
|
||||||
self.__dict__.update(state)
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
# Python 2 compatibility
|
|
||||||
return self.__bool__()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return smart_str("<%s: %s>" % (
|
|
||||||
self.__class__.__name__, self if self.name else "None")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LazyImageCacheFile(SimpleLazyObject):
|
class LazyImageCacheFile(SimpleLazyObject):
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
from ..utils import get_singleton, get_cache, sanitize_cache_key
|
from ..utils import get_singleton, sanitize_cache_key
|
||||||
import warnings
|
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
from django.core.cache import get_cache
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
class CacheFileState(object):
|
class CacheFileState(object):
|
||||||
EXISTS = 'exists'
|
EXISTS = 'exists'
|
||||||
GENERATING = 'generating'
|
PENDING = 'pending'
|
||||||
DOES_NOT_EXIST = 'does_not_exist'
|
DOES_NOT_EXIST = 'does_not_exist'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,7 +17,7 @@ def get_default_cachefile_backend():
|
||||||
"""
|
"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
return get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_BACKEND,
|
return get_singleton(settings.IMAGEKIT_DEFAULT_CACHEFILE_BACKEND,
|
||||||
'file backend')
|
'file backend')
|
||||||
|
|
||||||
|
|
||||||
class InvalidFileBackendError(ImproperlyConfigured):
|
class InvalidFileBackendError(ImproperlyConfigured):
|
||||||
|
|
@ -53,7 +52,8 @@ class CachedFileBackend(object):
|
||||||
@property
|
@property
|
||||||
def cache(self):
|
def cache(self):
|
||||||
if not getattr(self, '_cache', None):
|
if not getattr(self, '_cache', None):
|
||||||
self._cache = get_cache()
|
from django.conf import settings
|
||||||
|
self._cache = get_cache(settings.IMAGEKIT_CACHE_BACKEND)
|
||||||
return self._cache
|
return self._cache
|
||||||
|
|
||||||
def get_key(self, file):
|
def get_key(self, file):
|
||||||
|
|
@ -61,10 +61,10 @@ class CachedFileBackend(object):
|
||||||
return sanitize_cache_key('%s%s-state' %
|
return sanitize_cache_key('%s%s-state' %
|
||||||
(settings.IMAGEKIT_CACHE_PREFIX, file.name))
|
(settings.IMAGEKIT_CACHE_PREFIX, file.name))
|
||||||
|
|
||||||
def get_state(self, file, check_if_unknown=True):
|
def get_state(self, file):
|
||||||
key = self.get_key(file)
|
key = self.get_key(file)
|
||||||
state = self.cache.get(key)
|
state = self.cache.get(key)
|
||||||
if state is None and check_if_unknown:
|
if state is None:
|
||||||
exists = self._exists(file)
|
exists = self._exists(file)
|
||||||
state = CacheFileState.EXISTS if exists else CacheFileState.DOES_NOT_EXIST
|
state = CacheFileState.EXISTS if exists else CacheFileState.DOES_NOT_EXIST
|
||||||
self.set_state(file, state)
|
self.set_state(file, state)
|
||||||
|
|
@ -75,7 +75,7 @@ class CachedFileBackend(object):
|
||||||
if state == CacheFileState.DOES_NOT_EXIST:
|
if state == CacheFileState.DOES_NOT_EXIST:
|
||||||
self.cache.set(key, state, self.existence_check_timeout)
|
self.cache.set(key, state, self.existence_check_timeout)
|
||||||
else:
|
else:
|
||||||
self.cache.set(key, state, settings.IMAGEKIT_CACHE_TIMEOUT)
|
self.cache.set(key, state)
|
||||||
|
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
state = copy(self.__dict__)
|
state = copy(self.__dict__)
|
||||||
|
|
@ -91,11 +91,9 @@ class CachedFileBackend(object):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def generate_now(self, file, force=False):
|
def generate_now(self, file, force=False):
|
||||||
if force or self.get_state(file) not in (CacheFileState.GENERATING, CacheFileState.EXISTS):
|
if force or self.get_state(file) == CacheFileState.DOES_NOT_EXIST:
|
||||||
self.set_state(file, CacheFileState.GENERATING)
|
|
||||||
file._generate()
|
file._generate()
|
||||||
self.set_state(file, CacheFileState.EXISTS)
|
self.set_state(file, CacheFileState.EXISTS)
|
||||||
file.close()
|
|
||||||
|
|
||||||
|
|
||||||
class Simple(CachedFileBackend):
|
class Simple(CachedFileBackend):
|
||||||
|
|
@ -110,86 +108,34 @@ class Simple(CachedFileBackend):
|
||||||
|
|
||||||
def _exists(self, file):
|
def _exists(self, file):
|
||||||
return bool(getattr(file, '_file', None)
|
return bool(getattr(file, '_file', None)
|
||||||
or (file.name and file.storage.exists(file.name)))
|
or file.storage.exists(file.name))
|
||||||
|
|
||||||
|
|
||||||
def _generate_file(backend, file, force=False):
|
def _generate_file(backend, file, force=False):
|
||||||
backend.generate_now(file, force=force)
|
backend.generate_now(file, force=force)
|
||||||
|
|
||||||
|
|
||||||
class BaseAsync(Simple):
|
|
||||||
"""
|
|
||||||
Base class for cache file backends that generate files asynchronously.
|
|
||||||
"""
|
|
||||||
is_async = True
|
|
||||||
|
|
||||||
def generate(self, file, force=False):
|
|
||||||
# Schedule the file for generation, unless we know for sure we don't
|
|
||||||
# need to. If an already-generated file sneaks through, that's okay;
|
|
||||||
# ``generate_now`` will catch it. We just want to make sure we don't
|
|
||||||
# schedule anything we know is unnecessary--but we also don't want to
|
|
||||||
# force a costly existence check.
|
|
||||||
state = self.get_state(file, check_if_unknown=False)
|
|
||||||
if state not in (CacheFileState.GENERATING, CacheFileState.EXISTS):
|
|
||||||
self.schedule_generation(file, force=force)
|
|
||||||
|
|
||||||
def schedule_generation(self, file, force=False):
|
|
||||||
# overwrite this to have the file generated in the background,
|
|
||||||
# e. g. in a worker queue.
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from celery import task
|
import celery
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
_celery_task = task(ignore_result=True, serializer='pickle')(_generate_file)
|
_generate_file = celery.task(ignore_result=True)(_generate_file)
|
||||||
|
|
||||||
|
|
||||||
class Celery(BaseAsync):
|
class Async(Simple):
|
||||||
"""
|
"""
|
||||||
A backend that uses Celery to generate the images.
|
A backend that uses Celery to generate the images.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
import celery # noqa
|
import celery
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ImproperlyConfigured('You must install celery to use'
|
raise ImproperlyConfigured('You must install celery to use'
|
||||||
' imagekit.cachefiles.backends.Celery.')
|
' imagekit.cachefiles.backend.Async.')
|
||||||
super(Celery, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def schedule_generation(self, file, force=False):
|
|
||||||
_celery_task.delay(self, file, force=force)
|
|
||||||
|
|
||||||
|
|
||||||
# Stub class to preserve backwards compatibility and issue a warning
|
|
||||||
class Async(Celery):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
message = '{path}.Async is deprecated. Use {path}.Celery instead.'
|
|
||||||
warnings.warn(message.format(path=__name__), DeprecationWarning)
|
|
||||||
super(Async, self).__init__(*args, **kwargs)
|
super(Async, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def generate(self, file, force=False):
|
||||||
try:
|
self.set_state(file, CacheFileState.PENDING)
|
||||||
from django_rq import job
|
_generate_file.delay(self, file, force=force)
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
_rq_job = job('default', result_ttl=0)(_generate_file)
|
|
||||||
|
|
||||||
|
|
||||||
class RQ(BaseAsync):
|
|
||||||
"""
|
|
||||||
A backend that uses RQ to generate the images.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
import django_rq # noqa
|
|
||||||
except ImportError:
|
|
||||||
raise ImproperlyConfigured('You must install django-rq to use'
|
|
||||||
' imagekit.cachefiles.backends.RQ.')
|
|
||||||
super(RQ, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def schedule_generation(self, file, force=False):
|
|
||||||
_rq_job.delay(self, file, force=force)
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
import six
|
|
||||||
|
|
||||||
from django.utils.functional import LazyObject
|
from django.utils.functional import LazyObject
|
||||||
from ..lib import force_text
|
|
||||||
from ..utils import get_singleton
|
from ..utils import get_singleton
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -29,9 +26,6 @@ class Optimistic(object):
|
||||||
def on_source_saved(self, file):
|
def on_source_saved(self, file):
|
||||||
file.generate()
|
file.generate()
|
||||||
|
|
||||||
def should_verify_existence(self, file):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class DictStrategy(object):
|
class DictStrategy(object):
|
||||||
def __init__(self, callbacks):
|
def __init__(self, callbacks):
|
||||||
|
|
@ -39,11 +33,24 @@ class DictStrategy(object):
|
||||||
setattr(self, k, v)
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
|
||||||
def load_strategy(strategy):
|
class StrategyWrapper(LazyObject):
|
||||||
if isinstance(strategy, six.string_types):
|
def __init__(self, strategy):
|
||||||
strategy = get_singleton(strategy, 'cache file strategy')
|
if isinstance(strategy, basestring):
|
||||||
elif isinstance(strategy, dict):
|
strategy = get_singleton(strategy, 'cache file strategy')
|
||||||
strategy = DictStrategy(strategy)
|
elif isinstance(strategy, dict):
|
||||||
elif callable(strategy):
|
strategy = DictStrategy(strategy)
|
||||||
strategy = strategy()
|
elif callable(strategy):
|
||||||
return strategy
|
strategy = strategy()
|
||||||
|
self._wrapped = strategy
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
return {'_wrapped': self._wrapped}
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
self._wrapped = state['_wrapped']
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return unicode(self._wrapped)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self._wrapped)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from appconf import AppConf
|
from appconf import AppConf
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
|
|
||||||
|
|
||||||
class ImageKitConf(AppConf):
|
class ImageKitConf(AppConf):
|
||||||
|
|
@ -14,24 +13,30 @@ class ImageKitConf(AppConf):
|
||||||
|
|
||||||
CACHE_BACKEND = None
|
CACHE_BACKEND = None
|
||||||
CACHE_PREFIX = 'imagekit:'
|
CACHE_PREFIX = 'imagekit:'
|
||||||
CACHE_TIMEOUT = None
|
|
||||||
USE_MEMCACHED_SAFE_CACHE_KEY = True
|
USE_MEMCACHED_SAFE_CACHE_KEY = True
|
||||||
|
|
||||||
def configure_cache_backend(self, value):
|
def configure_cache_backend(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
from django.core.cache import DEFAULT_CACHE_ALIAS
|
try:
|
||||||
return DEFAULT_CACHE_ALIAS
|
from django.core.cache.backends.dummy import DummyCache
|
||||||
|
except ImportError:
|
||||||
|
dummy_cache = 'dummy://'
|
||||||
|
else:
|
||||||
|
dummy_cache = 'django.core.cache.backends.dummy.DummyCache'
|
||||||
|
|
||||||
if value not in settings.CACHES:
|
# DEFAULT_CACHE_ALIAS doesn't exist in Django<=1.2
|
||||||
raise ImproperlyConfigured("{0} is not present in settings.CACHES".format(value))
|
try:
|
||||||
|
from django.core.cache import DEFAULT_CACHE_ALIAS as default_cache_alias
|
||||||
|
except ImportError:
|
||||||
|
default_cache_alias = 'default'
|
||||||
|
|
||||||
return value
|
if settings.DEBUG:
|
||||||
|
value = dummy_cache
|
||||||
|
elif default_cache_alias in getattr(settings, 'CACHES', {}):
|
||||||
|
value = default_cache_alias
|
||||||
|
else:
|
||||||
|
value = getattr(settings, 'CACHE_BACKEND', None) or dummy_cache
|
||||||
|
|
||||||
def configure_cache_timeout(self, value):
|
|
||||||
if value is None and settings.DEBUG:
|
|
||||||
# If value is not configured and is DEBUG set it to 5 minutes
|
|
||||||
return 300
|
|
||||||
# Otherwise leave it as is. If it is None then valies will never expire
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def configure_default_file_storage(self, value):
|
def configure_default_file_storage(self, value):
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.files.base import File, ContentFile
|
from django.core.files.base import File, ContentFile
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str, smart_unicode
|
||||||
from .lib import smart_text
|
import os
|
||||||
from .utils import format_to_mimetype, extension_to_mimetype
|
from .utils import format_to_mimetype, extension_to_mimetype
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -49,25 +46,14 @@ class BaseIKFile(File):
|
||||||
|
|
||||||
def _get_size(self):
|
def _get_size(self):
|
||||||
self._require_file()
|
self._require_file()
|
||||||
if not getattr(self, '_committed', False):
|
if not self._committed:
|
||||||
return self.file.size
|
return self.file.size
|
||||||
return self.storage.size(self.name)
|
return self.storage.size(self.name)
|
||||||
size = property(_get_size)
|
size = property(_get_size)
|
||||||
|
|
||||||
def open(self, mode='rb'):
|
def open(self, mode='rb'):
|
||||||
self._require_file()
|
self._require_file()
|
||||||
try:
|
self.file.open(mode)
|
||||||
self.file.open(mode)
|
|
||||||
except ValueError:
|
|
||||||
# if the underlaying file can't be reopened
|
|
||||||
# then we will use the storage to try to open it again
|
|
||||||
if self.file.closed:
|
|
||||||
# clear cached file instance
|
|
||||||
del self.file
|
|
||||||
# Because file is a property we can acces it after
|
|
||||||
# we deleted it
|
|
||||||
return self.file.open(mode)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _get_closed(self):
|
def _get_closed(self):
|
||||||
file = getattr(self, '_file', None)
|
file = getattr(self, '_file', None)
|
||||||
|
|
@ -106,5 +92,4 @@ class IKContentFile(ContentFile):
|
||||||
return smart_str(self.file.name or '')
|
return smart_str(self.file.name or '')
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
# Python 2
|
return smart_unicode(self.file.name or u'')
|
||||||
return smart_text(self.file.name or '')
|
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,8 @@ class ProcessedImageField(ImageField, SpecHost):
|
||||||
def clean(self, data, initial=None):
|
def clean(self, data, initial=None):
|
||||||
data = super(ProcessedImageField, self).clean(data, initial)
|
data = super(ProcessedImageField, self).clean(data, initial)
|
||||||
|
|
||||||
if data and data != initial:
|
if data:
|
||||||
spec = self.get_spec(source=data)
|
spec = self.get_spec(source=data)
|
||||||
f = generate(spec)
|
data = generate(spec)
|
||||||
# Name is required in Django 1.4. When we drop support for it
|
|
||||||
# then we can dirrectly return the result from `generate(spec)`
|
|
||||||
f.name = data.name
|
|
||||||
return f
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from pickle import MARK, DICT
|
from pickle import Pickler, MARK, DICT
|
||||||
try:
|
from types import DictionaryType
|
||||||
from pickle import _Pickler
|
|
||||||
except ImportError:
|
|
||||||
# Python 2 compatible
|
|
||||||
from pickle import Pickler as _Pickler
|
|
||||||
from .lib import StringIO
|
from .lib import StringIO
|
||||||
|
|
||||||
|
|
||||||
class CanonicalizingPickler(_Pickler):
|
class CanonicalizingPickler(Pickler):
|
||||||
dispatch = copy(_Pickler.dispatch)
|
dispatch = copy(Pickler.dispatch)
|
||||||
|
|
||||||
def save_set(self, obj):
|
def save_set(self, obj):
|
||||||
rv = obj.__reduce_ex__(0)
|
rv = obj.__reduce_ex__(0)
|
||||||
|
|
@ -24,9 +20,9 @@ class CanonicalizingPickler(_Pickler):
|
||||||
write(MARK + DICT)
|
write(MARK + DICT)
|
||||||
|
|
||||||
self.memoize(obj)
|
self.memoize(obj)
|
||||||
self._batch_setitems(sorted(obj.items()))
|
self._batch_setitems(sorted(obj.iteritems()))
|
||||||
|
|
||||||
dispatch[dict] = save_dict
|
dispatch[DictionaryType] = save_dict
|
||||||
|
|
||||||
|
|
||||||
def pickle(obj):
|
def pickle(obj):
|
||||||
|
|
|
||||||
29
imagekit/importers.py
Normal file
29
imagekit/importers.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from django.utils.importlib import import_module
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessorImporter(object):
|
||||||
|
"""
|
||||||
|
The processors were moved to the PILKit project so they could be used
|
||||||
|
separtely from ImageKit (which has a bunch of Django dependencies). However,
|
||||||
|
there's no real need to expose this fact (and we want to maintain backwards
|
||||||
|
compatibility), so we proxy all "imagekit.processors" imports to
|
||||||
|
"pilkit.processors" using this object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pattern = re.compile(r'^imagekit\.processors((\..*)?)$')
|
||||||
|
|
||||||
|
def find_module(self, name, path=None):
|
||||||
|
if self.pattern.match(name):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def load_module(self, name):
|
||||||
|
if name in sys.modules:
|
||||||
|
return sys.modules[name]
|
||||||
|
|
||||||
|
new_name = self.pattern.sub(r'pilkit.processors\1', name)
|
||||||
|
return import_module(new_name)
|
||||||
|
|
||||||
|
|
||||||
|
sys.meta_path.append(ProcessorImporter())
|
||||||
|
|
@ -19,12 +19,9 @@ except ImportError:
|
||||||
raise ImportError('ImageKit was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.')
|
raise ImportError('ImageKit was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from io import BytesIO as StringIO
|
from cStringIO import StringIO
|
||||||
except:
|
except ImportError:
|
||||||
try:
|
from StringIO import StringIO
|
||||||
from cStringIO import StringIO
|
|
||||||
except ImportError:
|
|
||||||
from StringIO import StringIO
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from logging import NullHandler
|
from logging import NullHandler
|
||||||
|
|
@ -34,19 +31,3 @@ except ImportError:
|
||||||
class NullHandler(Handler):
|
class NullHandler(Handler):
|
||||||
def emit(self, record):
|
def emit(self, record):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Try to import `force_text` available from Django 1.5
|
|
||||||
# This function will replace `unicode` used in the code
|
|
||||||
# If Django version is under 1.5 then use `force_unicde`
|
|
||||||
# It is used for compatibility between Python 2 and Python 3
|
|
||||||
try:
|
|
||||||
from django.utils.encoding import force_text, force_bytes, smart_text
|
|
||||||
except ImportError:
|
|
||||||
# Django < 1.5
|
|
||||||
from django.utils.encoding import (force_unicode as force_text,
|
|
||||||
smart_str as force_bytes,
|
|
||||||
smart_unicode as smart_text)
|
|
||||||
|
|
||||||
__all__ = ['Image', 'ImageColor', 'ImageChops', 'ImageEnhance', 'ImageFile',
|
|
||||||
'ImageFilter', 'ImageDraw', 'ImageStat', 'StringIO', 'NullHandler',
|
|
||||||
'force_text', 'force_bytes', 'smart_text']
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
import re
|
import re
|
||||||
from ...registry import generator_registry, cachefile_registry
|
from ...registry import generator_registry, cachefile_registry
|
||||||
from ...exceptions import MissingSource
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|
@ -14,28 +13,23 @@ match both. Subsegments are always matched, so "a" will match "a" as
|
||||||
well as "a:b" and "a:b:c".""")
|
well as "a:b" and "a:b:c".""")
|
||||||
args = '[generator_ids]'
|
args = '[generator_ids]'
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
parser.add_argument('generator_id', nargs='*', help='<app_name>:<model>:<field> for model specs')
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
generators = generator_registry.get_ids()
|
generators = generator_registry.get_ids()
|
||||||
|
|
||||||
generator_ids = options['generator_id'] if 'generator_id' in options else args
|
if args:
|
||||||
if generator_ids:
|
patterns = self.compile_patterns(args)
|
||||||
patterns = self.compile_patterns(generator_ids)
|
|
||||||
generators = (id for id in generators if any(p.match(id) for p in patterns))
|
generators = (id for id in generators if any(p.match(id) for p in patterns))
|
||||||
|
|
||||||
for generator_id in generators:
|
for generator_id in generators:
|
||||||
self.stdout.write('Validating generator: %s\n' % generator_id)
|
self.stdout.write('Validating generator: %s\n' % generator_id)
|
||||||
for image_file in cachefile_registry.get(generator_id):
|
for file in cachefile_registry.get(generator_id):
|
||||||
if image_file.name:
|
self.stdout.write(' %s\n' % file)
|
||||||
self.stdout.write(' %s\n' % image_file.name)
|
try:
|
||||||
try:
|
# TODO: Allow other validation actions through command option
|
||||||
image_file.generate()
|
file.generate()
|
||||||
except MissingSource as err:
|
except Exception as err:
|
||||||
self.stdout.write('\t No source associated with\n')
|
# TODO: How should we handle failures? Don't want to error, but should call it out more than this.
|
||||||
except Exception as err:
|
self.stdout.write(' FAILED: %s\n' % err)
|
||||||
self.stdout.write('\tFailed %s\n' % (err))
|
|
||||||
|
|
||||||
def compile_patterns(self, generator_ids):
|
def compile_patterns(self, generator_ids):
|
||||||
return [self.compile_pattern(id) for id in generator_ids]
|
return [self.compile_pattern(id) for id in generator_ids]
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.signals import class_prepared
|
|
||||||
from .files import ProcessedImageFieldFile
|
from .files import ProcessedImageFieldFile
|
||||||
from .utils import ImageSpecFileDescriptor
|
from .utils import ImageSpecFileDescriptor
|
||||||
from ...specs import SpecHost
|
from ...specs import SpecHost
|
||||||
|
|
@ -17,7 +13,7 @@ class SpecHostField(SpecHost):
|
||||||
# Generate a spec_id to register the spec with. The default spec id is
|
# Generate a spec_id to register the spec with. The default spec id is
|
||||||
# "<app>:<model>_<field>"
|
# "<app>:<model>_<field>"
|
||||||
if not spec_id:
|
if not spec_id:
|
||||||
spec_id = ('%s:%s:%s' % (cls._meta.app_label,
|
spec_id = (u'%s:%s:%s' % (cls._meta.app_label,
|
||||||
cls._meta.object_name, name)).lower()
|
cls._meta.object_name, name)).lower()
|
||||||
|
|
||||||
# Register the spec with the id. This allows specs to be overridden
|
# Register the spec with the id. This allows specs to be overridden
|
||||||
|
|
@ -48,37 +44,27 @@ class ImageSpecField(SpecHostField):
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
def contribute_to_class(self, cls, name):
|
||||||
# If the source field name isn't defined, figure it out.
|
# If the source field name isn't defined, figure it out.
|
||||||
|
source = self.source
|
||||||
|
if not source:
|
||||||
|
image_fields = [f.attname for f in cls._meta.fields if
|
||||||
|
isinstance(f, models.ImageField)]
|
||||||
|
if len(image_fields) == 0:
|
||||||
|
raise Exception(
|
||||||
|
'%s does not define any ImageFields, so your %s'
|
||||||
|
' ImageSpecField has no image to act on.' %
|
||||||
|
(cls.__name__, name))
|
||||||
|
elif len(image_fields) > 1:
|
||||||
|
raise Exception(
|
||||||
|
'%s defines multiple ImageFields, but you have not'
|
||||||
|
' specified a source for your %s ImageSpecField.' %
|
||||||
|
(cls.__name__, name))
|
||||||
|
source = image_fields[0]
|
||||||
|
|
||||||
def register_source_group(source):
|
setattr(cls, name, ImageSpecFileDescriptor(self, name, source))
|
||||||
setattr(cls, name, ImageSpecFileDescriptor(self, name, source))
|
self._set_spec_id(cls, name)
|
||||||
self._set_spec_id(cls, name)
|
|
||||||
|
|
||||||
# Add the model and field as a source for this spec id
|
# Add the model and field as a source for this spec id
|
||||||
register.source_group(self.spec_id, ImageFieldSourceGroup(cls, source))
|
register.source_group(self.spec_id, ImageFieldSourceGroup(cls, source))
|
||||||
|
|
||||||
if self.source:
|
|
||||||
register_source_group(self.source)
|
|
||||||
else:
|
|
||||||
# The source argument is not defined
|
|
||||||
# Then we need to see if there is only one ImageField in that model
|
|
||||||
# But we need to do that after full model initialization
|
|
||||||
def handle_model_preparation(sender, **kwargs):
|
|
||||||
|
|
||||||
image_fields = [f.attname for f in cls._meta.fields if
|
|
||||||
isinstance(f, models.ImageField)]
|
|
||||||
if len(image_fields) == 0:
|
|
||||||
raise Exception(
|
|
||||||
'%s does not define any ImageFields, so your %s'
|
|
||||||
' ImageSpecField has no image to act on.' %
|
|
||||||
(cls.__name__, name))
|
|
||||||
elif len(image_fields) > 1:
|
|
||||||
raise Exception(
|
|
||||||
'%s defines multiple ImageFields, but you have not'
|
|
||||||
' specified a source for your %s ImageSpecField.' %
|
|
||||||
(cls.__name__, name))
|
|
||||||
register_source_group(image_fields[0])
|
|
||||||
|
|
||||||
class_prepared.connect(handle_model_preparation, sender=cls, weak=False)
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessedImageField(models.ImageField, SpecHostField):
|
class ProcessedImageField(models.ImageField, SpecHostField):
|
||||||
|
|
@ -93,7 +79,7 @@ class ProcessedImageField(models.ImageField, SpecHostField):
|
||||||
|
|
||||||
def __init__(self, processors=None, format=None, options=None,
|
def __init__(self, processors=None, format=None, options=None,
|
||||||
verbose_name=None, name=None, width_field=None, height_field=None,
|
verbose_name=None, name=None, width_field=None, height_field=None,
|
||||||
autoconvert=None, spec=None, spec_id=None, **kwargs):
|
autoconvert=True, spec=None, spec_id=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
The ProcessedImageField constructor accepts all of the arguments that
|
The ProcessedImageField constructor accepts all of the arguments that
|
||||||
the :class:`django.db.models.ImageField` constructor accepts, as well
|
the :class:`django.db.models.ImageField` constructor accepts, as well
|
||||||
|
|
@ -101,10 +87,6 @@ class ProcessedImageField(models.ImageField, SpecHostField):
|
||||||
:class:`imagekit.models.ImageSpecField`.
|
:class:`imagekit.models.ImageSpecField`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# if spec is not provided then autoconvert will be True by default
|
|
||||||
if spec is None and autoconvert is None:
|
|
||||||
autoconvert = True
|
|
||||||
|
|
||||||
SpecHost.__init__(self, processors=processors, format=format,
|
SpecHost.__init__(self, processors=processors, format=format,
|
||||||
options=options, autoconvert=autoconvert, spec=spec,
|
options=options, autoconvert=autoconvert, spec=spec,
|
||||||
spec_id=spec_id)
|
spec_id=spec_id)
|
||||||
|
|
@ -116,11 +98,9 @@ class ProcessedImageField(models.ImageField, SpecHostField):
|
||||||
return super(ProcessedImageField, self).contribute_to_class(cls, name)
|
return super(ProcessedImageField, self).contribute_to_class(cls, name)
|
||||||
|
|
||||||
|
|
||||||
# If the project does not use south, then we will not try to add introspection
|
try:
|
||||||
if 'south' in settings.INSTALLED_APPS:
|
from south.modelsinspector import add_introspection_rules
|
||||||
try:
|
except ImportError:
|
||||||
from south.modelsinspector import add_introspection_rules
|
pass
|
||||||
except ImportError:
|
else:
|
||||||
pass
|
add_introspection_rules([], [r'^imagekit\.models\.fields\.ProcessedImageField$'])
|
||||||
else:
|
|
||||||
add_introspection_rules([], [r'^imagekit\.models\.fields\.ProcessedImageField$'])
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
__title__ = 'django-imagekit'
|
__title__ = 'django-imagekit'
|
||||||
__author__ = 'Matthew Tretter, Venelin Stoykov, Eric Eldredge, Bryan Veloso, Greg Newman, Chris Drackett, Justin Driscoll'
|
__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge'
|
||||||
__version__ = '4.0.2'
|
__version__ = '3.0.2'
|
||||||
__license__ = 'BSD'
|
__license__ = 'BSD'
|
||||||
__all__ = ['__title__', '__author__', '__version__', '__license__']
|
__all__ = ['__title__', '__author__', '__version__', '__license__']
|
||||||
|
|
|
||||||
5
imagekit/processors.py
Normal file
5
imagekit/processors.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""
|
||||||
|
Looking for processors? They have moved to PILKit. See imagekit.importers for
|
||||||
|
details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
from pilkit.processors import *
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
# Base
|
|
||||||
'ProcessorPipeline', 'Adjust', 'Reflection', 'Transpose',
|
|
||||||
'Anchor', 'MakeOpaque',
|
|
||||||
# Crop
|
|
||||||
'TrimBorderColor', 'Crop', 'SmartCrop',
|
|
||||||
# Resize
|
|
||||||
'Resize', 'ResizeToCover', 'ResizeToFill', 'SmartResize',
|
|
||||||
'ResizeCanvas', 'AddBorder', 'ResizeToFit', 'Thumbnail'
|
|
||||||
]
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pilkit.processors.base import *
|
|
||||||
|
|
||||||
warnings.warn('imagekit.processors.base is deprecated use imagekit.processors instead', DeprecationWarning)
|
|
||||||
|
|
||||||
__all__ = ['ProcessorPipeline', 'Adjust', 'Reflection', 'Transpose', 'Anchor', 'MakeOpaque']
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pilkit.processors.crop import *
|
|
||||||
|
|
||||||
warnings.warn('imagekit.processors.crop is deprecated use imagekit.processors instead', DeprecationWarning)
|
|
||||||
|
|
||||||
__all__ = ['TrimBorderColor', 'Crop', 'SmartCrop']
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pilkit.processors.resize import *
|
|
||||||
|
|
||||||
warnings.warn('imagekit.processors.resize is deprecated use imagekit.processors instead', DeprecationWarning)
|
|
||||||
|
|
||||||
__all__ = ['Resize', 'ResizeToCover', 'ResizeToFill', 'SmartResize', 'ResizeCanvas', 'AddBorder', 'ResizeToFit', 'Thumbnail']
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import warnings
|
|
||||||
|
|
||||||
from pilkit.processors.utils import *
|
|
||||||
|
|
||||||
warnings.warn('imagekit.processors.utils is deprecated use pilkit.processors.utils instead', DeprecationWarning)
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from .exceptions import AlreadyRegistered, NotRegistered
|
from .exceptions import AlreadyRegistered, NotRegistered
|
||||||
from .signals import content_required, existence_required, source_saved
|
from .signals import content_required, existence_required, source_saved
|
||||||
from .utils import autodiscover, call_strategy_method
|
from .utils import call_strategy_method
|
||||||
|
|
||||||
|
|
||||||
class GeneratorRegistry(object):
|
class GeneratorRegistry(object):
|
||||||
|
|
@ -30,8 +30,6 @@ class GeneratorRegistry(object):
|
||||||
' registered' % id)
|
' registered' % id)
|
||||||
|
|
||||||
def get(self, id, **kwargs):
|
def get(self, id, **kwargs):
|
||||||
autodiscover()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
generator = self._generators[id]
|
generator = self._generators[id]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
@ -43,7 +41,6 @@ class GeneratorRegistry(object):
|
||||||
return generator
|
return generator
|
||||||
|
|
||||||
def get_ids(self):
|
def get_ids(self):
|
||||||
autodiscover()
|
|
||||||
return self._generators.keys()
|
return self._generators.keys()
|
||||||
|
|
||||||
def content_required_receiver(self, sender, file, **kwargs):
|
def content_required_receiver(self, sender, file, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from copy import copy
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models.fields.files import ImageFieldFile
|
from django.db.models.fields.files import ImageFieldFile
|
||||||
from ..cachefiles.backends import get_default_cachefile_backend
|
from ..cachefiles.backends import get_default_cachefile_backend
|
||||||
from ..cachefiles.strategies import load_strategy
|
from ..cachefiles.strategies import StrategyWrapper
|
||||||
from .. import hashers
|
from .. import hashers
|
||||||
from ..exceptions import AlreadyRegistered, MissingSource
|
from ..exceptions import AlreadyRegistered, MissingSource
|
||||||
from ..utils import open_image, get_by_qname, process_image
|
from ..utils import open_image, get_by_qname, process_image
|
||||||
|
|
@ -36,7 +36,7 @@ class BaseImageSpec(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.cachefile_backend = self.cachefile_backend or get_default_cachefile_backend()
|
self.cachefile_backend = self.cachefile_backend or get_default_cachefile_backend()
|
||||||
self.cachefile_strategy = load_strategy(self.cachefile_strategy)
|
self.cachefile_strategy = StrategyWrapper(self.cachefile_strategy)
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
@ -145,25 +145,17 @@ class ImageSpec(BaseImageSpec):
|
||||||
|
|
||||||
# TODO: Move into a generator base class
|
# TODO: Move into a generator base class
|
||||||
# TODO: Factor out a generate_image function so you can create a generator and only override the PIL.Image creating part. (The tricky part is how to deal with original_format since generator base class won't have one.)
|
# TODO: Factor out a generate_image function so you can create a generator and only override the PIL.Image creating part. (The tricky part is how to deal with original_format since generator base class won't have one.)
|
||||||
|
|
||||||
closed = self.source.closed
|
|
||||||
if closed:
|
|
||||||
# Django file object should know how to reopen itself if it was closed
|
|
||||||
# https://code.djangoproject.com/ticket/13750
|
|
||||||
self.source.open()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
img = open_image(self.source)
|
img = open_image(self.source)
|
||||||
new_image = process_image(img,
|
except ValueError:
|
||||||
processors=self.processors,
|
|
||||||
format=self.format,
|
# Re-open the file -- https://code.djangoproject.com/ticket/13750
|
||||||
autoconvert=self.autoconvert,
|
self.source.open()
|
||||||
options=self.options)
|
img = open_image(self.source)
|
||||||
finally:
|
|
||||||
if closed:
|
return process_image(img, processors=self.processors,
|
||||||
# We need to close the file if it was opened by us
|
format=self.format, autoconvert=self.autoconvert,
|
||||||
self.source.close()
|
options=self.options)
|
||||||
return new_image
|
|
||||||
|
|
||||||
|
|
||||||
def create_spec_class(class_attrs):
|
def create_spec_class(class_attrs):
|
||||||
|
|
|
||||||
|
|
@ -72,45 +72,33 @@ class ModelSignalRouter(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.init_instance(instance)
|
self.init_instance(instance)
|
||||||
instance._ik['source_hashes'] = dict(
|
instance._ik['source_hashes'] = dict((attname, hash(file_field))
|
||||||
(attname, hash(getattr(instance, attname)))
|
for attname, file_field in self.get_field_dict(instance).items())
|
||||||
for attname in self.get_source_fields(instance))
|
|
||||||
return instance._ik['source_hashes']
|
return instance._ik['source_hashes']
|
||||||
|
|
||||||
def get_source_fields(self, instance):
|
def get_field_dict(self, instance):
|
||||||
"""
|
"""
|
||||||
Returns a list of the source fields for the given instance.
|
Returns the source fields for the given instance, in a dictionary whose
|
||||||
|
keys are the field names and values are the fields themselves.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return set(src.image_field
|
return dict((src.image_field, getattr(instance, src.image_field)) for
|
||||||
for src in self._source_groups
|
src in self._source_groups if isinstance(instance, src.model_class))
|
||||||
if isinstance(instance, src.model_class))
|
|
||||||
|
|
||||||
@ik_model_receiver
|
@ik_model_receiver
|
||||||
def post_save_receiver(self, sender, instance=None, created=False, update_fields=None, raw=False, **kwargs):
|
def post_save_receiver(self, sender, instance=None, created=False, raw=False, **kwargs):
|
||||||
if not raw:
|
if not raw:
|
||||||
self.init_instance(instance)
|
self.init_instance(instance)
|
||||||
old_hashes = instance._ik.get('source_hashes', {}).copy()
|
old_hashes = instance._ik.get('source_hashes', {}).copy()
|
||||||
new_hashes = self.update_source_hashes(instance)
|
new_hashes = self.update_source_hashes(instance)
|
||||||
for attname in self.get_source_fields(instance):
|
for attname, file in self.get_field_dict(instance).items():
|
||||||
if update_fields and attname not in update_fields:
|
if file and old_hashes[attname] != new_hashes[attname]:
|
||||||
continue
|
|
||||||
|
|
||||||
file = getattr(instance, attname)
|
|
||||||
if file and old_hashes.get(attname) != new_hashes[attname]:
|
|
||||||
self.dispatch_signal(source_saved, file, sender, instance,
|
self.dispatch_signal(source_saved, file, sender, instance,
|
||||||
attname)
|
attname)
|
||||||
|
|
||||||
@ik_model_receiver
|
@ik_model_receiver
|
||||||
def post_init_receiver(self, sender, instance=None, **kwargs):
|
def post_init_receiver(self, sender, instance=None, **kwargs):
|
||||||
self.init_instance(instance)
|
self.update_source_hashes(instance)
|
||||||
source_fields = self.get_source_fields(instance)
|
|
||||||
local_fields = dict((field.name, field)
|
|
||||||
for field in instance._meta.local_fields
|
|
||||||
if field.name in source_fields)
|
|
||||||
instance._ik['source_hashes'] = dict(
|
|
||||||
(attname, hash(file_field))
|
|
||||||
for attname, file_field in local_fields.items())
|
|
||||||
|
|
||||||
def dispatch_signal(self, signal, file, model_class, instance, attname):
|
def dispatch_signal(self, signal, file, model_class, instance, attname):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from .compat import parse_bits
|
||||||
from ..compat import parse_bits
|
|
||||||
from ..cachefiles import ImageCacheFile
|
from ..cachefiles import ImageCacheFile
|
||||||
from ..registry import generator_registry
|
from ..registry import generator_registry
|
||||||
from ..lib import force_text
|
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
@ -44,9 +40,12 @@ class GenerateImageAssignmentNode(template.Node):
|
||||||
self._variable_name = variable_name
|
self._variable_name = variable_name
|
||||||
|
|
||||||
def get_variable_name(self, context):
|
def get_variable_name(self, context):
|
||||||
return force_text(self._variable_name)
|
return unicode(self._variable_name)
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
|
from ..utils import autodiscover
|
||||||
|
autodiscover()
|
||||||
|
|
||||||
variable_name = self.get_variable_name(context)
|
variable_name = self.get_variable_name(context)
|
||||||
context[variable_name] = get_cachefile(context, self._generator_id,
|
context[variable_name] = get_cachefile(context, self._generator_id,
|
||||||
self._generator_kwargs)
|
self._generator_kwargs)
|
||||||
|
|
@ -61,6 +60,9 @@ class GenerateImageTagNode(template.Node):
|
||||||
self._html_attrs = html_attrs
|
self._html_attrs = html_attrs
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
|
from ..utils import autodiscover
|
||||||
|
autodiscover()
|
||||||
|
|
||||||
file = get_cachefile(context, self._generator_id,
|
file = get_cachefile(context, self._generator_id,
|
||||||
self._generator_kwargs)
|
self._generator_kwargs)
|
||||||
attrs = dict((k, v.resolve(context)) for k, v in
|
attrs = dict((k, v.resolve(context)) for k, v in
|
||||||
|
|
@ -74,7 +76,7 @@ class GenerateImageTagNode(template.Node):
|
||||||
attrs['src'] = file.url
|
attrs['src'] = file.url
|
||||||
attr_str = ' '.join('%s="%s"' % (escape(k), escape(v)) for k, v in
|
attr_str = ' '.join('%s="%s"' % (escape(k), escape(v)) for k, v in
|
||||||
attrs.items())
|
attrs.items())
|
||||||
return mark_safe('<img %s />' % attr_str)
|
return mark_safe(u'<img %s />' % attr_str)
|
||||||
|
|
||||||
|
|
||||||
class ThumbnailAssignmentNode(template.Node):
|
class ThumbnailAssignmentNode(template.Node):
|
||||||
|
|
@ -87,9 +89,12 @@ class ThumbnailAssignmentNode(template.Node):
|
||||||
self._generator_kwargs = generator_kwargs
|
self._generator_kwargs = generator_kwargs
|
||||||
|
|
||||||
def get_variable_name(self, context):
|
def get_variable_name(self, context):
|
||||||
return force_text(self._variable_name)
|
return unicode(self._variable_name)
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
|
from ..utils import autodiscover
|
||||||
|
autodiscover()
|
||||||
|
|
||||||
variable_name = self.get_variable_name(context)
|
variable_name = self.get_variable_name(context)
|
||||||
|
|
||||||
generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR
|
generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR
|
||||||
|
|
@ -114,6 +119,9 @@ class ThumbnailImageTagNode(template.Node):
|
||||||
self._html_attrs = html_attrs
|
self._html_attrs = html_attrs
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
|
from ..utils import autodiscover
|
||||||
|
autodiscover()
|
||||||
|
|
||||||
generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR
|
generator_id = self._generator_id.resolve(context) if self._generator_id else DEFAULT_THUMBNAIL_GENERATOR
|
||||||
dimensions = parse_dimensions(self._dimensions.resolve(context))
|
dimensions = parse_dimensions(self._dimensions.resolve(context))
|
||||||
kwargs = dict((k, v.resolve(context)) for k, v in
|
kwargs = dict((k, v.resolve(context)) for k, v in
|
||||||
|
|
@ -135,7 +143,7 @@ class ThumbnailImageTagNode(template.Node):
|
||||||
attrs['src'] = file.url
|
attrs['src'] = file.url
|
||||||
attr_str = ' '.join('%s="%s"' % (escape(k), escape(v)) for k, v in
|
attr_str = ' '.join('%s="%s"' % (escape(k), escape(v)) for k, v in
|
||||||
attrs.items())
|
attrs.items())
|
||||||
return mark_safe('<img %s />' % attr_str)
|
return mark_safe(u'<img %s />' % attr_str)
|
||||||
|
|
||||||
|
|
||||||
def parse_ik_tag_bits(parser, bits):
|
def parse_ik_tag_bits(parser, bits):
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,18 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from hashlib import md5
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
try:
|
from django.utils.importlib import import_module
|
||||||
from importlib import import_module
|
from hashlib import md5
|
||||||
except ImportError:
|
|
||||||
from django.utils.importlib import import_module
|
|
||||||
from pilkit.utils import *
|
from pilkit.utils import *
|
||||||
from .lib import NullHandler, force_bytes
|
import re
|
||||||
|
from .lib import NullHandler
|
||||||
|
|
||||||
|
|
||||||
bad_memcached_key_chars = re.compile('[\u0000-\u001f\\s]+')
|
bad_memcached_key_chars = re.compile(ur'[\u0000-\u001f\s]+')
|
||||||
|
|
||||||
_autodiscovered = False
|
|
||||||
|
|
||||||
def get_nonabstract_descendants(model):
|
def get_nonabstract_descendants(model):
|
||||||
""" Returns all non-abstract descendants of the model. """
|
""" Returns all non-abstract descendants of the model. """
|
||||||
|
|
@ -36,7 +31,7 @@ def get_by_qname(path, desc):
|
||||||
module, objname = path[:dot], path[dot + 1:]
|
module, objname = path[:dot], path[dot + 1:]
|
||||||
try:
|
try:
|
||||||
mod = import_module(module)
|
mod = import_module(module)
|
||||||
except ImportError as e:
|
except ImportError, e:
|
||||||
raise ImproperlyConfigured('Error importing %s module %s: "%s"' %
|
raise ImproperlyConfigured('Error importing %s module %s: "%s"' %
|
||||||
(desc, module, e))
|
(desc, module, e))
|
||||||
try:
|
try:
|
||||||
|
|
@ -67,54 +62,22 @@ def autodiscover():
|
||||||
|
|
||||||
Copied from django.contrib.admin
|
Copied from django.contrib.admin
|
||||||
"""
|
"""
|
||||||
global _autodiscovered
|
|
||||||
|
|
||||||
if _autodiscovered:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
from django.utils.module_loading import autodiscover_modules
|
|
||||||
except ImportError:
|
|
||||||
# Django<1.7
|
|
||||||
_autodiscover_modules_fallback()
|
|
||||||
else:
|
|
||||||
autodiscover_modules('imagegenerators')
|
|
||||||
_autodiscovered = True
|
|
||||||
|
|
||||||
|
|
||||||
def _autodiscover_modules_fallback():
|
|
||||||
"""
|
|
||||||
Auto-discover INSTALLED_APPS imagegenerators.py modules and fail silently
|
|
||||||
when not present. This forces an import on them to register any admin bits
|
|
||||||
they may want.
|
|
||||||
|
|
||||||
Copied from django.contrib.admin
|
|
||||||
|
|
||||||
Used for Django versions < 1.7
|
|
||||||
"""
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
try:
|
from django.utils.importlib import import_module
|
||||||
from importlib import import_module
|
|
||||||
except ImportError:
|
|
||||||
from django.utils.importlib import import_module
|
|
||||||
from django.utils.module_loading import module_has_submodule
|
from django.utils.module_loading import module_has_submodule
|
||||||
|
|
||||||
for app in settings.INSTALLED_APPS:
|
for app in settings.INSTALLED_APPS:
|
||||||
# As of Django 1.7, settings.INSTALLED_APPS may contain classes instead of modules, hence the try/except
|
mod = import_module(app)
|
||||||
# See here: https://docs.djangoproject.com/en/dev/releases/1.7/#introspecting-applications
|
# Attempt to import the app's admin module.
|
||||||
try:
|
try:
|
||||||
mod = import_module(app)
|
import_module('%s.imagegenerators' % app)
|
||||||
# Attempt to import the app's admin module.
|
except:
|
||||||
try:
|
# Decide whether to bubble up this error. If the app just
|
||||||
import_module('%s.imagegenerators' % app)
|
# doesn't have an imagegenerators module, we can ignore the error
|
||||||
except:
|
# attempting to import it, otherwise we want it to bubble up.
|
||||||
# Decide whether to bubble up this error. If the app just
|
if module_has_submodule(mod, 'imagegenerators'):
|
||||||
# doesn't have an imagegenerators module, we can ignore the error
|
raise
|
||||||
# attempting to import it, otherwise we want it to bubble up.
|
|
||||||
if module_has_submodule(mod, 'imagegenerators'):
|
|
||||||
raise
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_logger(logger_name='imagekit', add_null_handler=True):
|
def get_logger(logger_name='imagekit', add_null_handler=True):
|
||||||
|
|
@ -150,13 +113,16 @@ def generate(generator):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
content = generator.generate()
|
content = generator.generate()
|
||||||
f = File(content)
|
|
||||||
# The size of the File must be known or Django will try to open a file
|
# If the file doesn't have a name, Django will raise an Exception while
|
||||||
# without a name and raise an Exception.
|
# trying to save it, so we create a named temporary file.
|
||||||
f.size = len(content.read())
|
if not getattr(content, 'name', None):
|
||||||
# After getting the size reset the file pointer for future reads.
|
f = NamedTemporaryFile()
|
||||||
content.seek(0)
|
f.write(content.read())
|
||||||
return f
|
f.seek(0)
|
||||||
|
content = f
|
||||||
|
|
||||||
|
return File(content)
|
||||||
|
|
||||||
|
|
||||||
def call_strategy_method(file, method_name):
|
def call_strategy_method(file, method_name):
|
||||||
|
|
@ -166,17 +132,6 @@ def call_strategy_method(file, method_name):
|
||||||
fn(file)
|
fn(file)
|
||||||
|
|
||||||
|
|
||||||
def get_cache():
|
|
||||||
try:
|
|
||||||
from django.core.cache import caches
|
|
||||||
except ImportError:
|
|
||||||
# Django < 1.7
|
|
||||||
from django.core.cache import get_cache
|
|
||||||
return get_cache(settings.IMAGEKIT_CACHE_BACKEND)
|
|
||||||
|
|
||||||
return caches[settings.IMAGEKIT_CACHE_BACKEND]
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize_cache_key(key):
|
def sanitize_cache_key(key):
|
||||||
if settings.IMAGEKIT_USE_MEMCACHED_SAFE_CACHE_KEY:
|
if settings.IMAGEKIT_USE_MEMCACHED_SAFE_CACHE_KEY:
|
||||||
# Memcached keys can't contain whitespace or control characters.
|
# Memcached keys can't contain whitespace or control characters.
|
||||||
|
|
@ -185,7 +140,7 @@ def sanitize_cache_key(key):
|
||||||
# The also can't be > 250 chars long. Since we don't know what the
|
# The also can't be > 250 chars long. Since we don't know what the
|
||||||
# user's cache ``KEY_FUNCTION`` setting is like, we'll limit it to 200.
|
# user's cache ``KEY_FUNCTION`` setting is like, we'll limit it to 200.
|
||||||
if len(new_key) >= 200:
|
if len(new_key) >= 200:
|
||||||
new_key = '%s:%s' % (new_key[:200-33], md5(force_bytes(key)).hexdigest())
|
new_key = '%s:%s' % (new_key[:200-33], md5(key).hexdigest())
|
||||||
|
|
||||||
key = new_key
|
key = new_key
|
||||||
return key
|
return key
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[bdist_wheel]
|
|
||||||
universal = 1
|
|
||||||
36
setup.py
36
setup.py
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#/usr/bin/env python
|
||||||
import codecs
|
import codecs
|
||||||
import os
|
import os
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
@ -7,27 +7,23 @@ import sys
|
||||||
|
|
||||||
# Workaround for multiprocessing/nose issue. See http://bugs.python.org/msg170215
|
# Workaround for multiprocessing/nose issue. See http://bugs.python.org/msg170215
|
||||||
try:
|
try:
|
||||||
import multiprocessing # NOQA
|
import multiprocessing
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if 'publish' in sys.argv:
|
if 'publish' in sys.argv:
|
||||||
os.system('python setup.py sdist bdist_wheel upload')
|
os.system('python setup.py sdist upload')
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
read = lambda filepath: codecs.open(filepath, 'r', 'utf-8').read()
|
read = lambda filepath: codecs.open(filepath, 'r', 'utf-8').read()
|
||||||
|
|
||||||
|
|
||||||
def exec_file(filepath, globalz=None, localz=None):
|
|
||||||
exec(read(filepath), globalz, localz)
|
|
||||||
|
|
||||||
|
|
||||||
# Load package meta from the pkgmeta module without loading imagekit.
|
# Load package meta from the pkgmeta module without loading imagekit.
|
||||||
pkgmeta = {}
|
pkgmeta = {}
|
||||||
exec_file(os.path.join(os.path.dirname(__file__),
|
execfile(os.path.join(os.path.dirname(__file__),
|
||||||
'imagekit', 'pkgmeta.py'), pkgmeta)
|
'imagekit', 'pkgmeta.py'), pkgmeta)
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
|
@ -41,26 +37,23 @@ setup(
|
||||||
maintainer_email='bryan@revyver.com',
|
maintainer_email='bryan@revyver.com',
|
||||||
license='BSD',
|
license='BSD',
|
||||||
url='http://github.com/matthewwithanm/django-imagekit/',
|
url='http://github.com/matthewwithanm/django-imagekit/',
|
||||||
packages=find_packages(exclude=['*.tests', '*.tests.*', 'tests.*', 'tests']),
|
packages=find_packages(),
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
tests_require=[
|
tests_require=[
|
||||||
'beautifulsoup4>=4.4.0',
|
'beautifulsoup4==4.1.3',
|
||||||
'nose>=1.3.6',
|
'nose==1.2.1',
|
||||||
'nose-progressive>=1.5.1',
|
'nose-progressive==1.3',
|
||||||
'django-nose>=1.4',
|
'django-nose==1.1',
|
||||||
'Pillow',
|
'Pillow<3.0',
|
||||||
'mock>=1.0.1',
|
|
||||||
],
|
],
|
||||||
test_suite='testrunner.run_tests',
|
test_suite='testrunner.run_tests',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'django-appconf>=0.5',
|
'django-appconf>=0.5',
|
||||||
'pilkit>=0.2.0',
|
'pilkit>=0.2.0',
|
||||||
'six',
|
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
'async': ['django-celery>=3.0'],
|
'async': ['django-celery>=3.0'],
|
||||||
'async_rq': ['django-rq>=0.6.0'],
|
|
||||||
},
|
},
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
|
|
@ -69,12 +62,9 @@ setup(
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: BSD License',
|
'License :: OSI Approved :: BSD License',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python :: 2',
|
'Programming Language :: Python :: 2.5',
|
||||||
|
'Programming Language :: Python :: 2.6',
|
||||||
'Programming Language :: Python :: 2.7',
|
'Programming Language :: Python :: 2.7',
|
||||||
'Programming Language :: Python :: 3',
|
|
||||||
'Programming Language :: Python :: 3.3',
|
|
||||||
'Programming Language :: Python :: 3.4',
|
|
||||||
'Programming Language :: Python :: 3.5',
|
|
||||||
'Topic :: Utilities'
|
'Topic :: Utilities'
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,4 @@ def run_tests():
|
||||||
cls = get_runner(settings)
|
cls = get_runner(settings)
|
||||||
runner = cls()
|
runner = cls()
|
||||||
failures = runner.run_tests(['tests'])
|
failures = runner.run_tests(['tests'])
|
||||||
# Clean autogenerated junk before exit
|
|
||||||
from tests.utils import clear_imagekit_test_files
|
|
||||||
clear_imagekit_test_files()
|
|
||||||
sys.exit(failures)
|
sys.exit(failures)
|
||||||
|
|
|
||||||
BIN
tests/assets/Lenna.png
Normal file
BIN
tests/assets/Lenna.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 463 KiB |
BIN
tests/assets/lenna-800x600-white-border.jpg
Normal file
BIN
tests/assets/lenna-800x600-white-border.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 198 KiB |
BIN
tests/assets/lenna-800x600.jpg
Normal file
BIN
tests/assets/lenna-800x600.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
BIN
tests/media/lenna.png
Normal file
BIN
tests/media/lenna.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 463 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 117 KiB |
|
|
@ -1,17 +1,10 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from imagekit import ImageSpec
|
|
||||||
from imagekit.models import ProcessedImageField
|
from imagekit.models import ProcessedImageField
|
||||||
from imagekit.models import ImageSpecField
|
from imagekit.models import ImageSpecField
|
||||||
from imagekit.processors import Adjust, ResizeToFill, SmartCrop
|
from imagekit.processors import Adjust, ResizeToFill, SmartCrop
|
||||||
|
|
||||||
|
|
||||||
class Thumbnail(ImageSpec):
|
|
||||||
processors = [ResizeToFill(100, 60)]
|
|
||||||
format = 'JPEG'
|
|
||||||
options = {'quality': 60}
|
|
||||||
|
|
||||||
|
|
||||||
class ImageModel(models.Model):
|
class ImageModel(models.Model):
|
||||||
image = models.ImageField(upload_to='b')
|
image = models.ImageField(upload_to='b')
|
||||||
|
|
||||||
|
|
@ -34,10 +27,6 @@ class ProcessedImageFieldModel(models.Model):
|
||||||
options={'quality': 90}, upload_to='p')
|
options={'quality': 90}, upload_to='p')
|
||||||
|
|
||||||
|
|
||||||
class ProcessedImageFieldWithSpecModel(models.Model):
|
|
||||||
processed = ProcessedImageField(spec=Thumbnail, upload_to='p')
|
|
||||||
|
|
||||||
|
|
||||||
class CountingCacheFileStrategy(object):
|
class CountingCacheFileStrategy(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.on_existence_required_count = 0
|
self.on_existence_required_count = 0
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ INSTALLED_APPS = [
|
||||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||||
NOSE_ARGS = [
|
NOSE_ARGS = [
|
||||||
'-s',
|
'-s',
|
||||||
|
'--with-progressive',
|
||||||
|
|
||||||
# When the tests are run --with-coverage, these args configure coverage
|
# When the tests are run --with-coverage, these args configure coverage
|
||||||
# reporting (requires coverage to be installed).
|
# reporting (requires coverage to be installed).
|
||||||
|
|
@ -44,26 +45,6 @@ NOSE_ARGS = [
|
||||||
'--cover-html-dir=%s' % os.path.join(BASE_PATH, 'cover')
|
'--cover-html-dir=%s' % os.path.join(BASE_PATH, 'cover')
|
||||||
]
|
]
|
||||||
|
|
||||||
if os.getenv('TERM'):
|
DEBUG = True
|
||||||
NOSE_ARGS.append('--with-progressive')
|
TEMPLATE_DEBUG = DEBUG
|
||||||
|
|
||||||
CACHE_BACKEND = 'locmem://'
|
CACHE_BACKEND = 'locmem://'
|
||||||
|
|
||||||
# Django >= 1.8
|
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'context_processors': [
|
|
||||||
'django.contrib.auth.context_processors.auth',
|
|
||||||
'django.template.context_processors.debug',
|
|
||||||
'django.template.context_processors.i18n',
|
|
||||||
'django.template.context_processors.media',
|
|
||||||
'django.template.context_processors.static',
|
|
||||||
'django.template.context_processors.tz',
|
|
||||||
'django.contrib.messages.context_processors.messages',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
from unittest import mock
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from imagekit.cachefiles import ImageCacheFile, LazyImageCacheFile
|
from imagekit.cachefiles import ImageCacheFile, LazyImageCacheFile
|
||||||
from imagekit.cachefiles.backends import Simple
|
from imagekit.cachefiles.backends import Simple
|
||||||
from imagekit.lib import force_bytes
|
|
||||||
from nose.tools import raises, eq_
|
from nose.tools import raises, eq_
|
||||||
from .imagegenerators import TestSpec
|
from .imagegenerators import TestSpec
|
||||||
from .utils import (assert_file_is_truthy, assert_file_is_falsy,
|
from .utils import (assert_file_is_truthy, assert_file_is_falsy,
|
||||||
|
|
@ -49,31 +47,6 @@ def test_no_source_error():
|
||||||
file.generate()
|
file.generate()
|
||||||
|
|
||||||
|
|
||||||
def test_repr_does_not_send_existence_required():
|
|
||||||
"""
|
|
||||||
Ensure that `__repr__` method does not send `existance_required` signal
|
|
||||||
|
|
||||||
Cachefile strategy may be configured to generate file on
|
|
||||||
`existance_required`.
|
|
||||||
To generate images, backend passes `ImageCacheFile` instance to worker.
|
|
||||||
Both celery and RQ calls `__repr__` method for each argument to enque call.
|
|
||||||
And if `__repr__` of object will send this signal, we will get endless
|
|
||||||
recursion
|
|
||||||
|
|
||||||
"""
|
|
||||||
with mock.patch('imagekit.cachefiles.existence_required') as signal:
|
|
||||||
# import here to apply mock
|
|
||||||
from imagekit.cachefiles import ImageCacheFile
|
|
||||||
|
|
||||||
spec = TestSpec(source=get_unique_image_file())
|
|
||||||
file = ImageCacheFile(
|
|
||||||
spec,
|
|
||||||
cachefile_backend=DummyAsyncCacheFileBackend()
|
|
||||||
)
|
|
||||||
file.__repr__()
|
|
||||||
eq_(signal.send.called, False)
|
|
||||||
|
|
||||||
|
|
||||||
def test_memcached_cache_key():
|
def test_memcached_cache_key():
|
||||||
"""
|
"""
|
||||||
Ensure the default cachefile backend is sanitizing its cache key for
|
Ensure the default cachefile backend is sanitizing its cache key for
|
||||||
|
|
@ -100,7 +73,7 @@ def test_memcached_cache_key():
|
||||||
eq_(backend.get_key(file), '%s%s:%s' % (
|
eq_(backend.get_key(file), '%s%s:%s' % (
|
||||||
settings.IMAGEKIT_CACHE_PREFIX,
|
settings.IMAGEKIT_CACHE_PREFIX,
|
||||||
'1' * (200 - len(':') - 32 - len(settings.IMAGEKIT_CACHE_PREFIX)),
|
'1' * (200 - len(':') - 32 - len(settings.IMAGEKIT_CACHE_PREFIX)),
|
||||||
md5(force_bytes('%s%s-state' % (settings.IMAGEKIT_CACHE_PREFIX, filename))).hexdigest()))
|
md5('%s%s-state' % (settings.IMAGEKIT_CACHE_PREFIX, filename)).hexdigest()))
|
||||||
|
|
||||||
|
|
||||||
def test_lazyfile_stringification():
|
def test_lazyfile_stringification():
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
from nose.tools import assert_false, assert_true
|
|
||||||
|
|
||||||
from .models import Thumbnail
|
|
||||||
from .utils import create_photo
|
|
||||||
|
|
||||||
|
|
||||||
def test_do_not_leak_open_files():
|
|
||||||
instance = create_photo('leak-test.jpg')
|
|
||||||
source_file = instance.original_image
|
|
||||||
# Ensure the FieldFile is closed before generation
|
|
||||||
source_file.close()
|
|
||||||
image_generator = Thumbnail(source=source_file)
|
|
||||||
image_generator.generate()
|
|
||||||
assert_true(source_file.closed)
|
|
||||||
|
|
||||||
|
|
||||||
def test_do_not_close_open_files_after_generate():
|
|
||||||
instance = create_photo('do-not-close-test.jpg')
|
|
||||||
source_file = instance.original_image
|
|
||||||
# Ensure the FieldFile is opened before generation
|
|
||||||
source_file.open()
|
|
||||||
image_generator = Thumbnail(source=source_file)
|
|
||||||
image_generator.generate()
|
|
||||||
assert_false(source_file.closed)
|
|
||||||
source_file.close()
|
|
||||||
|
|
@ -5,9 +5,7 @@ from imagekit import forms as ikforms
|
||||||
from imagekit.processors import SmartCrop
|
from imagekit.processors import SmartCrop
|
||||||
from nose.tools import eq_
|
from nose.tools import eq_
|
||||||
from . import imagegenerators # noqa
|
from . import imagegenerators # noqa
|
||||||
from .models import (ProcessedImageFieldModel,
|
from .models import ProcessedImageFieldModel, ImageModel
|
||||||
ProcessedImageFieldWithSpecModel,
|
|
||||||
ImageModel)
|
|
||||||
from .utils import get_image_file
|
from .utils import get_image_file
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -21,16 +19,6 @@ def test_model_processedimagefield():
|
||||||
eq_(instance.processed.height, 50)
|
eq_(instance.processed.height, 50)
|
||||||
|
|
||||||
|
|
||||||
def test_model_processedimagefield_with_spec():
|
|
||||||
instance = ProcessedImageFieldWithSpecModel()
|
|
||||||
file = File(get_image_file())
|
|
||||||
instance.processed.save('whatever.jpeg', file)
|
|
||||||
instance.save()
|
|
||||||
|
|
||||||
eq_(instance.processed.width, 100)
|
|
||||||
eq_(instance.processed.height, 60)
|
|
||||||
|
|
||||||
|
|
||||||
def test_form_processedimagefield():
|
def test_form_processedimagefield():
|
||||||
class TestForm(forms.ModelForm):
|
class TestForm(forms.ModelForm):
|
||||||
image = ikforms.ProcessedImageField(spec_id='tests:testform_image',
|
image = ikforms.ProcessedImageField(spec_id='tests:testform_image',
|
||||||
|
|
@ -38,7 +26,6 @@ def test_form_processedimagefield():
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ImageModel
|
model = ImageModel
|
||||||
fields = 'image',
|
|
||||||
|
|
||||||
upload_file = get_image_file()
|
upload_file = get_image_file()
|
||||||
file_dict = {'image': SimpleUploadedFile('abc.jpg', upload_file.read())}
|
file_dict = {'image': SimpleUploadedFile('abc.jpg', upload_file.read())}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
from django.template import TemplateSyntaxError
|
from django.template import TemplateSyntaxError
|
||||||
from nose.tools import eq_, assert_false, raises, assert_not_equal
|
from nose.tools import eq_, assert_false, raises, assert_not_equal
|
||||||
from . import imagegenerators # noqa
|
from . import imagegenerators # noqa
|
||||||
from .utils import render_tag, get_html_attrs, clear_imagekit_cache
|
from .utils import render_tag, get_html_attrs
|
||||||
|
|
||||||
|
|
||||||
def test_img_tag():
|
def test_img_tag():
|
||||||
ttag = r"""{% generateimage 'testspec' source=img %}"""
|
ttag = r"""{% generateimage 'testspec' source=img %}"""
|
||||||
clear_imagekit_cache()
|
|
||||||
attrs = get_html_attrs(ttag)
|
attrs = get_html_attrs(ttag)
|
||||||
expected_attrs = set(['src', 'width', 'height'])
|
expected_attrs = set(['src', 'width', 'height'])
|
||||||
eq_(set(attrs.keys()), expected_attrs)
|
eq_(set(attrs.keys()), expected_attrs)
|
||||||
|
|
@ -16,7 +15,6 @@ def test_img_tag():
|
||||||
|
|
||||||
def test_img_tag_attrs():
|
def test_img_tag_attrs():
|
||||||
ttag = r"""{% generateimage 'testspec' source=img -- alt="Hello" %}"""
|
ttag = r"""{% generateimage 'testspec' source=img -- alt="Hello" %}"""
|
||||||
clear_imagekit_cache()
|
|
||||||
attrs = get_html_attrs(ttag)
|
attrs = get_html_attrs(ttag)
|
||||||
eq_(attrs.get('alt'), 'Hello')
|
eq_(attrs.get('alt'), 'Hello')
|
||||||
|
|
||||||
|
|
@ -30,7 +28,7 @@ def test_dangling_html_attrs_delimiter():
|
||||||
@raises(TemplateSyntaxError)
|
@raises(TemplateSyntaxError)
|
||||||
def test_html_attrs_assignment():
|
def test_html_attrs_assignment():
|
||||||
"""
|
"""
|
||||||
You can either use generateimage as an assignment tag or specify html attrs,
|
You can either use generateimage as an assigment tag or specify html attrs,
|
||||||
but not both.
|
but not both.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -44,13 +42,11 @@ def test_single_dimension_attr():
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ttag = r"""{% generateimage 'testspec' source=img -- width="50" %}"""
|
ttag = r"""{% generateimage 'testspec' source=img -- width="50" %}"""
|
||||||
clear_imagekit_cache()
|
|
||||||
attrs = get_html_attrs(ttag)
|
attrs = get_html_attrs(ttag)
|
||||||
assert_false('height' in attrs)
|
assert_false('height' in attrs)
|
||||||
|
|
||||||
|
|
||||||
def test_assignment_tag():
|
def test_assignment_tag():
|
||||||
ttag = r"""{% generateimage 'testspec' source=img as th %}{{ th.url }}{{ th.height }}{{ th.width }}"""
|
ttag = r"""{% generateimage 'testspec' source=img as th %}{{ th.url }}"""
|
||||||
clear_imagekit_cache()
|
|
||||||
html = render_tag(ttag)
|
html = render_tag(ttag)
|
||||||
assert_not_equal(html.strip(), '')
|
assert_not_equal(html.strip(), '')
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
from nose.tools import assert_false
|
|
||||||
from unittest.mock import Mock, PropertyMock, patch
|
|
||||||
from .models import Photo
|
|
||||||
|
|
||||||
|
|
||||||
def test_dont_access_source():
|
|
||||||
"""
|
|
||||||
Touching the source may trigger an unneeded query.
|
|
||||||
See <https://github.com/matthewwithanm/django-imagekit/issues/295>
|
|
||||||
|
|
||||||
"""
|
|
||||||
pmock = PropertyMock()
|
|
||||||
pmock.__get__ = Mock()
|
|
||||||
with patch.object(Photo, 'original_image', pmock):
|
|
||||||
photo = Photo() # noqa
|
|
||||||
assert_false(pmock.__get__.called)
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
from nose.tools import assert_true, assert_false
|
|
||||||
from imagekit.cachefiles import ImageCacheFile
|
|
||||||
from unittest.mock import Mock
|
|
||||||
from .utils import create_image
|
|
||||||
from django.core.files.storage import FileSystemStorage
|
|
||||||
from imagekit.cachefiles.backends import Simple as SimpleCFBackend
|
|
||||||
from imagekit.cachefiles.strategies import Optimistic as OptimisticStrategy
|
|
||||||
|
|
||||||
|
|
||||||
class ImageGenerator(object):
|
|
||||||
def generate(self):
|
|
||||||
return create_image()
|
|
||||||
|
|
||||||
def get_hash(self):
|
|
||||||
return 'abc123'
|
|
||||||
|
|
||||||
|
|
||||||
def get_image_cache_file():
|
|
||||||
storage = Mock(FileSystemStorage)
|
|
||||||
backend = SimpleCFBackend()
|
|
||||||
strategy = OptimisticStrategy()
|
|
||||||
generator = ImageGenerator()
|
|
||||||
return ImageCacheFile(generator, storage=storage,
|
|
||||||
cachefile_backend=backend,
|
|
||||||
cachefile_strategy=strategy)
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_io_on_bool():
|
|
||||||
"""
|
|
||||||
When checking the truthiness of an ImageCacheFile, the storage shouldn't
|
|
||||||
peform IO operations.
|
|
||||||
|
|
||||||
"""
|
|
||||||
file = get_image_cache_file()
|
|
||||||
bool(file)
|
|
||||||
assert_false(file.storage.exists.called)
|
|
||||||
assert_false(file.storage.open.called)
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_io_on_url():
|
|
||||||
"""
|
|
||||||
When getting the URL of an ImageCacheFile, the storage shouldn't be
|
|
||||||
checked.
|
|
||||||
|
|
||||||
"""
|
|
||||||
file = get_image_cache_file()
|
|
||||||
file.url
|
|
||||||
assert_false(file.storage.exists.called)
|
|
||||||
assert_false(file.storage.open.called)
|
|
||||||
|
|
@ -4,13 +4,10 @@ deserialized. This is important when using IK with Celery.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from imagekit.cachefiles import ImageCacheFile
|
from .utils import create_photo, pickleback
|
||||||
from .imagegenerators import TestSpec
|
|
||||||
from .utils import create_photo, pickleback, get_unique_image_file, clear_imagekit_cache
|
|
||||||
|
|
||||||
|
|
||||||
def test_imagespecfield():
|
def test_imagespecfield():
|
||||||
clear_imagekit_cache()
|
|
||||||
instance = create_photo('pickletest2.jpg')
|
instance = create_photo('pickletest2.jpg')
|
||||||
thumbnail = pickleback(instance.thumbnail)
|
thumbnail = pickleback(instance.thumbnail)
|
||||||
thumbnail.generate()
|
thumbnail.generate()
|
||||||
|
|
@ -23,21 +20,6 @@ def test_circular_ref():
|
||||||
This corresponds to #234
|
This corresponds to #234
|
||||||
|
|
||||||
"""
|
"""
|
||||||
clear_imagekit_cache()
|
|
||||||
instance = create_photo('pickletest3.jpg')
|
instance = create_photo('pickletest3.jpg')
|
||||||
instance.thumbnail # Cause thumbnail to be added to instance's __dict__
|
instance.thumbnail # Cause thumbnail to be added to instance's __dict__
|
||||||
pickleback(instance)
|
pickleback(instance)
|
||||||
|
|
||||||
|
|
||||||
def test_cachefiles():
|
|
||||||
clear_imagekit_cache()
|
|
||||||
spec = TestSpec(source=get_unique_image_file())
|
|
||||||
file = ImageCacheFile(spec)
|
|
||||||
file.url
|
|
||||||
# remove link to file from spec source generator
|
|
||||||
# test __getstate__ of ImageCacheFile
|
|
||||||
file.generator.source = None
|
|
||||||
restored_file = pickleback(file)
|
|
||||||
assert file is not restored_file
|
|
||||||
# Assertion for #437 and #451
|
|
||||||
assert file.storage is restored_file.storage
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
from django.template import TemplateSyntaxError
|
from django.template import TemplateSyntaxError
|
||||||
from nose.tools import eq_, raises, assert_not_equal
|
from nose.tools import eq_, raises, assert_not_equal
|
||||||
from . import imagegenerators # noqa
|
from . import imagegenerators # noqa
|
||||||
from .utils import render_tag, get_html_attrs, clear_imagekit_cache
|
from .utils import render_tag, get_html_attrs
|
||||||
|
|
||||||
|
|
||||||
def test_img_tag():
|
def test_img_tag():
|
||||||
ttag = r"""{% thumbnail '100x100' img %}"""
|
ttag = r"""{% thumbnail '100x100' img %}"""
|
||||||
clear_imagekit_cache()
|
|
||||||
attrs = get_html_attrs(ttag)
|
attrs = get_html_attrs(ttag)
|
||||||
expected_attrs = set(['src', 'width', 'height'])
|
expected_attrs = set(['src', 'width', 'height'])
|
||||||
eq_(set(attrs.keys()), expected_attrs)
|
eq_(set(attrs.keys()), expected_attrs)
|
||||||
|
|
@ -16,7 +15,6 @@ def test_img_tag():
|
||||||
|
|
||||||
def test_img_tag_attrs():
|
def test_img_tag_attrs():
|
||||||
ttag = r"""{% thumbnail '100x100' img -- alt="Hello" %}"""
|
ttag = r"""{% thumbnail '100x100' img -- alt="Hello" %}"""
|
||||||
clear_imagekit_cache()
|
|
||||||
attrs = get_html_attrs(ttag)
|
attrs = get_html_attrs(ttag)
|
||||||
eq_(attrs.get('alt'), 'Hello')
|
eq_(attrs.get('alt'), 'Hello')
|
||||||
|
|
||||||
|
|
@ -42,7 +40,7 @@ def test_too_many_args():
|
||||||
@raises(TemplateSyntaxError)
|
@raises(TemplateSyntaxError)
|
||||||
def test_html_attrs_assignment():
|
def test_html_attrs_assignment():
|
||||||
"""
|
"""
|
||||||
You can either use thumbnail as an assignment tag or specify html attrs,
|
You can either use thumbnail as an assigment tag or specify html attrs,
|
||||||
but not both.
|
but not both.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -52,20 +50,17 @@ def test_html_attrs_assignment():
|
||||||
|
|
||||||
def test_assignment_tag():
|
def test_assignment_tag():
|
||||||
ttag = r"""{% thumbnail '100x100' img as th %}{{ th.url }}"""
|
ttag = r"""{% thumbnail '100x100' img as th %}{{ th.url }}"""
|
||||||
clear_imagekit_cache()
|
|
||||||
html = render_tag(ttag)
|
html = render_tag(ttag)
|
||||||
assert_not_equal(html, '')
|
assert_not_equal(html, '')
|
||||||
|
|
||||||
|
|
||||||
def test_single_dimension():
|
def test_single_dimension():
|
||||||
ttag = r"""{% thumbnail '100x' img as th %}{{ th.width }}"""
|
ttag = r"""{% thumbnail '100x' img as th %}{{ th.width }}"""
|
||||||
clear_imagekit_cache()
|
|
||||||
html = render_tag(ttag)
|
html = render_tag(ttag)
|
||||||
eq_(html, '100')
|
eq_(html, '100')
|
||||||
|
|
||||||
|
|
||||||
def test_alternate_generator():
|
def test_alternate_generator():
|
||||||
ttag = r"""{% thumbnail '1pxsq' '100x' img as th %}{{ th.width }}"""
|
ttag = r"""{% thumbnail '1pxsq' '100x' img as th %}{{ th.width }}"""
|
||||||
clear_imagekit_cache()
|
|
||||||
html = render_tag(ttag)
|
html = render_tag(ttag)
|
||||||
eq_(html, '1')
|
eq_(html, '1')
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import os
|
import os
|
||||||
import shutil
|
from django.conf import settings
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from imagekit.cachefiles.backends import Simple, CacheFileState
|
from imagekit.cachefiles.backends import Simple, CacheFileState
|
||||||
from imagekit.conf import settings
|
|
||||||
from imagekit.lib import Image, StringIO
|
from imagekit.lib import Image, StringIO
|
||||||
from imagekit.utils import get_cache
|
|
||||||
from nose.tools import assert_true, assert_false
|
from nose.tools import assert_true, assert_false
|
||||||
import pickle
|
import pickle
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
@ -19,10 +17,9 @@ def get_image_file():
|
||||||
|
|
||||||
http://en.wikipedia.org/wiki/Lenna
|
http://en.wikipedia.org/wiki/Lenna
|
||||||
http://sipi.usc.edu/database/database.php?volume=misc&image=12
|
http://sipi.usc.edu/database/database.php?volume=misc&image=12
|
||||||
https://lintian.debian.org/tags/license-problem-non-free-img-lenna.html
|
|
||||||
https://github.com/libav/libav/commit/8895bf7b78650c0c21c88cec0484e138ec511a4b
|
|
||||||
"""
|
"""
|
||||||
path = os.path.join(settings.MEDIA_ROOT, 'reference.png')
|
path = os.path.join(settings.MEDIA_ROOT, 'lenna.png')
|
||||||
return open(path, 'r+b')
|
return open(path, 'r+b')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -64,7 +61,7 @@ def render_tag(ttag):
|
||||||
|
|
||||||
|
|
||||||
def get_html_attrs(ttag):
|
def get_html_attrs(ttag):
|
||||||
return BeautifulSoup(render_tag(ttag), features="html.parser").img.attrs
|
return BeautifulSoup(render_tag(ttag)).img.attrs
|
||||||
|
|
||||||
|
|
||||||
def assert_file_is_falsy(file):
|
def assert_file_is_falsy(file):
|
||||||
|
|
@ -77,30 +74,9 @@ def assert_file_is_truthy(file):
|
||||||
|
|
||||||
class DummyAsyncCacheFileBackend(Simple):
|
class DummyAsyncCacheFileBackend(Simple):
|
||||||
"""
|
"""
|
||||||
A cache file backend meant to simulate async generation.
|
A cache file backend meant to simulate async generation (by marking the
|
||||||
|
file as pending but never actually creating it).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
is_async = True
|
|
||||||
|
|
||||||
def generate(self, file, force=False):
|
def generate(self, file, force=False):
|
||||||
pass
|
self.set_state(file, CacheFileState.PENDING)
|
||||||
|
|
||||||
|
|
||||||
def clear_imagekit_cache():
|
|
||||||
cache = get_cache()
|
|
||||||
cache.clear()
|
|
||||||
# Clear IMAGEKIT_CACHEFILE_DIR
|
|
||||||
cache_dir = os.path.join(settings.MEDIA_ROOT, settings.IMAGEKIT_CACHEFILE_DIR)
|
|
||||||
if os.path.exists(cache_dir):
|
|
||||||
shutil.rmtree(cache_dir)
|
|
||||||
|
|
||||||
|
|
||||||
def clear_imagekit_test_files():
|
|
||||||
clear_imagekit_cache()
|
|
||||||
for fname in os.listdir(settings.MEDIA_ROOT):
|
|
||||||
if fname != 'reference.png':
|
|
||||||
path = os.path.join(settings.MEDIA_ROOT, fname)
|
|
||||||
if os.path.isdir(path):
|
|
||||||
shutil.rmtree(path)
|
|
||||||
else:
|
|
||||||
os.remove(path)
|
|
||||||
|
|
|
||||||
41
tox.ini
41
tox.ini
|
|
@ -1,18 +1,37 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
py38-django{master,30,22,21,20,111},
|
py27-django14, py27-django13, py27-django12,
|
||||||
py37-django{master,30,22,21,20,111},
|
py26-django14, py26-django13, py26-django12
|
||||||
py36-django{master,30,22,21,20,111},
|
|
||||||
py35-django{21,20,111},
|
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands = python setup.py test
|
commands = python setup.py test
|
||||||
|
|
||||||
|
[testenv:py27-django14]
|
||||||
|
basepython = python2.7
|
||||||
deps =
|
deps =
|
||||||
djangomaster: git+https://github.com/django/django.git@master#egg=Django
|
Django>=1.4,<1.5
|
||||||
django30: Django>=3.0,<3.1
|
|
||||||
django22: Django>=2.2,<3.0
|
[testenv:py27-django13]
|
||||||
django21: Django>=2.1,<2.2
|
basepython = python2.7
|
||||||
django20: Django>=2.0,<2.1
|
deps =
|
||||||
django111: Django>=1.11,<2.0
|
Django>=1.3,<1.4
|
||||||
django{21,20,111}: django-nose==1.4.5
|
|
||||||
|
[testenv:py27-django12]
|
||||||
|
basepython = python2.7
|
||||||
|
deps =
|
||||||
|
Django>=1.2,<1.3
|
||||||
|
|
||||||
|
[testenv:py26-django14]
|
||||||
|
basepython = python2.6
|
||||||
|
deps =
|
||||||
|
Django>=1.4,<1.5
|
||||||
|
|
||||||
|
[testenv:py26-django13]
|
||||||
|
basepython = python2.6
|
||||||
|
deps =
|
||||||
|
Django>=1.3,<1.4
|
||||||
|
|
||||||
|
[testenv:py26-django12]
|
||||||
|
basepython = python2.6
|
||||||
|
deps =
|
||||||
|
Django>=1.2,<1.3
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue