Compare commits

...

198 commits

Author SHA1 Message Date
Benedikt Willi
238573051e Fix build, remove unmaintained Django & Python versions.
Update .travis.yml

Update tox.ini

Update test_cachefiles.py

Update test_optimistic_strategy.py

Update test_no_extra_queries.py

Update test_cachefiles.py

Update .travis.yml

Update tox.ini

Update .travis.yml
2020-05-11 09:36:36 +02:00
Karthikeyan Singaravelan
417e33ff5a Fix deprecation warning regarding invalid escape sequences. 2020-05-11 09:35:48 +02:00
Tim Gates
9d450a78b8
docs: Fix simple typo, assigment -> assignment
There is a small typo in tests/test_generateimage_tag.py, tests/test_thumbnail_tag.py.

Should read `assignment` rather than `assigment`.
2020-03-08 13:04:21 +11:00
Venelin Stoykov
bc12a319b3
Merge pull request #496 from nthall/documentation-links
fix broken links in documentation (#319)
2020-02-21 23:34:17 +02:00
Noah Hall
85f0741594 fix broken links in documentation (#319) 2020-02-20 22:25:29 -05:00
Venelin Stoykov
3317273401
Merge pull request #477 from vstoykov/fix/django-master
Do not check for existence if name is None
2018-10-12 23:32:50 +03:00
Venelin Stoykov
94cc8ed9e4
Merge pull request #478 from vstoykov/fix/warnings
Pass features to BeautifulSoup constructor
2018-10-12 23:31:54 +03:00
Venelin Stoykov
60f35b0af5 Pass features to BeautifulSoup constructor
This will remove a warning durring tests
2018-10-12 23:08:20 +03:00
Venelin Stoykov
2c85d5aafe Do not check for existence if name is None
This will fix tests for Django master
2018-10-12 23:06:28 +03:00
Venelin Stoykov
f3c5f7cb16
Merge pull request #475 from matthewwithanm/modernize-testing-config
Python 3.6 and Django 2.1
2018-10-12 22:41:40 +03:00
Venelin Stoykov
66db460c24 Python 3.6 and Django 2.1
Stop testing some configurations of older Django versions.
2018-09-25 00:37:25 +03:00
Venelin Stoykov
6f7de35f79
Merge pull request #469 from matthewwithanm/fix-image-cachefile-serializtion
Fix pickle serialization for ImageCacheFile
2018-06-03 18:21:48 +03:00
Roman Gorbil
de991d4048 Fix pickle serialization for ImageCacheFile
When Celery CachedFileBackend used with filesystem storage (django.core.files.storage.FileSystemStorage), everything works fine.
But there are issues with storages.backends.s3boto3.S3Boto3Storage (and it's fix from #391), as well as with django_s3_storage.storage.S3Storage.

Exception was:

```
Traceback (most recent call last):
  ...

  File "/src/django-imagekit/imagekit/cachefiles/__init__.py", line 131, in __bool__
    existence_required.send(sender=self, file=self)
  ...
  File "/libs/utils.py", line 380, in on_existence_required
    file.generate()
  File "/src/django-imagekit/imagekit/cachefiles/__init__.py", line 94, in generate
    self.cachefile_backend.generate(self, force)
  File "/src/django-imagekit/imagekit/cachefiles/backends.py", line 136, in generate
    self.schedule_generation(file, force=force)
  File "/src/django-imagekit/imagekit/cachefiles/backends.py", line 165, in schedule_generation
    _celery_task.delay(self, file.generator, force=force)
  ...
  File "/lib/python3.6/site-packages/kombu/serialization.py", line 221, in dumps
    payload = encoder(data)
  File "/lib/python3.6/site-packages/kombu/serialization.py", line 350, in pickle_dumps
    return dumper(obj, protocol=pickle_protocol)
kombu.exceptions.EncodeError: can't pickle _thread._local objects
```
2018-06-03 18:06:57 +03:00
Leonardo
595f7b35ef Enhance condition in _get_size (#463)
This fix the issue #326.
2018-04-24 15:48:43 +03:00
Venelin Stoykov
fc221335b7
Merge pull request #448 from matthewwithanm/feature/django2.0
Test against Django 2.0
2017-12-06 19:54:26 +02:00
Venelin Stoykov
58e44975c7 Test against Django 2.0 2017-12-06 00:13:26 +02:00
Venelin Stoykov
115b596a8d Merge branch 'release/4.0.2' into develop
* release/4.0.2:
  Bump version to 4.0.2
2017-12-05 22:27:15 +02:00
Venelin Stoykov
ea66e3d10d Bump version to 4.0.2 2017-11-20 10:24:12 +02:00
Venelin Stoykov
6319891697
Merge pull request #440 from matthewwithanm/fix/open-files-leak
Fixed #429 Do not leak open files after generation
2017-11-20 10:02:01 +02:00
Venelin Stoykov
6ee931398f Do not leak open files after generation 2017-11-17 18:37:54 +02:00
Venelin Stoykov
7e23384145 Merge pull request #435 from Saritasa/fix-async-with-existance-required
Fix `ImageCacheFile.__repr__` to not send signals
2017-10-11 14:39:10 +03:00
Roman Gorbil
d80f426d3c Fix ImageCacheFile.__repr__ to not send signals
Cachefile strategy may be configured to generate file when file existance required.

To generate images, async backends 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 `existnace_required` signal, we will get endless recursion.

Issue: #434
2017-10-10 17:39:46 +07:00
Venelin Stoykov
c95542ee2a Merge pull request #431 from x-yuri/generateimages
generateimages: fix taking arguments
2017-09-13 02:22:57 +03:00
Yuri Kanivetsky
de3047e73d Make generateimages support pre Django 1.8 versions 2017-09-12 20:46:24 +03:00
Yuri Kanivetsky
a153812add generateimages: fix taking arguments 2017-08-29 11:27:10 +03:00
Venelin Stoykov
364cd49278 Merge pull request #428 from adamchainz/patch-1
README - use Python 3 print function
2017-07-24 15:55:15 +03:00
Adam Johnson
2e1b574486 README - use Python 3 print function
It's 2017!!!
2017-07-24 13:41:54 +01:00
Venelin Stoykov
3819e61fdb Merge pull request #419 from vstoykov/fix/368-processedimagefield-with-spec
Fixed #368 use specs directly in ProcessedImageField
2017-06-01 20:05:47 +03:00
Venelin Stoykov
845eeab3ce Merge pull request #422 from vstoykov/fix/documentation-for-python3
Improve docs for Python 3 - files should be opened as binary
2017-06-01 20:02:59 +03:00
Venelin Stoykov
755bd34c3e In Python 3 files should be opened as binary 2017-05-31 11:07:37 +03:00
Venelin Stoykov
2b04099dc4 Fixed #368 use specs directly in ProcessedImageField
Thanks to @xcono for pointing to solution to the problem
2017-05-18 23:38:32 +03:00
Venelin Stoykov
c3dbb1edf0 Merge branch 'release/4.0.1' into develop
* release/4.0.1:
  stylling and linting fixes to setup.py
  Bump version to 4.0.1
2017-05-17 18:20:52 +03:00
Venelin Stoykov
681b85d7bf stylling and linting fixes to setup.py 2017-05-17 18:12:57 +03:00
Venelin Stoykov
f96dadbfe0 Bump version to 4.0.1 2017-05-17 17:45:22 +03:00
Venelin Stoykov
499e9e1e07 Merge pull request #417 from Proper-Job/performance_improvement
Performance improvement
2017-05-17 12:27:07 +03:00
Moritz Pfeiffer
36fa53e249 Cleaned up _autodiscovered flag handling. 2017-05-17 09:51:24 +02:00
Moritz Pfeiffer
c74d8424b8 Added huge performance improvement by running imagekit.utils.autodiscover() only once on Django > 1.7 as it was intended. 2017-05-16 14:19:05 +02:00
Venelin Stoykov
3d37fb3d3a Merge pull request #414 from mikob/develop
Improved docs to include example on how to use plain ImageSpec (defin…
2017-05-10 17:00:24 +03:00
mikob
c24455ef36 Update README.st change model->instance for clarity in defining specs outside of models. 2017-05-10 18:03:28 +09:00
mikob
934a5283ad Improved docs to include example on how to use plain ImageSpec (defined outside of model, not ImageSpecField) with AdminThumbnail. 2017-04-10 17:46:29 +09:00
Venelin Stoykov
5281859d60 Merge branch 'release/4.0' into develop
* release/4.0:
  Bump version to 4.0
2017-02-22 15:58:12 +02:00
Venelin Stoykov
46d2a9e663 Bump version to 4.0 2017-02-22 15:35:20 +02:00
Venelin Stoykov
855c9a32b1 Merge pull request #411 from vstoykov/feature/wheel
Add universal wheels support
2017-02-22 15:28:11 +02:00
Venelin Stoykov
16ab0d2c99 Merge pull request #412 from vstoykov/feature/appconfigs-module-loading
Replaces #301 autodiscover works with AppConfig
2017-02-22 15:13:14 +02:00
Venelin Stoykov
96383451a0 Universal wheels! 2017-02-22 15:11:07 +02:00
Venelin Stoykov
755193699b Replaces #301 autodiscover works with AppConfig 2017-02-22 14:21:09 +02:00
Venelin Stoykov
d3369eec89 Merge pull request #410 from vstoykov/feature/ignore-ctags
Ignore autogenerated CTags file
2017-02-22 13:53:21 +02:00
Venelin Stoykov
12fdee81dd Ignore autogenerated CTags file
It used by some IDEs to index all Symbols in the project
2017-02-22 13:28:11 +02:00
Venelin Stoykov
dee14b6c22 Merge pull request #409 from matthewwithanm/fix/south
Fixed #408 Do not try south modelinspector when not needed
2017-02-21 18:42:13 +02:00
Venelin Stoykov
2bc6241f55 Do not try south modelinspector when not needed
Fixed #408
2017-02-21 15:19:40 +02:00
Venelin Stoykov
3546c39178 Merge pull request #403 from vstoykov/improve/caching
Improve caching (By default cache files state "forever")
2017-02-17 14:35:05 +02:00
Venelin Stoykov
4d1ee41f2e Make it possible to configure IMAGEKIT_CACHE_TIMEOUT
By default cache forever
2017-02-17 02:47:19 +02:00
Venelin Stoykov
f6d3cbe4a1 Merge pull request #406 from matthewwithanm/feature/dajngo-1.11
Test against Django 1.11
2017-02-16 17:25:59 +02:00
Venelin Stoykov
48cf03b482 Test against Django 1.11 2017-02-16 14:01:30 +02:00
Venelin Stoykov
175904617e Merge pull request #405 from calmyoga/file-opened-locally
Close the file only if it has been opened locally

Fixed #404
2017-02-16 09:26:17 +02:00
rohit suri
732f7045e4 Close the file only if it has been opened locally 2017-02-15 21:24:32 -08:00
Venelin Stoykov
95e484d073 Cleanup caching configuration
Requires Django 1.3+
2017-02-09 00:43:08 +02:00
Venelin Stoykov
47ff56cfe2 Merge pull request #392 from sobolevn/patch-1
updated readme.rst with a svg badge
2016-11-09 23:47:05 +02:00
Sobolev Nikita
c354bb365a updated readme.rst with a svg badge 2016-11-05 20:47:32 +03:00
Venelin Stoykov
7e4bf0e3d8 Merge pull request #390 from papercapp/develop
honor post_save's update_fields and only fire the source_saved signal…
2016-10-26 14:57:56 +03:00
Hannes Tismer
f1f295e054 honor post_save's update_fields and only fire the source_saved signal when needed 2016-10-24 16:21:53 +02:00
Venelin Stoykov
6457cf0c55 Merge pull request #384 from vstoykov/fix-350
Fixed #350: Error when trying to access width/height after url
2016-08-02 01:53:34 +03:00
Venelin Stoykov
6a8fe5f83c Merge pull request #385 from vstoykov/ignore/vscode
Ignore VSCode workspace config files
2016-07-17 05:28:56 +03:00
Venelin Stoykov
3c0c47d8ed Ignore VSCode workspace config files 2016-07-17 05:23:13 +03:00
Venelin Stoykov
d86ec082f1 Fixed #350: Error when trying to access width/height after url
If the file is closed and something is calling `open` now the file will be opened correctly event if it was already closed
2016-07-17 05:08:01 +03:00
Venelin Stoykov
23a243c51e Merge pull request #383 from vstoykov/cleanup/tests
Fixes #382: Tests no longer leave junk
2016-07-17 05:00:56 +03:00
Venelin Stoykov
07d29b3bf7 Fixes #382: Tests no longer leave junk 2016-07-17 04:54:03 +03:00
Venelin Stoykov
5061679b17 Merge pull request #380 from vstoykov/update-django
Fixes #379 Support for Django 1.10
2016-07-14 13:55:40 +03:00
Venelin Stoykov
5cde74e3e2 Fixes #379 Support for Django 1.10 2016-07-11 01:59:32 +03:00
Venelin Stoykov
e9425df833 Merge pull request #378 from vstoykov/ignore-idea
Ignore .idea from git
2016-07-09 14:24:34 +03:00
Venelin Stoykov
f98ee822a4 Ignore .idea from git 2016-07-09 14:23:42 +03:00
Venelin Stoykov
52ad8a0ace Merge pull request #363 from fladi/include_tests_in_source
Include the test suite in the sourcetarball but do not install it.
2016-02-25 09:56:07 +02:00
Michael Fladischer
f2255a5d3a Include the test suite in the sourcetarball but do not install it.
I reworked the `MANIFEST.in` to include the whole test suite so it can be used
by distribution packages during build time. It is excluded from the installed
packages automatically.

The inclusion rules for the documentation were also made more verbose to prevent
build artifacts from entering the source tarball (think .pyc files).
2016-02-25 08:40:18 +01:00
Venelin Stoykov
03a8d0d443 Merge pull request #367 from vstoykov/drop-older-support
Fix travis configuration to Include Python 3.5 and remove old Django versions
2016-02-25 02:28:17 +02:00
Venelin Stoykov
b460a66874 Make travis happy 2016-02-25 02:23:01 +02:00
Venelin Stoykov
124b23ccc8 Merge pull request #366 from vstoykov/drop-older-support
Drop support for Django 1.2 and 1.3 and Python 2.6 and 3.2
2016-02-25 01:42:54 +02:00
Venelin Stoykov
371a3bb376 Drop support for older Django and Python versions
This change drop official support for Django 1.2 and 1.3
and Python 2.6 and 3.2

In this commit test requirements are not so strict to allow versions
available in Debian sid.

Fixes #362

Many thanks to @fladi
2016-02-25 01:23:39 +02:00
Matthew Dapena-Tretter
b3084b43b2 Merge pull request #364 from fladi/non_free_lenna
Replace Lenna image in tests with a truly free alternative.
2016-02-09 18:36:43 -08:00
Michael Fladischer
4e370fdc59 Replace Lenna image in tests with a truly free alternative.
The Lenna image used in image processing tests is considered problematic due to
its unclear copyright status. Right now it is considered to be "overlooked" by
the copyright holder Playboy.

A suitable replacement image is already provided in the libav sources which is
licensed under the Expat (MIT) license and thus truly free.

This replaces the Lenna image with the one from the libav project.
2016-02-09 11:43:05 +01:00
Venelin Stoykov
7ddca36712 Merge pull request #354 from vstoykov/compat
Move compat module outside of templatetags package
2015-12-26 18:06:31 +02:00
Venelin Stoykov
340e26cd67 Move compat module outside of templatetags package 2015-12-26 17:58:22 +02:00
Venelin Stoykov
5ce8b9f072 Merge pull request #353 from vstoykov/improve-tox
Fix test requirements for django 1.9 and Python3.5
2015-12-26 17:57:18 +02:00
Venelin Stoykov
d280ad8989 Fix test requirements for django 1.9 and Python3.5 2015-12-24 23:37:53 +02:00
Bryan Veloso
0c435539df Merge branch 'release/3.3' into develop
* release/3.3:
  Add @vstoykov to the author list.
  Bump version number.
2015-12-08 11:41:50 -08:00
Bryan Veloso
7903efd9b7 Add @vstoykov to the author list. 2015-12-08 11:39:03 -08:00
Bryan Veloso
53fb3a8722 Bump version number. 2015-12-08 11:38:39 -08:00
Venelin Stoykov
d1e877f07d Merge pull request #345 from Photonomie/fix-django19
Django 1.9 compatibility

Fixes #347 
Fixes #340 
Fixes #321 
Fixes #317
2015-12-03 23:56:08 +02:00
Pierre Dulac
cec8cd7780 Update django-nose version to work with Django 1.9 2015-10-31 19:14:32 +01:00
Pierre Dulac
e79d2ba60e Add a missing env to the tox matrix 2015-10-31 19:02:02 +01:00
Pierre Dulac
97dc4b6cb2 Work a compatibility implementation for Django 1.2 2015-10-31 18:51:07 +01:00
Pierre Dulac
6fabad9749 Tells tox to only run the designated env 2015-10-31 11:43:17 +01:00
Pierre Dulac
b475de7b48 Enable the new travis architecture for speed and reliability 2015-10-31 11:39:46 +01:00
Pierre Dulac
820d2f00eb Allow the test to fail fast 2015-10-31 11:39:06 +01:00
Pierre Dulac
c89a63edbe Allow travis to fail for the python3.5 interpreter not yet available 2015-10-31 11:38:40 +01:00
Pierre Dulac
ecf5e892e2 Use the env conf for travis to split the test builds 2015-10-31 11:37:44 +01:00
Pierre Dulac
c858936e0c Add tox env for django 1.9
supported python versions can be found at
https://docs.djangoproject.com/en/1.9/releases/1.9/
2015-10-31 11:36:01 +01:00
Pierre Dulac
7f36f897f8 Update the doc to reflect the new IMAGEKIT_CACHE_BACKEND behavior 2015-10-31 00:22:55 +01:00
Pierre Dulac
5855e97997 Cleaner implementation thanks to @vstoykov explanation 2015-10-30 16:23:28 +01:00
Pierre Dulac
e155b632cd Handle cases where DEFAULT_CACHE_ALIAS is None in old Django versions 2015-10-30 00:24:50 +01:00
Pierre Dulac
fbf15befb8 Do not take a decision on which cache to use in DEBUG mode
maybe the developer wants to test his cache configuration locally, or
maybe he has to test different types of caches, we just don't know
2015-10-29 23:27:02 +01:00
Pierre Dulac
0a0708d2d6 Use a compat method to wrap the new way of retrieving the cache engine 2015-10-29 23:03:48 +01:00
Bryan Veloso
673b95b4c4 Merge branch 'release/3.2.7' into develop
* release/3.2.7:
  Bump the version to 3.2.7.
2015-08-23 18:02:55 -07:00
Bryan Veloso
75763b80f8 Bump the version to 3.2.7. 2015-08-23 17:58:46 -07:00
Matthew Dapena-Tretter
71e2a5b802 Merge pull request #335 from mrigor/close-files
Close files
2015-08-21 16:25:42 -04:00
Igor
7cdda46070 Fixes open cache file never getting closed
In a processes that generates many images, you could run into a
cituation with too man files being open. This results in:
IOError: [Errno 24] Too many open files
2015-08-02 00:05:43 -07:00
Igor
eb81b9c88c Fixes open source file never getting closed
In a processes that generates many images, you could run into a
cituation with too man files being open. This results in:
IOError: [Errno 24] Too many open files
2015-08-02 00:01:30 -07:00
Matthew Dapena-Tretter
9e5ef330fa Merge pull request #324 from vstoykov/python34-django18
Add test environments for Python3.4 and Django1.7 and Django1.8
2015-07-10 09:01:32 -04:00
Venelin Stoykov
458f80050c Do not use progressive when we are not running in terminal 2015-06-05 03:01:07 +03:00
Venelin Stoykov
e455768352 Add test environments for Python3.4 and Django1.7 and Django1.8 2015-06-05 03:01:00 +03:00
Matthew Dapena-Tretter
db70f810ad Merge pull request #323 from vegaro/develop
Fixes imports in README example for ProcessedImageField
2015-05-10 20:40:05 -04:00
Cesar de la Vega
561b5d7ab7 Fixes imports in README example for ProcessedImageField 2015-05-10 23:59:26 +01:00
Bryan Veloso
6bb45bc532 Merge branch 'release/3.2.6' into develop
* release/3.2.6:
  Bump the version to 3.2.6.
2015-02-26 10:28:58 -08:00
Bryan Veloso
d6bbff47f0 Bump the version to 3.2.6. 2015-02-26 10:28:38 -08:00
ILYA
41f45a4fe7 Updated importlib import to fix DeprecationWarning (for django 1.8) 2015-02-26 10:28:11 -05:00
Matthew Dapena-Tretter
8ad3d1e8be Merge pull request #311 from tino/feature/docs-async-optimistic
Add note about usage of optimistic strategy with async backend
2015-02-21 08:45:01 -05:00
Tino de Bruijn
f6e0033aae Add note about usage of optimistic strategy with async backend 2015-02-20 17:58:53 +01:00
David Ray
207849e48e Fix typo 2015-01-27 09:36:16 -05:00
Bryan Veloso
c5738740fb Merge branch 'release/3.2.5' into develop
* release/3.2.5:
  Bump the version to 3.2.5.
2015-01-05 15:58:54 -08:00
Bryan Veloso
a159e7c75b Bump the version to 3.2.5. 2015-01-05 15:58:38 -08:00
Niklas A Emanuelsson
d9fe8d24b2 Explicitly setting serializer for celery task 2015-01-02 15:40:40 +01:00
Bryan Veloso
5275d613e6 Merge branch 'release/3.2.4' into develop
* release/3.2.4:
  Bump the version to 3.2.4.
2014-09-28 13:24:00 -07:00
Bryan Veloso
1d5606b3d7 Bump the version to 3.2.4. 2014-09-28 13:23:32 -07:00
Matthew Dapena-Tretter
7f40d4fd4b Merge pull request #291 from danxshap/import_fix
Fix a Django 1.7 issue with importing INSTALLED_APPS modules
2014-09-28 16:06:16 -04:00
Matthew Dapena-Tretter
94255855db Merge branch 'processors-deprecation' into develop
* processors-deprecation:
  Deprecate `imagekit.processors` submodules
2014-09-28 12:12:54 -04:00
Venelin Stoykov
1ac3399737 Deprecate imagekit.processors submodules
- `base`, `crop`, `resize`, and `utils` are now placed in `pilkit` app
- remove magic compatibility between `imagekit.processors` and `pilkit.procesors`
2014-09-28 18:31:33 +03:00
Bryan Veloso
e56f8c5925 Merge branch 'release/3.2.3' into develop
* release/3.2.3:
  Bump the version to 3.2.3.
2014-09-27 22:21:24 -07:00
Bryan Veloso
5f4f7070f4 Bump the version to 3.2.3. 2014-09-27 22:20:40 -07:00
Matthew Dapena-Tretter
3a2150e515 Exclude tests from dist
Related: matthewwithanm/pilkit#14
2014-09-27 18:03:07 -04:00
Matthew Dapena-Tretter
e2ae850866 Revert "Remove test dir __init__.py"
This reverts commit f5b23a67bd.

I forgot we were using 'tests.settings' as a settings module path.
2014-09-27 17:52:21 -04:00
Matthew Dapena-Tretter
b9b95717c6 Merge branch 'no-extra-queries' into develop
* no-extra-queries:
  Only include fetched fields in initial hash of sources
  Add test to illustrate GH-295
2014-09-27 15:29:52 -04:00
Matthew Dapena-Tretter
78a1ccaf2f Only include fetched fields in initial hash of sources
Should avoid unnecessary queries, as detailed in GH-295.
2014-09-26 22:33:16 -04:00
Matthew Dapena-Tretter
8d35dad5fc Add test to illustrate GH-295 2014-09-26 21:32:01 -04:00
Matthew Dapena-Tretter
002b5bdac8 Merge pull request #292 from njamaleddine/develop
Fixed minor spelling error in README.rst
2014-09-23 22:11:01 -04:00
Matthew Dapena-Tretter
bbf48a7953 Test that there isn't IO done when you get a URL 2014-09-23 18:41:09 -04:00
Matthew Dapena-Tretter
00b4388245 Support should_verify_existence on strategies
This prevents extra IO. Different defaults are used for async backends
since we can’t assume that `existence_required` resulted in existence
synchronously.
2014-09-23 18:41:09 -04:00
Matthew Dapena-Tretter
c92f53c1b0 Test that Optimistic strategy doesn't cause reads
Using the Optimistic strategy should prevent IO ops when you cast the
file as a boolean.
2014-09-23 18:40:58 -04:00
Matthew Dapena-Tretter
9f4192a7c6 Ignore my Python3 virtualenv 2014-09-23 18:29:52 -04:00
Matthew Dapena-Tretter
f5b23a67bd Remove test dir __init__.py
Related: matthewwithanm/pilkit#14
2014-09-23 14:59:20 -04:00
Nabil
945a5623ef Fixed minor spelling error in README.rst 2014-09-21 18:50:44 -04:00
danxshap
06b06dbced Catch autodiscover module import error 2014-09-08 18:28:49 -04:00
Bryan Veloso
89b5666b02 Merge branch 'release/3.2.2' into develop
* release/3.2.2:
  Bump version to 3.2.2.
2014-07-14 12:24:46 -07:00
Bryan Veloso
d013b82c7f Bump version to 3.2.2. 2014-07-14 12:24:05 -07:00
Colin Wood
2f7bfe5dc7 Make sure image files has a name associated.
The generate image command will run into issues if the ImageSpecField does not
have any image file source associated wit hti. Like a Optional image field. So
we can not generate the images for that. So this should check to make sure that
it has one.
2014-07-11 10:07:43 -04:00
Bryan Veloso
5bb41bdccd Merge branch 'release/3.2.1' into develop
* release/3.2.1:
  Bump the version to 3.2.1.
2014-04-04 09:27:48 -07:00
Bryan Veloso
1d80e83732 Bump the version to 3.2.1. 2014-04-04 09:26:54 -07:00
Matthew Dapena-Tretter
1448e7dddd Merge pull request #279 from mkai/_rq_async_refactor
Simplified RQ cache file backed by using the job decorator
2014-04-04 10:33:27 -04:00
Markus Kaiserswerth
3056b3efc0 Simplified RQ cache file backed by using the job decorator 2014-04-04 14:24:21 +02:00
Matthew Dapena-Tretter
f45491bddb Merge pull request #278 from mkai/_rq_async_cleanup
Cleaned up RQ cache file backend code, added async_rq setup extra
2014-03-23 18:50:45 -04:00
Matthew Dapena-Tretter
b869f78b35 Merge pull request #277 from mkai/_async_deprecation
Added a DeprecationWarning if (old) Async cache file backend is used
2014-03-23 18:48:10 -04:00
Matthew Dapena-Tretter
6aa99adf1c Add note about open()
Hopefully this prevents people from going through acrobatics to get a File when
they've already got one!
2014-03-19 11:27:03 -04:00
Matthew Dapena-Tretter
9be8507ebd Always call variable "source_file"
This emphasizes that it's a file object and not a PIL image and also connects it
to the earlier example which creates that variable.
2014-03-19 11:26:10 -04:00
Markus Kaiserswerth
5b0c789f6b setup.py: added 'async_rq' extra, django-rq dependency
django-rq v0.6.0 is the first version with Python 3 support.
2014-03-18 17:03:16 +01:00
Markus Kaiserswerth
c5a1be3b8e Removed unneeded django_rq import 2014-03-18 17:02:23 +01:00
Markus Kaiserswerth
85d8cb15bb Added a DeprecationWarning if Async cache file backend is used 2014-03-18 16:37:18 +01:00
Matthew Dapena-Tretter
d3aabb0db3 Merge pull request #267 from bmispelon/issue-266
Fixed #266 -- Simplified (and renamed) StrategyWrapper.
2014-01-21 08:53:56 -08:00
Matthew Dapena-Tretter
169b594b28 Merge pull request #268 from bmispelon/tox-conf-update
Updated tox.ini to test against Django 1.6.
2014-01-21 08:53:32 -08:00
Baptiste Mispelon
df8d798551 Updated tox.ini to test against Django 1.6. 2014-01-21 17:50:58 +01:00
Baptiste Mispelon
3799f3c2f4 Fixed #266 -- Simplified (and renamed) StrategyWrapper.
StrategyWrapper was unnecessarily implemented as a LazyObject
and it triggered a bug in Django (issue 21840).

Changing the lazy object to a function works just as well and
bypasses the bug.
2014-01-21 17:46:19 +01:00
Matthew Dapena-Tretter
ffd3ba384e Link to Instakit.
Related: fish2000/instakit#2
2014-01-21 10:22:10 -05:00
Bryan Veloso
14c7979e4c Merge branch 'release/3.2' into develop
* release/3.2:
  Bump the version to 3.2.
2014-01-01 15:18:51 -08:00
Bryan Veloso
1ac1a44fc5 Bump the version to 3.2. 2014-01-01 15:17:33 -08:00
Matthew Dapena-Tretter
f113fc7517 Use signal.connect for backwards compat
The receiver decorator isn't available until Django 1.3.
2014-01-01 17:47:36 -05:00
Bryan Veloso
ce9a62c02c Merge branch 'python3' of https://github.com/vstoykov/django-imagekit into vstoykov-python3
* 'python3' of https://github.com/vstoykov/django-imagekit:
  Add Venelin Stoykov to AUTHORS
  Improve logic of contributing ImageSpecFields to Models
  Use force_bytes from imagekit.lib in test_cachefiles
  Remove @vstoykov's note. Seems like the right place to me (:
  Move force_bytes into lib module
  Don't use a raw string with \u escapes
  Fix sanitizing cache key under Python 3
  Add module to sys.modules
  Test for Python 3
  Insert importer at beginning of list
  Delay Django import until needed
  Add Python 3 suport and drop support for Python 2.5

Conflicts:
	imagekit/cachefiles/__init__.py
2013-12-26 17:30:42 -08:00
Bryan Veloso
2ff015a89a Merge branch 'release/3.1' into develop
* release/3.1:
  Tagging 3.1.
2013-12-20 09:20:28 -08:00
Bryan Veloso
452a9c1b31 Tagging 3.1. 2013-12-20 09:18:57 -08:00
Venelin Stoykov
3667c09d82 Add Venelin Stoykov to AUTHORS 2013-12-16 22:38:05 +02:00
Venelin Stoykov
26aa19eeef Improve logic of contributing ImageSpecFields to Models
- If `source` is defined then register source group immediately
- If `source` is not defined then create a signal handler and attach it to
`class_prepared` signal which will see if there is only one ImageField in
the given model.

This will fix problems coused in Python 3 about using ImageSpecField
without providing a `source`.
2013-12-16 10:48:57 +02:00
Venelin Stoykov
8a600d30b3 Use force_bytes from imagekit.lib in test_cachefiles 2013-12-15 01:58:22 +02:00
Matthew Dapena-Tretter
45f10075b6 Remove @vstoykov's note. Seems like the right place to me (: 2013-12-14 13:03:02 -05:00
Matthew Dapena-Tretter
87983c5e6d Move force_bytes into lib module 2013-12-14 13:02:21 -05:00
Matthew Dapena-Tretter
c1e16696b1 Don't use a raw string with \u escapes
Apparently, Python 3.2 doesn't process these in raw strings.
See https://mail.python.org/pipermail/python-list/2012-May/624756.html
and https://mail.python.org/pipermail/python-dev/2007-May/073042.html
2013-12-14 12:54:26 -05:00
Venelin Stoykov
fb947b1937 Fix sanitizing cache key under Python 3 2013-12-14 18:56:47 +02:00
Matthew Dapena-Tretter
68cfcce3f1 Correct reference to generateimage tag
Closes GH-261
2013-12-14 10:50:58 -05:00
Matthew Dapena-Tretter
43afb7c33d Fix celery backend 2013-11-28 00:39:57 -05:00
Matthew Dapena-Tretter
687884224c Merge pull request #258 from mkai/_rq_async_cachefile_backend
Added RQ-based async cache file backend
2013-11-27 20:50:56 -08:00
Markus Kaiserswerth
af3316278d Cache file backends: dropped the "Async" in class names 2013-11-04 13:12:02 +01:00
Markus Kaiserswerth
260c6f5a10 Added RQ-based async cache file backend 2013-10-27 13:27:21 +01:00
Bryan Veloso
2ed1855aa1 Merge branch 'release/3.0.4' into develop
* release/3.0.4:
  Bump the version to 3.0.4.
2013-09-26 10:56:40 -07:00
Bryan Veloso
d6a024ed2d Bump the version to 3.0.4. 2013-09-26 10:56:22 -07:00
Matthew Tretter
5a218e1465 Merge pull request #255 from nex2hex/develop
Add __getstate__ method to ImageCacheFile
2013-09-24 06:24:33 -07:00
nex2hex
1c26a2ea5c Add __getstate__ method to ImageCacheFile 2013-09-24 15:18:59 +04:00
Matthew Tretter
857b1e160e Update IMAGEKIT_DEFAULT_FILE_STORAGE description 2013-09-05 09:42:09 -05:00
Matthew Tretter
bf1b45c943 Add module to sys.modules 2013-09-01 21:56:42 -04:00
Matthew Tretter
2e4d435f4f Test for Python 3 2013-09-01 21:56:28 -04:00
Matthew Tretter
3732b2ee09 Insert importer at beginning of list 2013-09-01 21:55:58 -04:00
Matthew Tretter
183efabca7 Delay Django import until needed 2013-09-01 21:55:37 -04:00
Venelin Stoykov
0575011529 Add Python 3 suport and drop support for Python 2.5 2013-08-19 21:27:50 +03:00
Matthew Tretter
453efac553 Merge branch 'nex2hex/develop' into develop
* nex2hex/develop:
  Update fields.py
2013-08-14 23:40:02 -04:00
Matthew Tretter
c78cbfc089 Discover image generators during lookup
Fixes GH-241
2013-08-14 23:08:40 -04:00
Sean Hayes
d647678c2e Added global boolean to prevent autodiscover() from being called more than once. 2013-08-14 22:55:28 -04:00
nex2hex
1a33c2be51 Update fields.py
Don't overwrite existing image when form saved
2013-08-13 18:52:21 +06:00
Matthew Tretter
bc8fdd7ada Run tests with Django 1.5 2013-07-27 20:50:16 -04:00
Bryan Veloso
144c388689 Merge branch 'release/3.0.3' into develop
* release/3.0.3:
  Bump to 3.0.3.
2013-07-22 10:01:53 -07:00
Bryan Veloso
3be774bbf6 Bump to 3.0.3. 2013-07-22 10:01:20 -07:00
Bryan Veloso
532188bd51 Merge branch 'fix-pending-state' into develop
* fix-pending-state:
  Remove PENDING state
2013-07-22 09:59:08 -07:00
Matthew Tretter
e36290b4ee Woops. It uses the cache if DEBUG is False. 2013-07-17 16:09:02 -04:00
Bryan Veloso
8a709a845c Merge branch 'release/3.0.2' into develop
* release/3.0.2:
  Removing the changelog. Changelogs are hard.
  Bump to 3.0.2.
2013-07-17 11:46:18 -07:00
Matthew Tretter
3444626084 Remove PENDING state
Re: #227
2013-07-16 21:41:10 -04:00
55 changed files with 880 additions and 327 deletions

6
.gitignore vendored
View file

@ -4,10 +4,14 @@
*.pyc *.pyc
.DS_Store .DS_Store
.tox .tox
.idea
.vscode
MANIFEST MANIFEST
build build
dist dist
/tests/media/* /tests/media/*
!/tests/media/lenna.png !/tests/media/reference.png
/venv /venv
/venv3
/.env /.env
/tags

View file

@ -1,7 +1,35 @@
sudo: false
language: python language: python
python: python:
- 2.7 - "3.8"
install: pip install tox --use-mirrors - "3.7"
script: tox - "3.6"
- "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"

View file

@ -28,6 +28,7 @@ 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
@ -49,3 +50,4 @@ 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

View file

@ -1,5 +1,18 @@
include AUTHORS include AUTHORS
include LICENSE include LICENSE
include README.rst include README.rst
recursive-include docs * include testrunner.py
recursive-include imagekit/templates * include setup.cfg
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

View file

@ -1,6 +1,6 @@
|Build Status|_ |Build Status|_
.. |Build Status| image:: https://travis-ci.org/matthewwithanm/django-imagekit.png?branch=develop .. |Build Status| image:: https://travis-ci.org/matthewwithanm/django-imagekit.svg?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,10 +8,15 @@ 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
@ -34,6 +39,7 @@ Installation
Usage Overview Usage Overview
============== ==============
.. _specs:
Specs Specs
----- -----
@ -65,8 +71,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
@ -83,6 +89,7 @@ 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',
@ -91,8 +98,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
@ -131,24 +138,29 @@ particularly when the processing being done depends on user input.
format = 'JPEG' format = 'JPEG'
options = {'quality': 60} options = {'quality': 60}
It's probaby not surprising that this class is capable of processing an image It's probably 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') source_file = open('/path/to/myimage.jpg', 'rb')
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', 'w') dest = open('/path/to/dest.jpg', 'wb')
dest.write(result.read()) dest.write(result.read())
dest.close() dest.close()
@ -218,7 +230,7 @@ that's what we need to pass to use our thumbnail spec:
{% load imagekit %} {% load imagekit %}
{% generateimage 'myapp:thumbnail' source=source_image %} {% generateimage 'myapp:thumbnail' source=source_file %}
This will output the following HTML: This will output the following HTML:
@ -233,7 +245,7 @@ keyword args using two dashes:
{% load imagekit %} {% load imagekit %}
{% generateimage 'myapp:thumbnail' source=source_image -- alt="A picture of Me" id="mypicture" %} {% generateimage 'myapp:thumbnail' source=source_file -- 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:
@ -242,7 +254,7 @@ assignment tag, providing access to the underlying file object:
{% load imagekit %} {% load imagekit %}
{% generateimage 'myapp:thumbnail' source=source_image as th %} {% generateimage 'myapp:thumbnail' source=source_file 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>
@ -256,7 +268,7 @@ template tag:
{% load imagekit %} {% load imagekit %}
{% thumbnail '100x50' source_image %} {% thumbnail '100x50' source_file %}
Like the generateimage tag, the thumbnail tag outputs an <img> tag: Like the generateimage tag, the thumbnail tag outputs an <img> tag:
@ -277,15 +289,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 generatethumbnail tag, you can also specify additional HTML Like with the generateimage tag, you can also specify additional HTML attributes
attributes for the thumbnail tag, or use it as an assignment tag: for the thumbnail tag, or use it as an assignment tag:
.. code-block:: html .. code-block:: html
{% load imagekit %} {% load imagekit %}
{% thumbnail '100x50' source_image -- alt="A picture of Me" id="mypicture" %} {% thumbnail '100x50' source_file -- alt="A picture of Me" id="mypicture" %}
{% thumbnail '100x50' source_image as th %} {% thumbnail '100x50' source_file as th %}
Using Specs in Forms Using Specs in Forms
@ -395,6 +407,37 @@ 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``.

View file

@ -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) yield open(name, 'rb')
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:

View file

@ -3,7 +3,7 @@ Caching
Default Backend Workflow Default Backend Workflow
================ ========================
``ImageSpec`` ``ImageSpec``
@ -29,6 +29,8 @@ 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
------------------- -------------------
@ -55,6 +57,8 @@ 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
------------------ ------------------
@ -71,7 +75,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 exsit, generates it immediately and synchronously If file doesn't exist, 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
@ -100,13 +104,21 @@ ImageKit. Each has its own pros and cons.
Caching Data About Generated Files Caching Data About Generated Files
---------------------------------- ----------------------------------
The easiest, and most significant improvement you can make to improve the Generally, once a file is generated, you will never be removing it, so by
performance of your site is to have ImageKit cache the state of your generated default ImageKit will use default cache to cache the state of generated
files. The default cache file backend will already do this (if ``DEBUG`` is files "forever" (or only 5 minutes when ``DEBUG = True``).
``True``), using your default Django cache backend, but you can make it way
better by setting ``IMAGEKIT_CACHE_BACKEND``. Generally, once a file is The time for which ImageKit will cache state is configured with
generated, you will never be removing it; therefore, if you can, you should set ``IMAGEKIT_CACHE_TIMEOUT``. If set to ``None`` this means "never expire"
``IMAGEKIT_CACHE_BACKEND`` to a cache backend that will cache forever. (default when ``DEBUG = False``). You can reduce this timeout if you want
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
@ -132,7 +144,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 `django-celery`__ 1) install `celery`__ (or `django-celery`__ if you are bound to Celery<3.1)
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
@ -164,8 +176,28 @@ 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

View file

@ -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('\d+\.\d+', pkgmeta['__version__']).group() version = re.match(r'\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__']

View file

@ -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, the storage of the source file and none is specified by the spec definition, `your default file storage`__
will be used. will be used.
@ -44,11 +44,24 @@ Settings
.. attribute:: IMAGEKIT_CACHE_BACKEND .. attribute:: IMAGEKIT_CACHE_BACKEND
:default: If ``DEBUG`` is ``True``, ``'django.core.cache.backends.dummy.DummyCache'``. :default: ``'default'``
Otherwise, ``'default'``.
The Django cache backend to be used to store information like the state of The Django cache backend alias to retrieve the shared cache instance defined
cached images (i.e. validated or not). in your settings, as described in the `Django cache section`_.
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
@ -72,3 +85,6 @@ 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

View file

@ -79,12 +79,9 @@ 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 `cache file backends`_ and `cache file strategies`_ See the documentation on :ref:`cache file backends <cache-file-backend>` and :ref:`cache file strategies <cache-file-strategy>`
for more details. for more details.
.. _`cache file backends`:
.. _`cache file strategies`:
Conditional model ``processors`` Conditional model ``processors``
-------------------------------- --------------------------------
@ -93,9 +90,7 @@ 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 `advanced usage`_ documentation for more. solution: the custom ``spec``. See the :doc:`advanced usage <advanced_usage>` documentation for more.
.. _`advanced usage`:
Conditonal ``cache_to`` file names Conditonal ``cache_to`` file names
@ -109,9 +104,7 @@ 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 `specs`_ for more. unique file names. See the documentation on :ref:`specs` for more.
.. _`specs`:
Processors have moved to PILKit Processors have moved to PILKit

View file

@ -1,5 +1,4 @@
# 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

View file

@ -1,7 +1,9 @@
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
@ -120,14 +122,54 @@ class ImageCacheFile(BaseIKFile, ImageFile):
) )
) )
def __nonzero__(self): def __bool__(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):

View file

@ -1,12 +1,13 @@
from ..utils import get_singleton, sanitize_cache_key from ..utils import get_singleton, get_cache, 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'
PENDING = 'pending' GENERATING = 'generating'
DOES_NOT_EXIST = 'does_not_exist' DOES_NOT_EXIST = 'does_not_exist'
@ -17,7 +18,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):
@ -52,8 +53,7 @@ class CachedFileBackend(object):
@property @property
def cache(self): def cache(self):
if not getattr(self, '_cache', None): if not getattr(self, '_cache', None):
from django.conf import settings self._cache = get_cache()
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): def get_state(self, file, check_if_unknown=True):
key = self.get_key(file) key = self.get_key(file)
state = self.cache.get(key) state = self.cache.get(key)
if state is None: if state is None and check_if_unknown:
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) self.cache.set(key, state, settings.IMAGEKIT_CACHE_TIMEOUT)
def __getstate__(self): def __getstate__(self):
state = copy(self.__dict__) state = copy(self.__dict__)
@ -91,9 +91,11 @@ 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) == CacheFileState.DOES_NOT_EXIST: if force or self.get_state(file) not in (CacheFileState.GENERATING, CacheFileState.EXISTS):
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):
@ -108,34 +110,86 @@ 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.storage.exists(file.name)) or (file.name and 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:
import celery from celery import task
except ImportError: except ImportError:
pass pass
else: else:
_generate_file = celery.task(ignore_result=True)(_generate_file) _celery_task = task(ignore_result=True, serializer='pickle')(_generate_file)
class Async(Simple): class Celery(BaseAsync):
""" """
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 import celery # noqa
except ImportError: except ImportError:
raise ImproperlyConfigured('You must install celery to use' raise ImproperlyConfigured('You must install celery to use'
' imagekit.cachefiles.backend.Async.') ' imagekit.cachefiles.backends.Celery.')
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):
self.set_state(file, CacheFileState.PENDING) try:
_generate_file.delay(self, file, force=force) from django_rq import job
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)

View file

@ -1,4 +1,7 @@
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
@ -26,6 +29,9 @@ 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):
@ -33,24 +39,11 @@ class DictStrategy(object):
setattr(self, k, v) setattr(self, k, v)
class StrategyWrapper(LazyObject): def load_strategy(strategy):
def __init__(self, strategy): if isinstance(strategy, six.string_types):
if isinstance(strategy, basestring): strategy = get_singleton(strategy, 'cache file strategy')
strategy = get_singleton(strategy, 'cache file strategy') elif isinstance(strategy, dict):
elif isinstance(strategy, dict): strategy = DictStrategy(strategy)
strategy = DictStrategy(strategy) elif callable(strategy):
elif callable(strategy): strategy = strategy()
strategy = strategy() return 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)

View file

@ -1,5 +1,6 @@
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):
@ -13,30 +14,24 @@ 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:
try: from django.core.cache import DEFAULT_CACHE_ALIAS
from django.core.cache.backends.dummy import DummyCache return DEFAULT_CACHE_ALIAS
except ImportError:
dummy_cache = 'dummy://'
else:
dummy_cache = 'django.core.cache.backends.dummy.DummyCache'
# DEFAULT_CACHE_ALIAS doesn't exist in Django<=1.2 if value not in settings.CACHES:
try: raise ImproperlyConfigured("{0} is not present in settings.CACHES".format(value))
from django.core.cache import DEFAULT_CACHE_ALIAS as default_cache_alias
except ImportError:
default_cache_alias = 'default'
if settings.DEBUG: return value
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):

View file

@ -1,6 +1,9 @@
from django.core.files.base import File, ContentFile from __future__ import unicode_literals
from django.utils.encoding import smart_str, smart_unicode
import os import os
from django.core.files.base import File, ContentFile
from django.utils.encoding import smart_str
from .lib import smart_text
from .utils import format_to_mimetype, extension_to_mimetype from .utils import format_to_mimetype, extension_to_mimetype
@ -46,14 +49,25 @@ class BaseIKFile(File):
def _get_size(self): def _get_size(self):
self._require_file() self._require_file()
if not self._committed: if not getattr(self, '_committed', False):
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()
self.file.open(mode) try:
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)
@ -92,4 +106,5 @@ class IKContentFile(ContentFile):
return smart_str(self.file.name or '') return smart_str(self.file.name or '')
def __unicode__(self): def __unicode__(self):
return smart_unicode(self.file.name or u'') # Python 2
return smart_text(self.file.name or '')

View file

@ -22,8 +22,12 @@ 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: if data and data != initial:
spec = self.get_spec(source=data) spec = self.get_spec(source=data)
data = generate(spec) f = 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

View file

@ -1,12 +1,16 @@
from copy import copy from copy import copy
from hashlib import md5 from hashlib import md5
from pickle import Pickler, MARK, DICT from pickle import MARK, DICT
from types import DictionaryType try:
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)
@ -20,9 +24,9 @@ class CanonicalizingPickler(Pickler):
write(MARK + DICT) write(MARK + DICT)
self.memoize(obj) self.memoize(obj)
self._batch_setitems(sorted(obj.iteritems())) self._batch_setitems(sorted(obj.items()))
dispatch[DictionaryType] = save_dict dispatch[dict] = save_dict
def pickle(obj): def pickle(obj):

View file

@ -1,29 +0,0 @@
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())

View file

@ -19,9 +19,12 @@ 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 cStringIO import StringIO from io import BytesIO as StringIO
except ImportError: except:
from StringIO import StringIO try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
try: try:
from logging import NullHandler from logging import NullHandler
@ -31,3 +34,19 @@ 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']

View file

@ -1,6 +1,7 @@
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):
@ -13,23 +14,28 @@ 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()
if args: generator_ids = options['generator_id'] if 'generator_id' in options else args
patterns = self.compile_patterns(args) if generator_ids:
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 file in cachefile_registry.get(generator_id): for image_file in cachefile_registry.get(generator_id):
self.stdout.write(' %s\n' % file) if image_file.name:
try: self.stdout.write(' %s\n' % image_file.name)
# TODO: Allow other validation actions through command option try:
file.generate() image_file.generate()
except Exception as err: except MissingSource as err:
# TODO: How should we handle failures? Don't want to error, but should call it out more than this. self.stdout.write('\t No source associated with\n')
self.stdout.write(' FAILED: %s\n' % err) except Exception as 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]

View file

@ -1,4 +1,8 @@
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
@ -13,7 +17,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 = (u'%s:%s:%s' % (cls._meta.app_label, spec_id = ('%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
@ -44,27 +48,37 @@ 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]
setattr(cls, name, ImageSpecFileDescriptor(self, name, source)) def register_source_group(source):
self._set_spec_id(cls, name) setattr(cls, name, ImageSpecFileDescriptor(self, name, source))
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):
@ -79,7 +93,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=True, spec=None, spec_id=None, **kwargs): autoconvert=None, 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
@ -87,6 +101,10 @@ 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)
@ -98,9 +116,11 @@ class ProcessedImageField(models.ImageField, SpecHostField):
return super(ProcessedImageField, self).contribute_to_class(cls, name) return super(ProcessedImageField, self).contribute_to_class(cls, name)
try: # If the project does not use south, then we will not try to add introspection
from south.modelsinspector import add_introspection_rules if 'south' in settings.INSTALLED_APPS:
except ImportError: try:
pass from south.modelsinspector import add_introspection_rules
else: except ImportError:
add_introspection_rules([], [r'^imagekit\.models\.fields\.ProcessedImageField$']) pass
else:
add_introspection_rules([], [r'^imagekit\.models\.fields\.ProcessedImageField$'])

View file

@ -1,5 +1,5 @@
__title__ = 'django-imagekit' __title__ = 'django-imagekit'
__author__ = 'Justin Driscoll, Bryan Veloso, Greg Newman, Chris Drackett, Matthew Tretter, Eric Eldredge' __author__ = 'Matthew Tretter, Venelin Stoykov, Eric Eldredge, Bryan Veloso, Greg Newman, Chris Drackett, Justin Driscoll'
__version__ = '3.0.2' __version__ = '4.0.2'
__license__ = 'BSD' __license__ = 'BSD'
__all__ = ['__title__', '__author__', '__version__', '__license__'] __all__ = ['__title__', '__author__', '__version__', '__license__']

View file

@ -1,5 +0,0 @@
"""
Looking for processors? They have moved to PILKit. See imagekit.importers for
details.
"""

View file

@ -0,0 +1,12 @@
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'
]

View file

@ -0,0 +1,7 @@
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']

View file

@ -0,0 +1,7 @@
import warnings
from pilkit.processors.crop import *
warnings.warn('imagekit.processors.crop is deprecated use imagekit.processors instead', DeprecationWarning)
__all__ = ['TrimBorderColor', 'Crop', 'SmartCrop']

View file

@ -0,0 +1,7 @@
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']

View file

@ -0,0 +1,5 @@
import warnings
from pilkit.processors.utils import *
warnings.warn('imagekit.processors.utils is deprecated use pilkit.processors.utils instead', DeprecationWarning)

View file

@ -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 call_strategy_method from .utils import autodiscover, call_strategy_method
class GeneratorRegistry(object): class GeneratorRegistry(object):
@ -30,6 +30,8 @@ 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:
@ -41,6 +43,7 @@ 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):

View file

@ -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 StrategyWrapper from ..cachefiles.strategies import load_strategy
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 = StrategyWrapper(self.cachefile_strategy) self.cachefile_strategy = load_strategy(self.cachefile_strategy)
def generate(self): def generate(self):
raise NotImplementedError raise NotImplementedError
@ -145,17 +145,25 @@ 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)
except ValueError: new_image = process_image(img,
processors=self.processors,
# Re-open the file -- https://code.djangoproject.com/ticket/13750 format=self.format,
self.source.open() autoconvert=self.autoconvert,
img = open_image(self.source) options=self.options)
finally:
return process_image(img, processors=self.processors, if closed:
format=self.format, autoconvert=self.autoconvert, # We need to close the file if it was opened by us
options=self.options) self.source.close()
return new_image
def create_spec_class(class_attrs): def create_spec_class(class_attrs):

View file

@ -72,33 +72,45 @@ class ModelSignalRouter(object):
""" """
self.init_instance(instance) self.init_instance(instance)
instance._ik['source_hashes'] = dict((attname, hash(file_field)) instance._ik['source_hashes'] = dict(
for attname, file_field in self.get_field_dict(instance).items()) (attname, hash(getattr(instance, attname)))
for attname in self.get_source_fields(instance))
return instance._ik['source_hashes'] return instance._ik['source_hashes']
def get_field_dict(self, instance): def get_source_fields(self, instance):
""" """
Returns the source fields for the given instance, in a dictionary whose Returns a list of the source fields for the given instance.
keys are the field names and values are the fields themselves.
""" """
return dict((src.image_field, getattr(instance, src.image_field)) for return set(src.image_field
src in self._source_groups if isinstance(instance, src.model_class)) for src in self._source_groups
if isinstance(instance, src.model_class))
@ik_model_receiver @ik_model_receiver
def post_save_receiver(self, sender, instance=None, created=False, raw=False, **kwargs): def post_save_receiver(self, sender, instance=None, created=False, update_fields=None, 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, file in self.get_field_dict(instance).items(): for attname in self.get_source_fields(instance):
if file and old_hashes[attname] != new_hashes[attname]: if update_fields and attname not in update_fields:
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.update_source_hashes(instance) self.init_instance(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):
""" """

View file

@ -1,9 +1,13 @@
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()
@ -40,12 +44,9 @@ 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 unicode(self._variable_name) return force_text(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)
@ -60,9 +61,6 @@ 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
@ -76,7 +74,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(u'<img %s />' % attr_str) return mark_safe('<img %s />' % attr_str)
class ThumbnailAssignmentNode(template.Node): class ThumbnailAssignmentNode(template.Node):
@ -89,12 +87,9 @@ 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 unicode(self._variable_name) return force_text(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
@ -119,9 +114,6 @@ 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
@ -143,7 +135,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(u'<img %s />' % attr_str) return mark_safe('<img %s />' % attr_str)
def parse_ik_tag_bits(parser, bits): def parse_ik_tag_bits(parser, bits):

View file

@ -1,18 +1,23 @@
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
from django.utils.importlib import import_module try:
from hashlib import md5 from importlib import import_module
except ImportError:
from django.utils.importlib import import_module
from pilkit.utils import * from pilkit.utils import *
import re from .lib import NullHandler, force_bytes
from .lib import NullHandler
bad_memcached_key_chars = re.compile(ur'[\u0000-\u001f\s]+') bad_memcached_key_chars = re.compile('[\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. """
@ -31,7 +36,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, e: except ImportError as 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:
@ -62,22 +67,54 @@ 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
from django.utils.importlib import import_module try:
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:
mod = import_module(app) # As of Django 1.7, settings.INSTALLED_APPS may contain classes instead of modules, hence the try/except
# Attempt to import the app's admin module. # See here: https://docs.djangoproject.com/en/dev/releases/1.7/#introspecting-applications
try: try:
import_module('%s.imagegenerators' % app) mod = import_module(app)
except: # Attempt to import the app's admin module.
# Decide whether to bubble up this error. If the app just try:
# doesn't have an imagegenerators module, we can ignore the error import_module('%s.imagegenerators' % app)
# attempting to import it, otherwise we want it to bubble up. except:
if module_has_submodule(mod, 'imagegenerators'): # Decide whether to bubble up this error. If the app just
raise # doesn't have an imagegenerators module, we can ignore the error
# 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):
@ -113,16 +150,13 @@ def generate(generator):
""" """
content = generator.generate() content = generator.generate()
f = File(content)
# If the file doesn't have a name, Django will raise an Exception while # The size of the File must be known or Django will try to open a file
# trying to save it, so we create a named temporary file. # without a name and raise an Exception.
if not getattr(content, 'name', None): f.size = len(content.read())
f = NamedTemporaryFile() # After getting the size reset the file pointer for future reads.
f.write(content.read()) content.seek(0)
f.seek(0) return f
content = f
return File(content)
def call_strategy_method(file, method_name): def call_strategy_method(file, method_name):
@ -132,6 +166,17 @@ 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.
@ -140,7 +185,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(key).hexdigest()) new_key = '%s:%s' % (new_key[:200-33], md5(force_bytes(key)).hexdigest())
key = new_key key = new_key
return key return key

2
setup.cfg Normal file
View file

@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1

View file

@ -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,23 +7,27 @@ 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 import multiprocessing # NOQA
except ImportError: except ImportError:
pass pass
if 'publish' in sys.argv: if 'publish' in sys.argv:
os.system('python setup.py sdist upload') os.system('python setup.py sdist bdist_wheel 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 = {}
execfile(os.path.join(os.path.dirname(__file__), exec_file(os.path.join(os.path.dirname(__file__),
'imagekit', 'pkgmeta.py'), pkgmeta) 'imagekit', 'pkgmeta.py'), pkgmeta)
setup( setup(
@ -37,23 +41,26 @@ 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(), packages=find_packages(exclude=['*.tests', '*.tests.*', 'tests.*', 'tests']),
zip_safe=False, zip_safe=False,
include_package_data=True, include_package_data=True,
tests_require=[ tests_require=[
'beautifulsoup4==4.1.3', 'beautifulsoup4>=4.4.0',
'nose==1.2.1', 'nose>=1.3.6',
'nose-progressive==1.3', 'nose-progressive>=1.5.1',
'django-nose==1.1', 'django-nose>=1.4',
'Pillow<3.0', 'Pillow',
'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',
@ -62,9 +69,12 @@ 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.5', 'Programming Language :: Python :: 2',
'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'
], ],
) )

View file

@ -16,4 +16,7 @@ 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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

BIN
tests/media/reference.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View file

@ -1,10 +1,17 @@
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')
@ -27,6 +34,10 @@ 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

View file

@ -34,7 +34,6 @@ 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).
@ -45,6 +44,26 @@ NOSE_ARGS = [
'--cover-html-dir=%s' % os.path.join(BASE_PATH, 'cover') '--cover-html-dir=%s' % os.path.join(BASE_PATH, 'cover')
] ]
DEBUG = True if os.getenv('TERM'):
TEMPLATE_DEBUG = DEBUG NOSE_ARGS.append('--with-progressive')
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',
],
},
},
]

View file

@ -1,7 +1,9 @@
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,
@ -47,6 +49,31 @@ 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
@ -73,7 +100,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('%s%s-state' % (settings.IMAGEKIT_CACHE_PREFIX, filename)).hexdigest())) md5(force_bytes('%s%s-state' % (settings.IMAGEKIT_CACHE_PREFIX, filename))).hexdigest()))
def test_lazyfile_stringification(): def test_lazyfile_stringification():

View file

@ -0,0 +1,25 @@
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()

View file

@ -5,7 +5,9 @@ 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, ImageModel from .models import (ProcessedImageFieldModel,
ProcessedImageFieldWithSpecModel,
ImageModel)
from .utils import get_image_file from .utils import get_image_file
@ -19,6 +21,16 @@ 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',
@ -26,6 +38,7 @@ 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())}

View file

@ -1,11 +1,12 @@
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 from .utils import render_tag, get_html_attrs, clear_imagekit_cache
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)
@ -15,6 +16,7 @@ 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')
@ -28,7 +30,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 assigment tag or specify html attrs, You can either use generateimage as an assignment tag or specify html attrs,
but not both. but not both.
""" """
@ -42,11 +44,13 @@ 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 }}""" ttag = r"""{% generateimage 'testspec' source=img as th %}{{ th.url }}{{ th.height }}{{ th.width }}"""
clear_imagekit_cache()
html = render_tag(ttag) html = render_tag(ttag)
assert_not_equal(html.strip(), '') assert_not_equal(html.strip(), '')

View file

@ -0,0 +1,16 @@
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)

View file

@ -0,0 +1,49 @@
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)

View file

@ -4,10 +4,13 @@ deserialized. This is important when using IK with Celery.
""" """
from .utils import create_photo, pickleback from imagekit.cachefiles import ImageCacheFile
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()
@ -20,6 +23,21 @@ 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

View file

@ -1,11 +1,12 @@
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 from .utils import render_tag, get_html_attrs, clear_imagekit_cache
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)
@ -15,6 +16,7 @@ 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')
@ -40,7 +42,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 assigment tag or specify html attrs, You can either use thumbnail as an assignment tag or specify html attrs,
but not both. but not both.
""" """
@ -50,17 +52,20 @@ 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')

View file

@ -1,10 +1,12 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import os import os
from django.conf import settings import shutil
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
@ -17,9 +19,10 @@ 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, 'lenna.png') path = os.path.join(settings.MEDIA_ROOT, 'reference.png')
return open(path, 'r+b') return open(path, 'r+b')
@ -61,7 +64,7 @@ def render_tag(ttag):
def get_html_attrs(ttag): def get_html_attrs(ttag):
return BeautifulSoup(render_tag(ttag)).img.attrs return BeautifulSoup(render_tag(ttag), features="html.parser").img.attrs
def assert_file_is_falsy(file): def assert_file_is_falsy(file):
@ -74,9 +77,30 @@ def assert_file_is_truthy(file):
class DummyAsyncCacheFileBackend(Simple): class DummyAsyncCacheFileBackend(Simple):
""" """
A cache file backend meant to simulate async generation (by marking the A cache file backend meant to simulate async generation.
file as pending but never actually creating it).
""" """
is_async = True
def generate(self, file, force=False): def generate(self, file, force=False):
self.set_state(file, CacheFileState.PENDING) pass
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
View file

@ -1,37 +1,18 @@
[tox] [tox]
envlist = envlist =
py27-django14, py27-django13, py27-django12, py38-django{master,30,22,21,20,111},
py26-django14, py26-django13, py26-django12 py37-django{master,30,22,21,20,111},
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 =
Django>=1.4,<1.5 djangomaster: git+https://github.com/django/django.git@master#egg=Django
django30: Django>=3.0,<3.1
[testenv:py27-django13] django22: Django>=2.2,<3.0
basepython = python2.7 django21: Django>=2.1,<2.2
deps = django20: Django>=2.0,<2.1
Django>=1.3,<1.4 django111: Django>=1.11,<2.0
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