diff --git a/filebrowser/namers.py b/filebrowser/namers.py index 97b20b8fe..672504751 100644 --- a/filebrowser/namers.py +++ b/filebrowser/namers.py @@ -1,3 +1,4 @@ +import re from django.utils import six from django.utils.module_loading import import_string @@ -33,20 +34,33 @@ class OptionsNamer(VersionNamer): def get_version_name(self): name = "{root}_{options}{extension}".format( root=self.file_object.filename_root, - options='--'.join(self.prepared_options), + options=self.options_as_string, extension=self.file_object.extension, ) return name def get_original_name(self): - "Removes the substring containing the last _ (underscore) in the filename" + """ + Restores the original file name wipping out the last + `_version_suffix--plus-any-configs` block entirely. + """ root = self.file_object.filename_root tmp = root.split("_") options_part = tmp[len(tmp) - 1] return u"%s%s" % (root.replace("_%s" % options_part, ""), self.file_object.extension) @property - def prepared_options(self): + def options_as_string(self): + """ + The options part should not contain `_` (underscore) on order to get + original name back. + """ + name = '--'.join(self.options_list).replace(',', 'x') + name = re.sub(r'[_\s]', '-', name) + return re.sub(r'[^\w-]', '', name).strip() + + @property + def options_list(self): opts = [] if not self.options: return opts @@ -56,8 +70,10 @@ def prepared_options(self): if 'size' in self.options: opts.append('%sx%s' % tuple(self.options['size'])) - elif 'width' in self.options: - opts.append('%sx%s' % (self.options['width'], self.options['width'],)) + elif 'width' in self.options or 'height' in self.options: + width = float(self.options.get('width') or 0) + height = float(self.options.get('height') or 0) + opts.append('%dx%d' % (width, height)) for k, v in sorted(self.options.items()): if not v or k in ('size', 'width', 'height', @@ -71,9 +87,6 @@ def prepared_options(self): v = 'x'.join([six.text_type(v) for item in v]) except TypeError: v = six.text_type(v) - opts.append('%s-%s' % (k, self.sanitize_value(v))) + opts.append('%s-%s' % (k, v)) return opts - - def sanitize_value(self, value): - return value.replace(',', 'x') diff --git a/filebrowser/utils.py b/filebrowser/utils.py index 01a9c067e..c1ffeb68c 100644 --- a/filebrowser/utils.py +++ b/filebrowser/utils.py @@ -73,7 +73,7 @@ def process_image(source, processor_options, processors=None): return image -def scale_and_crop(im, width, height, opts='', **kwargs): +def scale_and_crop(im, width=None, height=None, opts='', **kwargs): """ Scale and Crop. """ @@ -81,9 +81,12 @@ def scale_and_crop(im, width, height, opts='', **kwargs): width = float(width or 0) height = float(height or 0) + if (x, y) == (width, height): + return im + if 'upscale' not in opts: if (x < width or not width) and (y < height or not height): - return False + return im if width: xr = float(width) diff --git a/tests/test_base.py b/tests/test_base.py index 911d63bb9..417d61080 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -419,3 +419,83 @@ def test_walk(self): self.assertEqual(list(f.path for f in self.F_LISTING_FOLDER.files_walk_filtered()), [u'_test/uploads/testimage.jpg', u'_test/uploads/folder', u'_test/uploads/folder/subfolder', u'_test/uploads/folder/subfolder/testimage.jpg']) self.assertEqual(self.F_LISTING_FOLDER.results_walk_total(), 4) self.assertEqual(self.F_LISTING_FOLDER.results_walk_filtered(), 4) + + +class FileObjecNamerTests(TestCase): + + PATCH_VERSIONS = { + 'thumbnail': {'verbose_name': 'Thumbnail (1 col)', 'width': 60, 'height': 60, 'opts': 'crop'}, + 'small': {'verbose_name': 'Small (2 col)', 'width': 140, 'height': '', 'opts': ''}, + 'large': {'verbose_name': 'Large (8 col)', 'width': 680, 'height': '', 'opts': ''}, + } + PATCH_ADMIN_VERSIONS = ['large'] + + def setUp(self): + super(FileObjecNamerTests, self).setUp() + shutil.copy(self.STATIC_IMG_PATH, self.FOLDER_PATH) + + @patch('filebrowser.namers.VERSION_NAMER', 'filebrowser.namers.OptionsNamer') + def test_init_attributes(self): + """ + FileObject init attributes + + # path + # head + # filename + # filename_lower + # filename_root + # extension + # mimetype + """ + self.assertEqual(self.F_IMAGE.path, "_test/uploads/folder/testimage.jpg") + self.assertEqual(self.F_IMAGE.head, '_test/uploads/folder') + self.assertEqual(self.F_IMAGE.filename, 'testimage.jpg') + self.assertEqual(self.F_IMAGE.filename_lower, 'testimage.jpg') + self.assertEqual(self.F_IMAGE.filename_root, 'testimage') + self.assertEqual(self.F_IMAGE.extension, '.jpg') + self.assertEqual(self.F_IMAGE.mimetype, ('image/jpeg', None)) + + @patch('filebrowser.namers.VERSION_NAMER', 'filebrowser.namers.OptionsNamer') + @patch('filebrowser.base.VERSIONS', PATCH_VERSIONS) + @patch('filebrowser.base.ADMIN_VERSIONS', PATCH_ADMIN_VERSIONS) + def test_version_attributes_with_options_namer(self): + """ + FileObject version attributes/methods + without versions_basedir + + # is_version + # original + # original_filename + # versions_basedir + # versions + # admin_versions + # version_name(suffix) + # version_path(suffix) + # version_generate(suffix) + """ + # new settings + version_list = sorted([ + '_test/_versions/folder/testimage_large--680x0.jpg', + '_test/_versions/folder/testimage_small--140x0.jpg', + '_test/_versions/folder/testimage_thumbnail--60x60--opts-crop.jpg' + ]) + admin_version_list = ['_test/_versions/folder/testimage_large--680x0.jpg'] + + self.assertEqual(self.F_IMAGE.is_version, False) + self.assertEqual(self.F_IMAGE.original.path, self.F_IMAGE.path) + self.assertEqual(self.F_IMAGE.versions_basedir, "_test/_versions/") + self.assertEqual(self.F_IMAGE.versions(), version_list) + self.assertEqual(self.F_IMAGE.admin_versions(), admin_version_list) + self.assertEqual(self.F_IMAGE.version_name("large"), "testimage_large--680x0.jpg") + self.assertEqual(self.F_IMAGE.version_path("large"), "_test/_versions/folder/testimage_large--680x0.jpg") + + # version does not exist yet + f_version = FileObject(os.path.join(site.directory, 'folder', "testimage_large--680x0.jpg"), site=site) + self.assertEqual(f_version.exists, False) + # generate version + f_version = self.F_IMAGE.version_generate("large") + self.assertEqual(f_version.path, "_test/_versions/folder/testimage_large--680x0.jpg") + self.assertEqual(f_version.exists, True) + self.assertEqual(f_version.is_version, True) + self.assertEqual(f_version.original_filename, "testimage.jpg") + self.assertEqual(f_version.original.path, self.F_IMAGE.path) diff --git a/tests/test_namers.py b/tests/test_namers.py new file mode 100644 index 000000000..79cc4d1e2 --- /dev/null +++ b/tests/test_namers.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +import shutil + +from mock import patch + +from filebrowser.settings import VERSIONS +from tests import FilebrowserTestCase as TestCase + +from filebrowser.namers import OptionsNamer + + +class BaseNamerTests(TestCase): + NAMER_CLASS = OptionsNamer + + def setUp(self): + super(BaseNamerTests, self).setUp() + shutil.copy(self.STATIC_IMG_PATH, self.FOLDER_PATH) + + def _get_namer(self, version_suffix, file_object=None, **extra_options): + if not file_object: + file_object = self.F_IMAGE + + extra_options.update(VERSIONS.get(version_suffix, {})) + + return self.NAMER_CLASS( + file_object=file_object, + version_suffix=version_suffix, + filename_root=file_object.filename_root, + extension=file_object.extension, + options=extra_options, + ) + + +class OptionsNamerTests(BaseNamerTests): + + def test_should_return_correct_version_name_using_predefined_versions(self): + expected = [ + ("admin_thumbnail", "testimage_admin-thumbnail--60x60--opts-crop.jpg", ), + ("thumbnail", "testimage_thumbnail--60x60--opts-crop.jpg", ), + ("small", "testimage_small--140x0.jpg", ), + ("medium", "testimage_medium--300x0.jpg", ), + ("big", "testimage_big--460x0.jpg", ), + ("large", "testimage_large--680x0.jpg", ), + ] + + for version_suffix, expected_name in expected: + namer = self._get_namer(version_suffix) + self.assertEqual(namer.get_version_name(), expected_name) + + @patch('filebrowser.namers.VERSION_NAMER', 'filebrowser.namers.OptionsNamer') + def test_should_return_correct_original_name_using_predefined_versions(self): + expected = 'testimage.jpg' + for version_suffix in VERSIONS.keys(): + file_object = self.F_IMAGE.version_generate(version_suffix) + namer = self._get_namer(version_suffix, file_object) + self.assertNotEqual(file_object.filename, expected) + + self.assertEqual(namer.get_original_name(), expected) + self.assertEqual(file_object.original_filename, expected) + + def test_should_append_extra_options(self): + expected = [ + ( + "thumbnail", + "testimage_thumbnail--60x60--opts-crop--sepia.jpg", + {'sepia': True} + ), ( + "small", + "testimage_small--140x0--thumb--transparency-08.jpg", + {'transparency': 0.8, 'thumb': True} + ), ( + "large", + "testimage_large--680x0--nested-xpto-ops--thumb.jpg", + {'thumb': True, 'nested': {'xpto': 'ops'}} + ), + ] + + for version_suffix, expected_name, extra_options in expected: + namer = self._get_namer(version_suffix, **extra_options) + self.assertEqual(namer.get_version_name(), expected_name) + + def test_generated_version_name_options_list_should_be_ordered(self): + "the order is important to always generate the same name" + expected = [ + ( + "small", + "testimage_small--140x0--a--x-crop--z-4.jpg", + {'z': 4, 'a': True, 'x': 'crop'} + ), + ] + + for version_suffix, expected_name, extra_options in expected: + namer = self._get_namer(version_suffix, **extra_options) + self.assertEqual(namer.get_version_name(), expected_name) diff --git a/tests/test_versions.py b/tests/test_versions.py index 1d3ba32f0..6063d9f36 100644 --- a/tests/test_versions.py +++ b/tests/test_versions.py @@ -8,7 +8,8 @@ from tests import FilebrowserTestCase as TestCase from filebrowser.settings import STRICT_PIL -from filebrowser.utils import scale_and_crop +from filebrowser import utils +from filebrowser.utils import scale_and_crop, process_image if STRICT_PIL: from PIL import Image @@ -19,6 +20,50 @@ import Image +def processor_mark_1(im, **kwargs): + im.mark_1 = True + return im + + +def processor_mark_2(im, **kwargs): + im.mark_2 = True + return im + + +class ImageProcessorsTests(TestCase): + def setUp(self): + super(ImageProcessorsTests, self).setUp() + shutil.copy(self.STATIC_IMG_PATH, self.FOLDER_PATH) + + utils._default_processors = None + self.im = Image.open(self.F_IMAGE.path_full) + + def tearDown(self): + super(ImageProcessorsTests, self).tearDown() + utils._default_processors = None + + def test_process_image_calls_scale_and_crop(self): + version = process_image(self.im, {'width': 500, 'height': ""}) + self.assertEqual(version.size, (500, 375)) + + @patch('filebrowser.utils.PROCESSORS', [ + 'tests.test_versions.processor_mark_1', + 'tests.test_versions.processor_mark_2', + ]) + def test_process_image_calls_the_stack_of_processors_in_settings(self): + version = process_image(self.im, {}) + self.assertTrue(hasattr(version, 'mark_1')) + self.assertTrue(hasattr(version, 'mark_2')) + + @patch('filebrowser.utils.PROCESSORS', [ + 'tests.test_versions.processor_mark_1', + ]) + def test_process_image_calls_only_explicit_provided_processors(self): + version = process_image(self.im, {}, processors=[processor_mark_2]) + self.assertFalse(hasattr(version, 'mark_1')) + self.assertTrue(hasattr(version, 'mark_2')) + + class ScaleAndCropTests(TestCase): def setUp(self): super(ScaleAndCropTests, self).setUp() @@ -26,6 +71,16 @@ def setUp(self): self.im = Image.open(self.F_IMAGE.path_full) + def test_do_not_scale(self): + version = scale_and_crop(self.im, "", "", "") + self.assertEqual(version.size, self.im.size) + + def test_do_not_scale_if_desired_size_is_equal_to_original(self): + width, height = self.im.size + version = scale_and_crop(self.im, width, height, "") + self.assertIs(version, self.im) + self.assertEqual(version.size, (width, height)) + def test_scale_width(self): version = scale_and_crop(self.im, 500, "", "") self.assertEqual(version.size, (500, 375)) @@ -37,15 +92,15 @@ def test_scale_height(self): def test_scale_no_upscale_too_wide(self): version = scale_and_crop(self.im, 1500, "", "") - self.assertEqual(version, False) + self.assertIs(version, self.im) def test_scale_no_upscale_too_tall(self): version = scale_and_crop(self.im, "", 1125, "") - self.assertEqual(version, False) + self.assertIs(version, self.im) def test_scale_no_upscale_too_wide_and_tall(self): version = scale_and_crop(self.im, 1500, 1125, "") - self.assertEqual(version, False) + self.assertIs(version, self.im) def test_scale_with_upscale_width(self): version = scale_and_crop(self.im, 1500, "", "upscale") @@ -82,7 +137,7 @@ def test_crop_width_and_height(self): def test_crop_width_and_height_too_large_no_upscale(self): # new width 1500 and height 1500 w. crop > false (upscale missing) version = scale_and_crop(self.im, 1500, 1500, "crop") - self.assertEqual(version, False) + self.assertIs(version, self.im) def test_crop_width_and_height_too_large_with_upscale(self): version = scale_and_crop(self.im, 1500, 1500, "crop,upscale")