From 693810361a445eb8d92cc9a6273fb9c1acaf18d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Andersson?= Date: Thu, 10 Apr 2014 16:26:00 +0800 Subject: [PATCH] Add random_filename factory A factory that can generate random filenames when used together with `FileField`'s `upload_to` argument. --- AUTHORS.rst | 1 + CHANGES.rst | 3 ++- docs/models.rst | 34 +++++++++++++++++++++++++++ model_utils/models.py | 30 ++++++++++++++++++++++++ model_utils/tests/tests.py | 48 +++++++++++++++++++++++++++++++++++++- 5 files changed, 114 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 5ee80df2..e3ede05b 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,6 +1,7 @@ Alejandro Varas Alex Orange Andy Freeland +Björn Andersson Carl Meyer Curtis Maloney Den Lesnov diff --git a/CHANGES.rst b/CHANGES.rst index de21f3e6..d9f468df 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ CHANGES master (unreleased) ------------------- +* Add ``random_function`` to models, a factory used with ``FileField``. + 2.0.3 (2014.03.19) ------------------- @@ -240,4 +242,3 @@ master (unreleased) ----- * Added ``QueryManager`` - diff --git a/docs/models.rst b/docs/models.rst index 7a05c79d..48df759f 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -47,3 +47,37 @@ returns objects with that status only: # this query will only return published articles: Article.published.all() + + +random_filename +--------------- + +A factory for random filenames to be used with the ``FileField``'s +``upload_to`` parameter. + +The extension from the uploaded file is taken and used for the newly +generated filename. + +``random_filename`` can take two arguments: + +``directory``: + The directory the uploaded file should be placed in. Defaults is blank. +``random_function``: + A function that takes one argument, filename, and returns a random/unique + representation of that filename. + Default is ``uuid.uuid4``. + + +An example filename: `assets/270ef3e7-2105-4986-a8fb-6ef715273211.png`. + + +.. code-block:: python + + from django.db import models + + from model_utils.models import random_filename + + + class Asset(models.Model): + name = models.CharField(max_length=50) + file = models.FileField(upload_to=random_filename('assets/')) diff --git a/model_utils/models.py b/model_utils/models.py index 8030bea0..25037c26 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from os import path from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -92,3 +93,32 @@ def add_timeframed_query_manager(sender, **kwargs): models.signals.class_prepared.connect(add_status_query_managers) models.signals.class_prepared.connect(add_timeframed_query_manager) + + +def random_filename(directory='', random_function=None): + """Returns a function that generates random filenames for file + uploads. The new filename keeps the extension from the original + upload. + + The default ``random_function`` is uuid.uuid4 which also will + ensure that the filename is reasonably unique. + + Args: + directory: The directory the file should be uploaded to. Default: '' + random_function: A function that takes a filename as an argument + to generate a unique filename. The file extension will be added + to the value returned from this function. Default: uuid.uuid4 + + Returns: a function that can be used with Django's ``FileField``. + + """ + if not random_function: + from uuid import uuid4 + random_function = lambda _: uuid4() + + def random_name(instance, filename): + return path.join(directory, + '{0}{1}'.format(random_function(filename), + path.splitext(filename)[1])) + + return random_name diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index 63b16d1c..69d5cdcd 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -17,7 +17,8 @@ from model_utils import Choices, FieldTracker from model_utils.fields import get_excerpt, MonitorField, StatusField from model_utils.managers import QueryManager -from model_utils.models import StatusModel, TimeFramedModel +from model_utils.models import (StatusModel, TimeFramedModel, + random_filename) from model_utils.tests.models import ( InheritanceManagerTestRelated, InheritanceManagerTestGrandChild1, InheritanceManagerTestGrandChild1_2, @@ -1871,3 +1872,48 @@ def test_child_fields_not_tracked(self): self.name2 = 'test' self.assertEqual(self.tracker.previous('name2'), None) self.assertTrue(self.tracker.has_changed('name2')) + + +class RandomFilenameFactoryTest(TestCase): + def setUp(self): + self.random = lambda _: 'HELLO-IR-UUID' + self.random_name = random_filename( + 'test_dir', + self.random + ) + + def test_upload_directory_should_be_configurable(self): + random_name = random_filename('new-directory/', self.random) + self.assertEqual(random_name(None, 'hello.jpeg'), + 'new-directory/HELLO-IR-UUID.jpeg') + + def test_should_return_a_random_name(self): + self.assertEqual(self.random_name(None, 'hello.jpeg'), + 'test_dir/HELLO-IR-UUID.jpeg') + + def test_should_use_the_current_extension_from_passed_in_filename(self): + self.assertEqual(self.random_name(None, 'foo.bar'), + 'test_dir/HELLO-IR-UUID.bar') + + def test_should_work_with_no_file_extension(self): + self.assertEqual(self.random_name(None, 'hello'), + 'test_dir/HELLO-IR-UUID') + + def test_allow_the_random_function_to_be_replaced(self): + random_name = random_filename('test_dir', lambda _: 'UNIQUE') + self.assertEqual(random_name(None, 'hello.jpeg'), + 'test_dir/UNIQUE.jpeg') + + def test_random_function_takes_the_filename_as_an_argument(self): + random_name = random_filename( + 'test_dir', + lambda f: 'UNIQUE-{0}'.format(f) + ) + self.assertEqual(random_name(None, 'hello.jpeg'), + 'test_dir/UNIQUE-hello.jpeg.jpeg') + + def test_default_random_function_accepts_parameter(self): + random_name = random_filename('test_dir') + filename = random_name(None, 'hello.jpeg') + self.assertTrue(filename.startswith('test_dir/')) + self.assertTrue(filename.endswith('.jpeg'))