Ticket #7048: DeletableFileFields-00_code.diff
File DeletableFileFields-00_code.diff, 17.7 KB (added by , 16 years ago) |
---|
-
django/forms/models.py
6 6 from django.utils.translation import ugettext_lazy as _ 7 7 from django.utils.encoding import smart_unicode 8 8 from django.utils.datastructures import SortedDict 9 from django.core.files.uploadedfile import UploadedFile 10 from django.core.files import directories 9 11 10 12 from util import ValidationError, ErrorList 11 13 from forms import BaseForm, get_declared_fields 12 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES14 from fields import Field, ChoiceField, IntegerField, FileField, DeletableFileField, EMPTY_VALUES 13 15 from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 14 16 from widgets import media_property 15 17 from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME 16 18 19 import os 20 17 21 __all__ = ( 18 22 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', 19 23 'save_instance', 'form_for_fields', 'ModelChoiceField', 20 24 'ModelMultipleChoiceField', 21 25 ) 22 26 27 def delete_file_field_file(file_field, model_instance, delete_from_disk=False, delete_empty_directories=False): 28 """ 29 Deletes the file of a model instances FileField. But only if no other 30 instances are pointing to it and it is not the default. 31 """ 32 file_obj = getattr(model_instance, file_field.attname) 33 if not file_obj: 34 return 35 36 # check whether we need to delete the file from disk or just clear the field 37 if delete_from_disk: 38 # create a queryset to determine if the file is being referenced 39 # by *another* instance. 40 queryset = model_instance.__class__._default_manager.\ 41 filter(**{file_field.attname: file_obj.name}).\ 42 exclude(pk=model_instance.pk) 43 # delete the file if is not referenced elsewhere and is not the default 44 if not queryset and file_obj.name != file_field.default: 45 file_path = file_obj.path 46 file_obj.delete(save=False) 47 if delete_empty_directories: 48 from django.conf import settings 49 directories.delete_empty_directories(stop_dir=settings.MEDIA_ROOT, dir=os.path.dirname(file_path)) 50 51 # TODO: look into this more. initially it was using None, but that 52 # was not working as the model field is null=False since there 53 # seems to be no reason why when the database field is a varchar. 54 setattr(model_instance, file_field.attname, u"") 55 56 23 57 def save_instance(form, instance, fields=None, fail_message='saved', 24 58 commit=True): 25 59 """ … … 40 74 continue 41 75 if fields and f.name not in fields: 42 76 continue 43 f.save_form_data(instance, cleaned_data[f.name]) 77 cleaned_field_data = cleaned_data[f.name] 78 # if this fields form field is a DeletableFileField instance we may need to delete the file 79 delete_file = False 80 if isinstance(form.fields[f.name], DeletableFileField): 81 # unpack the actual data and the delete checkbox value 82 cleaned_field_data, delete_file = cleaned_field_data 83 # replace file 84 if isinstance(cleaned_field_data, UploadedFile): 85 # TODO: if a user is uploading a file and checked the box that will 86 # call delete_file twice. 1) to delete the current file and 2) to 87 # delete the just uploaded file. maybe make this more clear or have a 88 # better sensible default. 89 delete_file_field_file(f, instance, form._meta.files_delete_from_disk, form._meta.files_delete_empty_dirs) 90 f.save_form_data(instance, cleaned_field_data) 91 if delete_file: 92 delete_file_field_file(f, instance, form._meta.files_delete_from_disk, form._meta.files_delete_empty_dirs) 93 44 94 # Wrap up the saving of m2m data as a function. 45 95 def save_m2m(): 46 96 opts = instance._meta … … 150 200 self.model = getattr(options, 'model', None) 151 201 self.fields = getattr(options, 'fields', None) 152 202 self.exclude = getattr(options, 'exclude', None) 203 self.files_add_delete_option = getattr(options, 'files_add_delete_option', True) 204 self.files_delete_from_disk = getattr(options, 'files_delete_from_disk', True) 205 self.files_delete_empty_dirs = getattr(options, 'files_delete_empty_dirs', False) 153 206 154 155 207 class ModelFormMetaclass(type): 156 208 def __new__(cls, name, bases, attrs): 157 209 formfield_callback = attrs.pop('formfield_callback', … … 177 229 # Override default model fields with any custom declared ones 178 230 # (plus, include all the other declared fields). 179 231 fields.update(declared_fields) 232 233 if opts.files_add_delete_option: 234 # wrap form FileFields in DeletableFileFields to show current file and get delete checkboxes (but only if blank=True) 235 for field_name, field in fields.items(): 236 if isinstance(field, FileField): 237 fields[field_name] = DeletableFileField(file_field_to_wrap=field, show_delete_checkbox=opts.model._meta.get_field(field_name).blank) 180 238 else: 181 239 fields = declared_fields 182 240 new_class.declared_fields = declared_fields … … 219 277 __metaclass__ = ModelFormMetaclass 220 278 221 279 def modelform_factory(model, form=ModelForm, fields=None, exclude=None, 222 formfield_callback=lambda f: f.formfield() ):280 formfield_callback=lambda f: f.formfield(), **kwargs): 223 281 # HACK: we should be able to construct a ModelForm without creating 224 282 # and passing in a temporary inner class 225 283 class Meta: … … 227 285 setattr(Meta, 'model', model) 228 286 setattr(Meta, 'fields', fields) 229 287 setattr(Meta, 'exclude', exclude) 288 # only add the options to the Meta class that are actually passed in via kwargs as the defaults are already in ModelFormOptions! 289 for attr in ('files_add_delete_option', 'files_delete_from_disk', 'files_delete_empty_dirs'): 290 if attr in kwargs: 291 setattr(Meta, attr, kwargs[attr]) 230 292 class_name = model.__name__ + 'Form' 231 293 return ModelFormMetaclass(class_name, (form,), {'Meta': Meta, 232 294 'formfield_callback': formfield_callback}) -
django/forms/fields.py
27 27 from django.utils.encoding import smart_unicode, smart_str 28 28 29 29 from util import ErrorList, ValidationError 30 from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput 30 from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput, DeletableFileInput, DeleteCheckboxInput 31 31 from django.core.files.uploadedfile import SimpleUploadedFile as UploadedFile 32 32 33 33 __all__ = ( … … 35 35 'DEFAULT_DATE_INPUT_FORMATS', 'DateField', 36 36 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', 37 37 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', 38 'RegexField', 'EmailField', 'FileField', ' ImageField', 'URLField',38 'RegexField', 'EmailField', 'FileField', 'DeletableFileField', 'ImageField', 'URLField', 39 39 'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField', 40 40 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', 41 41 'SplitDateTimeField', 'IPAddressField', 'FilePathField', … … 519 519 f.seek(0) 520 520 return f 521 521 522 class DeletableFileField(Field): 523 """ 524 A Field that adds a delete checkbox to a FileField and shows the current value. file_field_to_wrap is the 525 FileField subclass or its instance to be wrapped. If show_delete_checkbox is set to False the field just 526 displays the current value and no delete checkbox. It returns a tupel with the actual FileField value and 527 the state of the delete checkbox. 528 """ 529 def __init__(self, file_field_to_wrap=FileField, show_delete_checkbox=True, *args, **kwargs): 530 self.fields = ( 531 isinstance(file_field_to_wrap, type) and file_field_to_wrap() or file_field_to_wrap, # instantiate if it is a class 532 BooleanField(widget=DeleteCheckboxInput, required=False), 533 ) 534 # get widgets from the fields and pass them to our MultiWidget 535 defaults = { 536 'widget': DeletableFileInput([f.widget for f in self.fields], show_delete_checkbox=show_delete_checkbox), 537 'required': self.fields[0].required, # this is just for display purposes, the actual check is performed by the FileField 538 } 539 defaults.update(kwargs) 540 super(DeletableFileField, self).__init__(*args, **defaults) 541 542 def clean(self, value, initial=None): 543 return (self.fields[0].clean(value[0], initial), self.fields[1].clean(value[1])) 544 522 545 url_re = re.compile( 523 546 r'^https?://' # http:// or https:// 524 547 r'(?:(?:[A-Z0-9-]+\.)+[A-Z]{2,6}|' #domain... -
django/forms/forms.py
9 9 from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode 10 10 from django.utils.safestring import mark_safe 11 11 12 from fields import Field, FileField 12 from fields import Field, FileField, DeletableFileField 13 13 from widgets import Media, media_property, TextInput, Textarea 14 14 from util import flatatt, ErrorDict, ErrorList, ValidationError 15 15 … … 211 211 # widgets split data over several HTML fields. 212 212 value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) 213 213 try: 214 if isinstance(field, FileField):214 if isinstance(field, (FileField, DeletableFileField)): 215 215 initial = self.initial.get(name, field.initial) 216 216 value = field.clean(value, initial) 217 217 else: -
django/forms/widgets.py
16 16 from django.utils.encoding import StrAndUnicode, force_unicode 17 17 from django.utils.safestring import mark_safe 18 18 from django.utils import datetime_safe 19 from django.utils.translation import ugettext as _ 20 from django.core.files.uploadedfile import UploadedFile 19 21 from util import flatatt 20 22 from urlparse import urljoin 21 23 22 24 __all__ = ( 23 25 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput', 24 'HiddenInput', 'MultipleHiddenInput', 25 ' FileInput', 'DateTimeInput', 'Textarea', 'CheckboxInput',26 'HiddenInput', 'MultipleHiddenInput', 'FileInput', 'DateTimeInput', 27 'Textarea', 'CheckboxInput', 'DeletableFileInput', 'DeleteCheckboxInput', 26 28 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', 27 29 'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget', 28 30 ) … … 638 640 return media 639 641 media = property(_get_media) 640 642 643 class DeletableFileInput(MultiWidget): 644 """ 645 A MultiWidget for the use with the DeletableFileField. The contained widgets are 646 specified by the fields constructor. If the value of the model FileField is None or 647 UploadedFile the rendered widget of the form FileField is just passed through. 648 """ 649 def __init__(self, widgets, attrs=None, show_delete_checkbox=True): 650 self.show_delete_checkbox = show_delete_checkbox 651 super(DeletableFileInput, self).__init__(widgets, attrs) 652 653 def render(self, name, value, attrs=None): 654 self.show_checkbox = False 655 return super(DeletableFileInput, self).render(name, value, attrs) 656 657 def decompress(self, value): 658 # decompress() is only called when we have initial data (not POST data) 659 # so we can be sure we have a proper File object here 660 if value: 661 self.show_checkbox = True 662 self.current_file = value 663 return [value, False] 664 # the value for DeleteCheckboxInput doesn't really matter as we won't show it anyway 665 return [None, None] 666 667 def format_output(self, rendered_widgets): 668 """ 669 Adds a link to the current file and wraps everything in spans to afford formatting. 670 """ 671 # this is a bit strange as we to do the actual test in decompress() 672 # but we don't have the value of the model FileField here so ... 673 if not self.show_checkbox: 674 return rendered_widgets[0] 675 # current file 676 output = [u'<span class="%s"><span class="%s">%s <a target="_blank" href="%s">%s</a></span><br/>' % ( 677 u'deletable-file-input', 678 u'deletable-file-input-current', 679 _('Currently:'), 680 self.current_file.url, 681 self.current_file, 682 )] 683 # file input 684 output.append('<span class="%s">%s %s</span><br/>' % ( 685 u'deletable-file-input-file-field', 686 _('Change:'), 687 rendered_widgets[0], # the FileFields widget 688 )) 689 # delete checkbox 690 if self.show_delete_checkbox: 691 output.append('<span class="%s">%s</span>' % ( 692 u'deletable-file-input-delete-checkbox', 693 rendered_widgets[1], # the checkbox 694 )) 695 output.append('</span>') 696 return u''.join(output) 697 698 class DeleteCheckboxInput(CheckboxInput): 699 """ 700 A CheckboxInput with a delete label next to it. It only renders if value is not None. 701 """ 702 def render(self, name, value, attrs=None): 703 if value == None: 704 return u'' 705 return u'%s <label for="%s">%s</label>' % ( 706 super(DeleteCheckboxInput, self).render(name, value, attrs), 707 attrs["id"], 708 _("Delete"), 709 ) 710 641 711 class SplitDateTimeWidget(MultiWidget): 642 712 """ 643 713 A Widget that splits datetime input into two <input type="text"> boxes. -
django/core/files/directories.py
1 import os 2 3 def delete_empty_directories(stop_dir, dir): 4 """ 5 Deletes *empty* directories starting with dir and working it's way up till it hits stop_dir. 6 """ 7 # normalize paths 8 stop_dir = os.path.abspath(stop_dir) 9 dir = os.path.abspath(dir) 10 if not os.path.exists(stop_dir): 11 raise Exception("stop_dir ('%s') is not a valid directory." % stop_dir) 12 try: 13 while True: 14 if dir == stop_dir or len(dir) < 4: 15 return 16 os.rmdir(dir) 17 dir = os.path.dirname(dir) 18 except OSError: 19 # stop if we have not enough permissions or dir is not empty 20 pass -
django/contrib/admin/media/css/forms.css
41 41 fieldset.collapsed h2 { background-image:url(../img/admin/nav-bg.gif); background-position:bottom left; color:#999; } 42 42 fieldset.collapsed .collapse-toggle { padding:3px 5px !important; background:transparent; display:inline !important;} 43 43 44 /* DELETABLE FILE INPUTS */ 45 46 .deletable-file-input { display: block; padding-left: 9.818181em} /* should match the 9em that inputs get pushed to the side by labels */ 47 .deletable-file-input-delete-checkbox { display: block; } 48 .deletable-file-input-delete-checkbox label { display:inline; float:none; } 49 44 50 /* MONOSPACE TEXTAREAS */ 45 51 fieldset.monospace textarea { font-family:"Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace; } 46 52 -
django/contrib/admin/options.py
99 99 kwargs['widget'] = widgets.AdminTextInputWidget 100 100 return db_field.formfield(**kwargs) 101 101 102 # For FileFields and ImageFields add a link to the current file.103 if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField):104 kwargs['widget'] = widgets.AdminFileWidget105 return db_field.formfield(**kwargs)106 107 102 # For ForeignKey or ManyToManyFields, use a special widget. 108 103 if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)): 109 104 if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields: -
django/contrib/admin/widgets.py
75 75 class AdminRadioSelect(forms.RadioSelect): 76 76 renderer = AdminRadioFieldRenderer 77 77 78 class AdminFileWidget(forms.FileInput):79 """80 A FileField Widget that shows its current value if it has one.81 """82 def __init__(self, attrs={}):83 super(AdminFileWidget, self).__init__(attrs)84 85 def render(self, name, value, attrs=None):86 output = []87 if value and hasattr(value, "url"):88 output.append('%s <a target="_blank" href="%s">%s</a> <br />%s ' % \89 (_('Currently:'), value.url, value, _('Change:')))90 output.append(super(AdminFileWidget, self).render(name, value, attrs))91 return mark_safe(u''.join(output))92 93 78 class ForeignKeyRawIdWidget(forms.TextInput): 94 79 """ 95 80 A Widget for displaying ForeignKeys in the "raw_id" interface rather than