From 446f1904a3e95dba01f33e88259e7e59a22aa4b3 Mon Sep 17 00:00:00 2001 From: Filip Todic Date: Wed, 12 Oct 2016 15:27:33 +0200 Subject: [PATCH 01/12] Construct the directory path using the MD5 hash algorithm Construct the directory path where the adjusted images will be saved to using the MD5 hash algorithm. Can be customized using the DAGUERRE_PATH setting set in the project's settings. If left unspecified, the default value will be used, i.e 'dg'. The maximum length of the specified string is 13 characters. Example: * Default: dg/ce/2b/7014c0bdbedea0e4f4bf.jpeg * Custom (DAGUERRE_PATH = 'img'): img/ce/2b/7014c0bdbedea0e4f4bf.jpeg Known issue: If the extracted hash string is 'ad', ad blockers will block the image. All occurrences of 'ad' will be replaced with 'ag' since the MD5 hash produces letters from 'a' to 'f'. --- daguerre/models.py | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/daguerre/models.py b/daguerre/models.py index f9ad0cd..df4fac8 100644 --- a/daguerre/models.py +++ b/daguerre/models.py @@ -1,5 +1,9 @@ +import hashlib import operator +import warnings +from datetime import datetime +from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator from django.db import models @@ -99,12 +103,48 @@ def delete_adjusted_images(sender, **kwargs): qs.delete() +def upload_to(instance, filename): + """ + Construct the directory path where the adjusted images will be saved to + using the MD5 hash algorithm. + + Can be customized using the DAGUERRE_PATH setting set in the project's + settings. If left unspecified, the default value will be used, i.e. 'dg'. + WARNING: The maximum length of the specified string is 13 characters. + + Example: + * Default: dg/ce/2b/7014c0bdbedea0e4f4bf.jpeg + * DAGUERRE_PATH = 'img': img/ce/2b/7014c0bdbedea0e4f4bf.jpeg + + Known issue: + * If the extracted hash string is 'ad', ad blockers will block the image. + All occurrences of 'ad' will be replaced with 'ag' since the MD5 hash + produces letters from 'a' to 'f'. + """ + + first_dir = None + if hasattr(settings, 'DAGUERRE_PATH'): + first_dir = settings.DAGUERRE_PATH + + if not first_dir or len(first_dir) > 13: + msg = ('The DAGUERRE_PATH value is more than 13 characters long!' + 'Falling back to the default value: "dg".') + warnings.warn(msg) + first_dir = 'dg' + + hash_for_dir = hashlib.md5('{} {}'.format( + filename, datetime.utcnow())).hexdigest().replace('ad', 'ag') + return '{0}/{1}/{2}/{3}'.format( + first_dir, hash_for_dir[0:2], hash_for_dir[2:4], filename) + + class AdjustedImage(models.Model): """Represents a managed image adjustment.""" storage_path = models.CharField(max_length=200) # The image name is a 20-character hash, so the max length with a 4-char - # extension (jpeg) is 45. - adjusted = models.ImageField(upload_to='daguerre/%Y/%m/%d/', + # extension (jpeg) is 45. The maximum length of the DAGUERRE_PATH string + # is 13. + adjusted = models.ImageField(upload_to=upload_to, max_length=45) requested = models.CharField(max_length=100) From da73f3929572c7e6c741a494af64500ad2280389 Mon Sep 17 00:00:00 2001 From: Filip Todic Date: Thu, 13 Oct 2016 11:54:49 +0200 Subject: [PATCH 02/12] Improve Unicode compatibility Improve the models' Unicode compatibility and convert the source string to bytes before hashing. Due to Unicode-string-bytes changes introduced in Python 3, the hash algorithm throws a TypeError when run on Python 3, complaining that Unicode objects must be encoded before hashing. A quick view at the Django's source code revealed that the accepted approach is to convert the string to bytes. References: * Source code for django.contrib.auth.hashers --- daguerre/models.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/daguerre/models.py b/daguerre/models.py index df4fac8..6846d53 100644 --- a/daguerre/models.py +++ b/daguerre/models.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + import hashlib import operator import warnings @@ -9,11 +13,13 @@ from django.db import models from django.db.models.signals import post_delete, post_save from django.dispatch import receiver +from django.utils.encoding import force_bytes, python_2_unicode_compatible from six.moves import reduce from daguerre.adjustments import registry +@python_2_unicode_compatible class Area(models.Model): """ Represents an area of an image. Can be used to specify a crop. Also used @@ -72,7 +78,7 @@ def serialize(self): return dict((f.name, getattr(self, f.name)) for f in self._meta.fields) - def __unicode__(self): + def __str__(self): if self.name: name = self.name else: @@ -127,17 +133,21 @@ def upload_to(instance, filename): first_dir = settings.DAGUERRE_PATH if not first_dir or len(first_dir) > 13: - msg = ('The DAGUERRE_PATH value is more than 13 characters long!' + msg = ('The DAGUERRE_PATH value is more than 13 characters long! ' 'Falling back to the default value: "dg".') warnings.warn(msg) first_dir = 'dg' - hash_for_dir = hashlib.md5('{} {}'.format( - filename, datetime.utcnow())).hexdigest().replace('ad', 'ag') + # Avoid TypeError on Py3 by forcing the string to bytestring + # https://docs.djangoproject.com/en/dev/_modules/django/contrib/auth/hashers/ + # https://github.com/django/django/blob/master/django/contrib/auth/hashers.py#L524 + str_for_hash = force_bytes('{} {}'.format(filename, datetime.utcnow())) + hash_for_dir = hashlib.md5(str_for_hash).hexdigest().replace('ad', 'ag') return '{0}/{1}/{2}/{3}'.format( first_dir, hash_for_dir[0:2], hash_for_dir[2:4], filename) +@python_2_unicode_compatible class AdjustedImage(models.Model): """Represents a managed image adjustment.""" storage_path = models.CharField(max_length=200) @@ -151,5 +161,5 @@ class AdjustedImage(models.Model): class Meta: index_together = [['requested', 'storage_path'], ] - def __unicode__(self): + def __str__(self): return u"{0}: {1}".format(self.storage_path, self.requested) From 35d2ac63be162f0109b2e0f7efec1d31fc94ed15 Mon Sep 17 00:00:00 2001 From: Filip Todic Date: Thu, 13 Oct 2016 13:41:16 +0200 Subject: [PATCH 03/12] Test the upload_to function of the AdjustedImage model --- daguerre/tests/unit/test_models.py | 42 +++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/daguerre/tests/unit/test_models.py b/daguerre/tests/unit/test_models.py index 19469ea..24da225 100644 --- a/daguerre/tests/unit/test_models.py +++ b/daguerre/tests/unit/test_models.py @@ -1,6 +1,8 @@ -from daguerre.models import AdjustedImage +from daguerre.models import AdjustedImage, upload_to from daguerre.tests.base import BaseTestCase +from django.test.utils import override_settings + class AreaTestCase(BaseTestCase): def test_delete_adjusted_images__save(self): @@ -50,3 +52,41 @@ def test_delete_adjusted_images__delete(self): AdjustedImage.objects.get, pk=adjusted2.pk) AdjustedImage.objects.get(pk=adjusted1.pk) + + +class AdjustesImageUploadToTestCase(BaseTestCase): + + def setUp(self): + self.instance = None + self.filename = '7014c0bdbedea0e4f4bf.jpeg' + + def test_upload_to__default_upload_dir(self): + hash_path = upload_to(instance=self.instance, filename=self.filename) + self.assertTrue(hash_path.startswith('dg/')) + self.assertTrue(hash_path.endswith('/{}'.format(self.filename))) + self.assertEqual(len(hash_path.split('/')), 4) + self.assertTrue(len(hash_path) < 45) + + @override_settings(DAGUERRE_PATH='img') + def test_upload_to__custom_upload_dir(self): + hash_path = upload_to(instance=self.instance, filename=self.filename) + self.assertTrue(hash_path.startswith('img/')) + self.assertTrue(hash_path.endswith('/{}'.format(self.filename))) + self.assertEqual(len(hash_path.split('/')), 4) + self.assertTrue(len(hash_path) < 45) + + @override_settings(DAGUERRE_PATH='0123456789123') + def test_upload_to__custom_upload_dir_small(self): + hash_path = upload_to(instance=self.instance, filename=self.filename) + self.assertTrue(hash_path.startswith('0123456789123/')) + self.assertTrue(hash_path.endswith('/{}'.format(self.filename))) + self.assertEqual(len(hash_path.split('/')), 4) + self.assertTrue(len(hash_path) == 45) + + @override_settings(DAGUERRE_PATH='01234567891234') + def test_upload_to__custom_upload_dir_big(self): + hash_path = upload_to(instance=self.instance, filename=self.filename) + self.assertTrue(hash_path.startswith('dg/')) + self.assertTrue(hash_path.endswith('/{}'.format(self.filename))) + self.assertEqual(len(hash_path.split('/')), 4) + self.assertTrue(len(hash_path) < 45) From c3e88d59492469f1eea89d9a1d63565439ac1611 Mon Sep 17 00:00:00 2001 From: Filip Todic Date: Thu, 13 Oct 2016 13:43:42 +0200 Subject: [PATCH 04/12] Apply the upload_to migration on the AdjustedImage model --- .../migrations/0004_hash_upload_to_dir.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 daguerre/migrations/0004_hash_upload_to_dir.py diff --git a/daguerre/migrations/0004_hash_upload_to_dir.py b/daguerre/migrations/0004_hash_upload_to_dir.py new file mode 100644 index 0000000..8867d99 --- /dev/null +++ b/daguerre/migrations/0004_hash_upload_to_dir.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import daguerre.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('daguerre', '0003_auto_20160301_2342'), + ] + + operations = [ + migrations.AlterField( + model_name='adjustedimage', + name='adjusted', + field=models.ImageField(max_length=45, upload_to=daguerre.models.upload_to), + ), + ] From a88621d9bbb8ef0b680d187913820181eefded65 Mon Sep 17 00:00:00 2001 From: Filip Todic Date: Fri, 14 Oct 2016 10:41:48 +0200 Subject: [PATCH 05/12] Add the default image path constant Changes: * Define the DAGUERRE_DEFAULT_IMAGE_PATH as a constant at the beginning of the file * Update the variable names: * Setting: DAGUERRE_PATH -> DAGUERRE_ADJUSTED_IMAGE_PATH * local variable: first_dir -> image_path * Update the set method of the local variable --- daguerre/models.py | 19 ++++++++++++------- daguerre/tests/unit/test_models.py | 6 +++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/daguerre/models.py b/daguerre/models.py index 6846d53..8dab407 100644 --- a/daguerre/models.py +++ b/daguerre/models.py @@ -18,6 +18,11 @@ from daguerre.adjustments import registry +# The default image path where the images will be saved to. Can be overriden by +# defining the DAGUERRE_ADJUSTED_IMAGE_PATH setting in the project's settings. +# Example: DAGUERRE_ADJUSTED_IMAGE_PATH = 'img' +DAGUERRE_DEFAULT_IMAGE_PATH = 'dg' + @python_2_unicode_compatible class Area(models.Model): @@ -128,15 +133,15 @@ def upload_to(instance, filename): produces letters from 'a' to 'f'. """ - first_dir = None - if hasattr(settings, 'DAGUERRE_PATH'): - first_dir = settings.DAGUERRE_PATH + image_path = getattr( + settings, 'DAGUERRE_ADJUSTED_IMAGE_PATH', DAGUERRE_DEFAULT_IMAGE_PATH) - if not first_dir or len(first_dir) > 13: + if len(image_path) > 13: msg = ('The DAGUERRE_PATH value is more than 13 characters long! ' - 'Falling back to the default value: "dg".') + 'Falling back to the default value: "{}".'.format( + DAGUERRE_DEFAULT_IMAGE_PATH)) warnings.warn(msg) - first_dir = 'dg' + image_path = DAGUERRE_DEFAULT_IMAGE_PATH # Avoid TypeError on Py3 by forcing the string to bytestring # https://docs.djangoproject.com/en/dev/_modules/django/contrib/auth/hashers/ @@ -144,7 +149,7 @@ def upload_to(instance, filename): str_for_hash = force_bytes('{} {}'.format(filename, datetime.utcnow())) hash_for_dir = hashlib.md5(str_for_hash).hexdigest().replace('ad', 'ag') return '{0}/{1}/{2}/{3}'.format( - first_dir, hash_for_dir[0:2], hash_for_dir[2:4], filename) + image_path, hash_for_dir[0:2], hash_for_dir[2:4], filename) @python_2_unicode_compatible diff --git a/daguerre/tests/unit/test_models.py b/daguerre/tests/unit/test_models.py index 24da225..45c8ae2 100644 --- a/daguerre/tests/unit/test_models.py +++ b/daguerre/tests/unit/test_models.py @@ -67,7 +67,7 @@ def test_upload_to__default_upload_dir(self): self.assertEqual(len(hash_path.split('/')), 4) self.assertTrue(len(hash_path) < 45) - @override_settings(DAGUERRE_PATH='img') + @override_settings(DAGUERRE_ADJUSTED_IMAGE_PATH='img') def test_upload_to__custom_upload_dir(self): hash_path = upload_to(instance=self.instance, filename=self.filename) self.assertTrue(hash_path.startswith('img/')) @@ -75,7 +75,7 @@ def test_upload_to__custom_upload_dir(self): self.assertEqual(len(hash_path.split('/')), 4) self.assertTrue(len(hash_path) < 45) - @override_settings(DAGUERRE_PATH='0123456789123') + @override_settings(DAGUERRE_ADJUSTED_IMAGE_PATH='0123456789123') def test_upload_to__custom_upload_dir_small(self): hash_path = upload_to(instance=self.instance, filename=self.filename) self.assertTrue(hash_path.startswith('0123456789123/')) @@ -83,7 +83,7 @@ def test_upload_to__custom_upload_dir_small(self): self.assertEqual(len(hash_path.split('/')), 4) self.assertTrue(len(hash_path) == 45) - @override_settings(DAGUERRE_PATH='01234567891234') + @override_settings(DAGUERRE_ADJUSTED_IMAGE_PATH='01234567891234') def test_upload_to__custom_upload_dir_big(self): hash_path = upload_to(instance=self.instance, filename=self.filename) self.assertTrue(hash_path.startswith('dg/')) From 512963edfe7333502a1304387c23c9365511e137 Mon Sep 17 00:00:00 2001 From: Filip Todic Date: Fri, 14 Oct 2016 10:47:48 +0200 Subject: [PATCH 06/12] Add a comment explaining the replacements in the hash string --- daguerre/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daguerre/models.py b/daguerre/models.py index 8dab407..9bc76fe 100644 --- a/daguerre/models.py +++ b/daguerre/models.py @@ -147,6 +147,7 @@ def upload_to(instance, filename): # https://docs.djangoproject.com/en/dev/_modules/django/contrib/auth/hashers/ # https://github.com/django/django/blob/master/django/contrib/auth/hashers.py#L524 str_for_hash = force_bytes('{} {}'.format(filename, datetime.utcnow())) + # Replace all occurrences of 'ad' with 'ag' to avoid ad blockers hash_for_dir = hashlib.md5(str_for_hash).hexdigest().replace('ad', 'ag') return '{0}/{1}/{2}/{3}'.format( image_path, hash_for_dir[0:2], hash_for_dir[2:4], filename) From b5e94eb055516094cf31b59f0b369a8fe8d0d01a Mon Sep 17 00:00:00 2001 From: Filip Todic Date: Fri, 14 Oct 2016 14:24:55 +0200 Subject: [PATCH 07/12] Detect warnings in upload_to tests The tests have been modified according to the official docs: * https://docs.python.org/2/library/warnings.html#testing-warnings --- daguerre/tests/unit/test_models.py | 64 ++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/daguerre/tests/unit/test_models.py b/daguerre/tests/unit/test_models.py index 45c8ae2..e5f68ce 100644 --- a/daguerre/tests/unit/test_models.py +++ b/daguerre/tests/unit/test_models.py @@ -1,3 +1,5 @@ +import warnings + from daguerre.models import AdjustedImage, upload_to from daguerre.tests.base import BaseTestCase @@ -61,32 +63,54 @@ def setUp(self): self.filename = '7014c0bdbedea0e4f4bf.jpeg' def test_upload_to__default_upload_dir(self): - hash_path = upload_to(instance=self.instance, filename=self.filename) - self.assertTrue(hash_path.startswith('dg/')) - self.assertTrue(hash_path.endswith('/{}'.format(self.filename))) - self.assertEqual(len(hash_path.split('/')), 4) - self.assertTrue(len(hash_path) < 45) + with warnings.catch_warnings(record=True) as w: + hash_path = upload_to( + instance=self.instance, filename=self.filename) + self.assertTrue(hash_path.startswith('dg/')) + self.assertTrue(hash_path.endswith('/{}'.format(self.filename))) + self.assertEqual(len(hash_path.split('/')), 4) + self.assertTrue(len(hash_path) < 45) + self.assertEqual(w, []) @override_settings(DAGUERRE_ADJUSTED_IMAGE_PATH='img') def test_upload_to__custom_upload_dir(self): - hash_path = upload_to(instance=self.instance, filename=self.filename) - self.assertTrue(hash_path.startswith('img/')) - self.assertTrue(hash_path.endswith('/{}'.format(self.filename))) - self.assertEqual(len(hash_path.split('/')), 4) - self.assertTrue(len(hash_path) < 45) + with warnings.catch_warnings(record=True) as w: + hash_path = upload_to( + instance=self.instance, filename=self.filename) + self.assertTrue(hash_path.startswith('img/')) + self.assertTrue(hash_path.endswith('/{}'.format(self.filename))) + self.assertEqual(len(hash_path.split('/')), 4) + self.assertTrue(len(hash_path) < 45) + self.assertEqual(w, []) @override_settings(DAGUERRE_ADJUSTED_IMAGE_PATH='0123456789123') def test_upload_to__custom_upload_dir_small(self): - hash_path = upload_to(instance=self.instance, filename=self.filename) - self.assertTrue(hash_path.startswith('0123456789123/')) - self.assertTrue(hash_path.endswith('/{}'.format(self.filename))) - self.assertEqual(len(hash_path.split('/')), 4) - self.assertTrue(len(hash_path) == 45) + with warnings.catch_warnings(record=True) as w: + hash_path = upload_to( + instance=self.instance, filename=self.filename) + self.assertTrue(hash_path.startswith('0123456789123/')) + self.assertTrue(hash_path.endswith('/{}'.format(self.filename))) + self.assertEqual(len(hash_path.split('/')), 4) + self.assertTrue(len(hash_path) == 45) + self.assertEqual(w, []) @override_settings(DAGUERRE_ADJUSTED_IMAGE_PATH='01234567891234') def test_upload_to__custom_upload_dir_big(self): - hash_path = upload_to(instance=self.instance, filename=self.filename) - self.assertTrue(hash_path.startswith('dg/')) - self.assertTrue(hash_path.endswith('/{}'.format(self.filename))) - self.assertEqual(len(hash_path.split('/')), 4) - self.assertTrue(len(hash_path) < 45) + with warnings.catch_warnings(record=True) as w: + hash_path = upload_to( + instance=self.instance, filename=self.filename) + self.assertTrue(hash_path.startswith('dg/')) + self.assertTrue(hash_path.endswith('/{}'.format(self.filename))) + self.assertEqual(len(hash_path.split('/')), 4) + self.assertTrue(len(hash_path) < 45) + + # Test the warning message + # https://docs.python.org/2/library/warnings.html#testing-warnings + warning_message = ('The DAGUERRE_PATH value is more than 13 ' + 'characters long! Falling back to the default ' + 'value: "dg".') + + self.assertEqual(len(w), 1) + user_warning = w[0] + self.assertEqual(user_warning.category, UserWarning) + self.assertEqual(user_warning.message.__str__(), warning_message) From 347f9049a3e840fc7de7a22813bb88f4dfba42e2 Mon Sep 17 00:00:00 2001 From: Filip Todic Date: Fri, 14 Oct 2016 14:53:47 +0200 Subject: [PATCH 08/12] Add the custom settings section to the documentation --- daguerre/models.py | 4 ++-- docs/guides/settings.rst | 21 +++++++++++++++++++++ docs/index.rst | 3 ++- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 docs/guides/settings.rst diff --git a/daguerre/models.py b/daguerre/models.py index 9bc76fe..361a896 100644 --- a/daguerre/models.py +++ b/daguerre/models.py @@ -158,8 +158,8 @@ class AdjustedImage(models.Model): """Represents a managed image adjustment.""" storage_path = models.CharField(max_length=200) # The image name is a 20-character hash, so the max length with a 4-char - # extension (jpeg) is 45. The maximum length of the DAGUERRE_PATH string - # is 13. + # extension (jpeg) is 45. The maximum length of the + # DAGUERRE_ADJUSTED_IMAGE_PATH string is 13. adjusted = models.ImageField(upload_to=upload_to, max_length=45) requested = models.CharField(max_length=100) diff --git a/docs/guides/settings.rst b/docs/guides/settings.rst new file mode 100644 index 0000000..0ad6b61 --- /dev/null +++ b/docs/guides/settings.rst @@ -0,0 +1,21 @@ +Custom settings +=============== + +Adjust the image path ++++++++++++++++++++++ + +By default, the variations are stored under a hashed directory path that by +default starts with ``dg/``. This setting can be modified in the project's +settings by setting the ``DAGUERRE_ADJUSTED_IMAGE_PATH`` variable. + +Example: + +.. code-block:: django + + # settings.py + DAGUERRE_ADJUSTED_IMAGE_PATH = 'img' + + +**WARNING:** The maximum length of the ``DAGUERRE_ADJUSTED_IMAGE_PATH`` string +is 13 characters. If the string has more than 13 characters, it will gracefully +fall back to the the default value, i.e. ``dg/`` diff --git a/docs/index.rst b/docs/index.rst index 5f9d715..0e08a5c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ Django Daguerre :align: right :scale: 33 % :target: http://en.wikipedia.org/wiki/Louis_Daguerre - + Louis Daguerre, Father of Photography **Django Daguerre** manipulates images on the fly. Use it to scale @@ -45,6 +45,7 @@ Contents guides/template-tags guides/areas guides/commands + guides/settings API Docs From f92e3ce4d3eaa1dfe84aebccc92a1c3cd4b05095 Mon Sep 17 00:00:00 2001 From: Filip Todic Date: Fri, 14 Oct 2016 15:09:32 +0200 Subject: [PATCH 09/12] Fix indentation --- daguerre/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daguerre/models.py b/daguerre/models.py index 361a896..6757775 100644 --- a/daguerre/models.py +++ b/daguerre/models.py @@ -138,8 +138,8 @@ def upload_to(instance, filename): if len(image_path) > 13: msg = ('The DAGUERRE_PATH value is more than 13 characters long! ' - 'Falling back to the default value: "{}".'.format( - DAGUERRE_DEFAULT_IMAGE_PATH)) + 'Falling back to the default ' + 'value: "{}".'.format(DAGUERRE_DEFAULT_IMAGE_PATH)) warnings.warn(msg) image_path = DAGUERRE_DEFAULT_IMAGE_PATH From 5fe9797a310d48592ea9bbab83dd1ce2071c3922 Mon Sep 17 00:00:00 2001 From: Filip Todic Date: Mon, 17 Oct 2016 08:57:11 +0200 Subject: [PATCH 10/12] Minor changes to the docs and naming convention Changes: * Change the default variable from DAGUERRE_DEFAULT_IMAGE_PATH to DEFAULT_ADJUSTED_IMAGE_PATH * Update settings documentation --- daguerre/models.py | 8 ++++---- docs/guides/settings.rst | 16 ++++++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/daguerre/models.py b/daguerre/models.py index 6757775..7402411 100644 --- a/daguerre/models.py +++ b/daguerre/models.py @@ -21,7 +21,7 @@ # The default image path where the images will be saved to. Can be overriden by # defining the DAGUERRE_ADJUSTED_IMAGE_PATH setting in the project's settings. # Example: DAGUERRE_ADJUSTED_IMAGE_PATH = 'img' -DAGUERRE_DEFAULT_IMAGE_PATH = 'dg' +DEFAULT_ADJUSTED_IMAGE_PATH = 'dg' @python_2_unicode_compatible @@ -134,14 +134,14 @@ def upload_to(instance, filename): """ image_path = getattr( - settings, 'DAGUERRE_ADJUSTED_IMAGE_PATH', DAGUERRE_DEFAULT_IMAGE_PATH) + settings, 'DAGUERRE_ADJUSTED_IMAGE_PATH', DEFAULT_ADJUSTED_IMAGE_PATH) if len(image_path) > 13: msg = ('The DAGUERRE_PATH value is more than 13 characters long! ' 'Falling back to the default ' - 'value: "{}".'.format(DAGUERRE_DEFAULT_IMAGE_PATH)) + 'value: "{}".'.format(DEFAULT_ADJUSTED_IMAGE_PATH)) warnings.warn(msg) - image_path = DAGUERRE_DEFAULT_IMAGE_PATH + image_path = DEFAULT_ADJUSTED_IMAGE_PATH # Avoid TypeError on Py3 by forcing the string to bytestring # https://docs.djangoproject.com/en/dev/_modules/django/contrib/auth/hashers/ diff --git a/docs/guides/settings.rst b/docs/guides/settings.rst index 0ad6b61..a4b9808 100644 --- a/docs/guides/settings.rst +++ b/docs/guides/settings.rst @@ -4,9 +4,10 @@ Custom settings Adjust the image path +++++++++++++++++++++ -By default, the variations are stored under a hashed directory path that by -default starts with ``dg/``. This setting can be modified in the project's -settings by setting the ``DAGUERRE_ADJUSTED_IMAGE_PATH`` variable. +The variations are stored under a hashed directory path that starts with the +``dg`` directory by default (e.g. ``dg/ce/2b/7014c0bdbedea0e4f4bf.jpeg``). +This setting can be modified in the project's settings by setting the +``DAGUERRE_ADJUSTED_IMAGE_PATH`` variable. Example: @@ -15,7 +16,10 @@ Example: # settings.py DAGUERRE_ADJUSTED_IMAGE_PATH = 'img' +which would produce the following path: ``img/ce/2b/7014c0bdbedea0e4f4bf.jpeg`` -**WARNING:** The maximum length of the ``DAGUERRE_ADJUSTED_IMAGE_PATH`` string -is 13 characters. If the string has more than 13 characters, it will gracefully -fall back to the the default value, i.e. ``dg/`` + +.. WARNING:: + The maximum length of the ``DAGUERRE_ADJUSTED_IMAGE_PATH`` string + is 13 characters. If the string has more than 13 characters, it will + gracefully fall back to the the default value, i.e. ``dg`` From 800b3394f9c45c7cc77758dc949da589e1ff9bc0 Mon Sep 17 00:00:00 2001 From: Filip Todic Date: Tue, 18 Oct 2016 09:31:11 +0200 Subject: [PATCH 11/12] Fix typo in the name of the test class --- daguerre/tests/unit/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daguerre/tests/unit/test_models.py b/daguerre/tests/unit/test_models.py index e5f68ce..87b4c7a 100644 --- a/daguerre/tests/unit/test_models.py +++ b/daguerre/tests/unit/test_models.py @@ -56,7 +56,7 @@ def test_delete_adjusted_images__delete(self): AdjustedImage.objects.get(pk=adjusted1.pk) -class AdjustesImageUploadToTestCase(BaseTestCase): +class AdjustedImageUploadToTestCase(BaseTestCase): def setUp(self): self.instance = None From cb5eb1e00bb2fa7039e1d6550b31e71e1b3fe587 Mon Sep 17 00:00:00 2001 From: Filip Todic Date: Wed, 19 Oct 2016 09:21:23 +0200 Subject: [PATCH 12/12] Update management command and its tests Update the **daguerre clean** management command. Specifically, the function that traverses the filesystem in search for orphaned files. Now the management command takes the possible modifications to the image path into consideration. --- .../management/commands/_daguerre_clean.py | 9 ++++-- daguerre/tests/unit/test_management.py | 30 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/daguerre/management/commands/_daguerre_clean.py b/daguerre/management/commands/_daguerre_clean.py index 6aa9235..bb6cdd0 100644 --- a/daguerre/management/commands/_daguerre_clean.py +++ b/daguerre/management/commands/_daguerre_clean.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import os +from django.conf import settings from django.core.files.storage import default_storage from django.core.management.base import BaseCommand from django.db import models @@ -8,7 +9,7 @@ import six from daguerre.helpers import IOERRORS -from daguerre.models import AdjustedImage, Area +from daguerre.models import AdjustedImage, Area, DEFAULT_ADJUSTED_IMAGE_PATH class Command(BaseCommand): @@ -127,12 +128,16 @@ def _orphaned_files(self): in the database. """ + base_dir = getattr( + settings, 'DAGUERRE_ADJUSTED_IMAGE_PATH', + DEFAULT_ADJUSTED_IMAGE_PATH) + known_paths = set( AdjustedImage.objects.values_list('adjusted', flat=True).distinct() ) orphans = [] for dirpath, dirnames, filenames in self._walk( - 'daguerre', topdown=False): + base_dir, topdown=False): for filename in filenames: filepath = os.path.join(dirpath, filename) if filepath not in known_paths: diff --git a/daguerre/tests/unit/test_management.py b/daguerre/tests/unit/test_management.py index 3d8be63..edd3070 100644 --- a/daguerre/tests/unit/test_management.py +++ b/daguerre/tests/unit/test_management.py @@ -101,20 +101,36 @@ def test_duplicate_adjustments(self): self.assertTrue(list(duplicates) == [adjusted1] or list(duplicates) == [adjusted2]) - def test_orphaned_files(self): + def test_orphaned_files__default_path(self): clean = Clean() walk_ret = ( - ('daguerre', ['test'], []), - ('daguerre/test', [], ['fake1.png', 'fake2.png', 'fake3.png']) + ('dg', ['test'], []), + ('dg/test', [], ['fake1.png', 'fake2.png', 'fake3.png']) ) AdjustedImage.objects.create(requested='fit|50|50', storage_path='whatever.png', - adjusted='daguerre/test/fake2.png') + adjusted='dg/test/fake2.png') with mock.patch.object(clean, '_walk', return_value=walk_ret) as walk: self.assertEqual(clean._orphaned_files(), - ['daguerre/test/fake1.png', - 'daguerre/test/fake3.png']) - walk.assert_called_once_with('daguerre', topdown=False) + ['dg/test/fake1.png', + 'dg/test/fake3.png']) + walk.assert_called_once_with('dg', topdown=False) + + @override_settings(DAGUERRE_ADJUSTED_IMAGE_PATH='img') + def test_orphaned_files__modified_path(self): + clean = Clean() + walk_ret = ( + ('img', ['test'], []), + ('img/test', [], ['fake1.png', 'fake2.png', 'fake3.png']) + ) + AdjustedImage.objects.create(requested='fit|50|50', + storage_path='whatever.png', + adjusted='img/test/fake2.png') + with mock.patch.object(clean, '_walk', return_value=walk_ret) as walk: + self.assertEqual(clean._orphaned_files(), + ['img/test/fake1.png', + 'img/test/fake3.png']) + walk.assert_called_once_with('img', topdown=False) class PreadjustTestCase(BaseTestCase):