diff options
| author | Jannis Leidel <jannis@leidel.info> | 2010-10-01 02:02:58 +0000 |
|---|---|---|
| committer | Jannis Leidel <jannis@leidel.info> | 2010-10-01 02:02:58 +0000 |
| commit | 392d992f8295f96632179e01e790465cc9c8d3ec (patch) | |
| tree | bc1d6e3b0819f1b2a58a91d146fa6eb112d8396d /django | |
| parent | a64e96c227b36c701a86ded75d839b1cd2442713 (diff) | |
Fixed #7048 -- Added ClearableFileInput widget to clear file fields. Thanks for report and patch, jarrow and Carl Meyer.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@13968 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django')
| -rw-r--r-- | django/contrib/admin/media/css/widgets.css | 7 | ||||
| -rw-r--r-- | django/contrib/admin/widgets.py | 18 | ||||
| -rw-r--r-- | django/db/models/fields/files.py | 10 | ||||
| -rw-r--r-- | django/forms/fields.py | 41 | ||||
| -rw-r--r-- | django/forms/forms.py | 6 | ||||
| -rw-r--r-- | django/forms/widgets.py | 66 |
6 files changed, 125 insertions, 23 deletions
diff --git a/django/contrib/admin/media/css/widgets.css b/django/contrib/admin/media/css/widgets.css index 620e08289a..a761a7b7b0 100644 --- a/django/contrib/admin/media/css/widgets.css +++ b/django/contrib/admin/media/css/widgets.css @@ -198,6 +198,13 @@ p.file-upload { margin-left: 5px; } +span.clearable-file-input label { + color: #333; + font-size: 11px; + display: inline; + float: none; +} + /* CALENDARS & CLOCKS */ .calendarbox, .clockbox { diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 2c7ac5c794..eb7f217a9a 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -85,20 +85,12 @@ class AdminRadioFieldRenderer(RadioFieldRenderer): class AdminRadioSelect(forms.RadioSelect): renderer = AdminRadioFieldRenderer -class AdminFileWidget(forms.FileInput): - """ - A FileField Widget that shows its current value if it has one. - """ - def __init__(self, attrs={}): - super(AdminFileWidget, self).__init__(attrs) +class AdminFileWidget(forms.ClearableFileInput): + template_with_initial = (u'<p class="file-upload">%s</p>' + % forms.ClearableFileInput.template_with_initial) + template_with_clear = (u'<span class="clearable-file-input">%s</span>' + % forms.ClearableFileInput.template_with_clear) - def render(self, name, value, attrs=None): - output = [] - if value and hasattr(value, "url"): - output.append('%s <a target="_blank" href="%s">%s</a> <br />%s ' % \ - (_('Currently:'), value.url, value, _('Change:'))) - output.append(super(AdminFileWidget, self).render(name, value, attrs)) - return mark_safe(u''.join(output)) class ForeignKeyRawIdWidget(forms.TextInput): """ diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index 6dfeddbc41..9ee523b9d5 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -282,7 +282,15 @@ class FileField(Field): return os.path.join(self.get_directory_name(), self.get_filename(filename)) def save_form_data(self, instance, data): - if data: + # Important: None means "no change", other false value means "clear" + # This subtle distinction (rather than a more explicit marker) is + # needed because we need to consume values that are also sane for a + # regular (non Model-) Form to find in its cleaned_data dictionary. + if data is not None: + # This value will be converted to unicode and stored in the + # database, so leaving False as-is is not acceptable. + if not data: + data = '' setattr(instance, self.name, data) def formfield(self, **kwargs): diff --git a/django/forms/fields.py b/django/forms/fields.py index de14a5c8a8..03455e0989 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -27,8 +27,9 @@ from django.core.validators import EMPTY_VALUES from util import ErrorList from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, \ - FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, \ - DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget + ClearableFileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, \ + DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget, \ + FILE_INPUT_CONTRADICTION __all__ = ( 'Field', 'CharField', 'IntegerField', @@ -108,6 +109,9 @@ class Field(object): if self.localize: widget.is_localized = True + # Let the widget know whether it should display as required. + widget.is_required = self.required + # Hook into self.widget_attrs() for any Field-specific HTML attributes. extra_attrs = self.widget_attrs(widget) if extra_attrs: @@ -167,6 +171,17 @@ class Field(object): self.run_validators(value) return value + def bound_data(self, data, initial): + """ + Return the value that should be shown for this field on render of a + bound form, given the submitted POST data for the field and the initial + data, if any. + + For most fields, this will simply be data; FileFields need to handle it + a bit differently. + """ + return data + def widget_attrs(self, widget): """ Given a Widget instance (*not* a Widget class), returns a dictionary of @@ -434,12 +449,13 @@ class EmailField(CharField): default_validators = [validators.validate_email] class FileField(Field): - widget = FileInput + widget = ClearableFileInput default_error_messages = { 'invalid': _(u"No file was submitted. Check the encoding type on the form."), 'missing': _(u"No file was submitted."), 'empty': _(u"The submitted file is empty."), 'max_length': _(u'Ensure this filename has at most %(max)d characters (it has %(length)d).'), + 'contradiction': _(u'Please either submit a file or check the clear checkbox, not both.') } def __init__(self, *args, **kwargs): @@ -468,10 +484,29 @@ class FileField(Field): return data def clean(self, data, initial=None): + # If the widget got contradictory inputs, we raise a validation error + if data is FILE_INPUT_CONTRADICTION: + raise ValidationError(self.error_messages['contradiction']) + # False means the field value should be cleared; further validation is + # not needed. + if data is False: + if not self.required: + return False + # If the field is required, clearing is not possible (the widget + # shouldn't return False data in that case anyway). False is not + # in validators.EMPTY_VALUES; if a False value makes it this far + # it should be validated from here on out as None (so it will be + # caught by the required check). + data = None if not data and initial: return initial return super(FileField, self).clean(data) + def bound_data(self, data, initial): + if data in (None, FILE_INPUT_CONTRADICTION): + return initial + return data + class ImageField(FileField): default_error_messages = { 'invalid_image': _(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."), diff --git a/django/forms/forms.py b/django/forms/forms.py index 13ef1a7682..0377de4767 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -437,10 +437,8 @@ class BoundField(StrAndUnicode): if callable(data): data = data() else: - if isinstance(self.field, FileField) and self.data is None: - data = self.form.initial.get(self.name, self.field.initial) - else: - data = self.data + data = self.field.bound_data( + self.data, self.form.initial.get(self.name, self.field.initial)) data = self.field.prepare_value(data) if not only_initial: diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 2e16c35d8b..cb12586d6c 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -7,7 +7,7 @@ from itertools import chain from django.conf import settings from django.utils.datastructures import MultiValueDict, MergeDict from django.utils.html import escape, conditional_escape -from django.utils.translation import ugettext +from django.utils.translation import ugettext, ugettext_lazy from django.utils.encoding import StrAndUnicode, force_unicode from django.utils.safestring import mark_safe from django.utils import datetime_safe, formats @@ -18,7 +18,7 @@ from urlparse import urljoin __all__ = ( 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput', - 'HiddenInput', 'MultipleHiddenInput', + 'HiddenInput', 'MultipleHiddenInput', 'ClearableFileInput', 'FileInput', 'DateInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput', 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple', 'MultiWidget', @@ -134,6 +134,7 @@ class Widget(object): is_hidden = False # Determines whether this corresponds to an <input type="hidden">. needs_multipart_form = False # Determines does this widget need multipart-encrypted form is_localized = False + is_required = False def __init__(self, attrs=None): if attrs is not None: @@ -286,6 +287,67 @@ class FileInput(Input): return False return True +FILE_INPUT_CONTRADICTION = object() + +class ClearableFileInput(FileInput): + initial_text = ugettext_lazy('Currently') + input_text = ugettext_lazy('Change') + clear_checkbox_label = ugettext_lazy('Clear') + + template_with_initial = u'%(initial_text)s: %(initial)s %(clear_template)s<br />%(input_text)s: %(input)s' + + template_with_clear = u'%(clear)s <label for="%(clear_checkbox_id)s">%(clear_checkbox_label)s</label>' + + def clear_checkbox_name(self, name): + """ + Given the name of the file input, return the name of the clear checkbox + input. + """ + return name + '-clear' + + def clear_checkbox_id(self, name): + """ + Given the name of the clear checkbox input, return the HTML id for it. + """ + return name + '_id' + + def render(self, name, value, attrs=None): + substitutions = { + 'initial_text': self.initial_text, + 'input_text': self.input_text, + 'clear_template': '', + 'clear_checkbox_label': self.clear_checkbox_label, + } + template = u'%(input)s' + substitutions['input'] = super(ClearableFileInput, self).render(name, value, attrs) + + if value and hasattr(value, "url"): + template = self.template_with_initial + substitutions['initial'] = (u'<a target="_blank" href="%s">%s</a>' + % (value.url, value)) + if not self.is_required: + checkbox_name = self.clear_checkbox_name(name) + checkbox_id = self.clear_checkbox_id(checkbox_name) + substitutions['clear_checkbox_name'] = checkbox_name + substitutions['clear_checkbox_id'] = checkbox_id + substitutions['clear'] = CheckboxInput().render(checkbox_name, False, attrs={'id': checkbox_id}) + substitutions['clear_template'] = self.template_with_clear % substitutions + + return mark_safe(template % substitutions) + + def value_from_datadict(self, data, files, name): + upload = super(ClearableFileInput, self).value_from_datadict(data, files, name) + if not self.is_required and CheckboxInput().value_from_datadict( + data, files, self.clear_checkbox_name(name)): + if upload: + # If the user contradicts themselves (uploads a new file AND + # checks the "clear" checkbox), we return a unique marker + # object that FileField will turn into a ValidationError. + return FILE_INPUT_CONTRADICTION + # False signals to clear any existing value, as opposed to just None + return False + return upload + class Textarea(Widget): def __init__(self, attrs=None): # The 'rows' and 'cols' attributes are required for HTML correctness. |
