From 3c5ff9d703be67b43655c5a40e8ee3719ad2871b Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 22 Apr 2012 14:44:08 +0000 Subject: [PATCH] Fixed #5893 -- Added a flag to FilePathField to allow listing folders, in addition to regular files. Thank you to Brian Rosner, for encouraging me to first contribute to Django 4 years ago. git-svn-id: http://code.djangoproject.com/svn/django/trunk@17925 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/management/validation.py | 2 ++ django/db/models/fields/__init__.py | 5 ++- django/forms/fields.py | 25 +++++++++----- docs/ref/forms/fields.txt | 17 ++++++++++ docs/ref/models/fields.txt | 17 ++++++++++ tests/regressiontests/forms/tests/fields.py | 36 +++++++++++++++++++++ 6 files changed, 93 insertions(+), 9 deletions(-) diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 4833337445..272ac5be41 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -89,6 +89,8 @@ def get_validation_errors(outfile, app=None): e.add(opts, '"%s": To use ImageFields, you need to install the Python Imaging Library. Get it at http://www.pythonware.com/products/pil/ .' % f.name) if isinstance(f, models.BooleanField) and getattr(f, 'null', False): e.add(opts, '"%s": BooleanFields do not accept null values. Use a NullBooleanField instead.' % f.name) + if isinstance(f, models.FilePathField) and not (f.allow_files or f.allow_folders): + e.add(opts, '"%s": FilePathFields must have either allow_files or allow_folders set to True.' % f.name) if f.choices: if isinstance(f.choices, basestring) or not is_iterable(f.choices): e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 121797a581..e240d39cc4 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -909,8 +909,9 @@ class FilePathField(Field): description = _("File path") def __init__(self, verbose_name=None, name=None, path='', match=None, - recursive=False, **kwargs): + recursive=False, allow_files=True, allow_folders=False, **kwargs): self.path, self.match, self.recursive = path, match, recursive + self.allow_files, self.allow_folders = allow_files, allow_folders kwargs['max_length'] = kwargs.get('max_length', 100) Field.__init__(self, verbose_name, name, **kwargs) @@ -920,6 +921,8 @@ class FilePathField(Field): 'match': self.match, 'recursive': self.recursive, 'form_class': forms.FilePathField, + 'allow_files': self.allow_files, + 'allow_folders': self.allow_folders, } defaults.update(kwargs) return super(FilePathField, self).formfield(**defaults) diff --git a/django/forms/fields.py b/django/forms/fields.py index 49f29db723..3cf730e152 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -909,10 +909,11 @@ class MultiValueField(Field): raise NotImplementedError('Subclasses must implement this method.') class FilePathField(ChoiceField): - def __init__(self, path, match=None, recursive=False, required=True, - widget=None, label=None, initial=None, help_text=None, - *args, **kwargs): + def __init__(self, path, match=None, recursive=False, allow_files=True, + allow_folders=False, required=True, widget=None, label=None, + initial=None, help_text=None, *args, **kwargs): self.path, self.match, self.recursive = path, match, recursive + self.allow_files, self.allow_folders = allow_files, allow_folders super(FilePathField, self).__init__(choices=(), required=required, widget=widget, label=label, initial=initial, help_text=help_text, *args, **kwargs) @@ -927,15 +928,23 @@ class FilePathField(ChoiceField): if recursive: for root, dirs, files in sorted(os.walk(self.path)): - for f in files: - if self.match is None or self.match_re.search(f): - f = os.path.join(root, f) - self.choices.append((f, f.replace(path, "", 1))) + if self.allow_files: + for f in files: + if self.match is None or self.match_re.search(f): + f = os.path.join(root, f) + self.choices.append((f, f.replace(path, "", 1))) + if self.allow_folders: + for f in dirs: + if self.match is None or self.match_re.search(f): + f = os.path.join(root, f) + self.choices.append((f, f.replace(path, "", 1))) else: try: for f in sorted(os.listdir(self.path)): full_file = os.path.join(self.path, f) - if os.path.isfile(full_file) and (self.match is None or self.match_re.search(f)): + if (((self.allow_files and os.path.isfile(full_file)) or + (self.allow_folders and os.path.isdir(full_file))) and + (self.match is None or self.match_re.search(f))): self.choices.append((full_file, f)) except OSError: pass diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 4956a71b44..fa5800692f 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -555,6 +555,23 @@ For each field, we describe the default widget used if you don't specify A regular expression pattern; only files with names matching this expression will be allowed as choices. + .. attribute:: allow_files + + .. versionadded:: 1.5 + + Optional. Either ``True`` or ``False``. Default is ``True``. Specifies + whether files in the specified location should be included. Either this or + :attr:`allow_folders` must be ``True``. + + .. attribute:: allow_folders + + .. versionadded:: 1.5 + + Optional. Either ``True`` or ``False``. Default is ``False``. Specifies + whether folders in the specified location should be included. Either this or + :attr:`allow_files` must be ``True``. + + ``FloatField`` ~~~~~~~~~~~~~~ diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 5521a0ed24..d1f12ffc95 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -691,6 +691,23 @@ directory on the filesystem. Has three special arguments, of which the first is Optional. Either ``True`` or ``False``. Default is ``False``. Specifies whether all subdirectories of :attr:`~FilePathField.path` should be included +.. attribute:: FilePathField.allow_files + + .. versionadded:: 1.5 + + Optional. Either ``True`` or ``False``. Default is ``True``. Specifies + whether files in the specified location should be included. Either this or + :attr:`~FilePathField.allow_folders` must be ``True``. + +.. attribute:: FilePathField.allow_folders + + .. versionadded:: 1.5 + + Optional. Either ``True`` or ``False``. Default is ``False``. Specifies + whether folders in the specified location should be included. Either this + or :attr:`~FilePathField.allow_files` must be ``True``. + + Of course, these arguments can be used together. The one potential gotcha is that :attr:`~FilePathField.match` applies to the diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index 6e4a5445ca..4e942a1ca7 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -981,6 +981,42 @@ class FieldsTests(SimpleTestCase): self.assertEqual(exp[1], got[1]) self.assertTrue(got[0].endswith(exp[0])) + def test_filepathfield_folders(self): + path = forms.__file__ + path = os.path.dirname(path) + '/' + f = FilePathField(path=path, allow_folders=True, allow_files=False) + f.choices.sort() + expected = [ + ('/django/forms/extras', 'extras'), + ] + for exp, got in zip(expected, fix_os_paths(f.choices)): + self.assertEqual(exp[1], got[1]) + self.assert_(got[0].endswith(exp[0])) + + f = FilePathField(path=path, allow_folders=True, allow_files=True) + f.choices.sort() + expected = [ + ('/django/forms/__init__.py', '__init__.py'), + ('/django/forms/__init__.pyc', '__init__.pyc'), + ('/django/forms/extras', 'extras'), + ('/django/forms/fields.py', 'fields.py'), + ('/django/forms/fields.pyc', 'fields.pyc'), + ('/django/forms/forms.py', 'forms.py'), + ('/django/forms/forms.pyc', 'forms.pyc'), + ('/django/forms/formsets.py', 'formsets.py'), + ('/django/forms/formsets.pyc', 'formsets.pyc'), + ('/django/forms/models.py', 'models.py'), + ('/django/forms/models.pyc', 'models.pyc'), + ('/django/forms/util.py', 'util.py'), + ('/django/forms/util.pyc', 'util.pyc'), + ('/django/forms/widgets.py', 'widgets.py'), + ('/django/forms/widgets.pyc', 'widgets.pyc') + ] + for exp, got in zip(expected, fix_os_paths(f.choices)): + self.assertEqual(exp[1], got[1]) + self.assertEqual(exp[1], got[1]) + + # SplitDateTimeField ########################################################## def test_splitdatetimefield_1(self):