Ticket #7048: DeletableFileFields-02.diff
File DeletableFileFields-02.diff, 19.6 KB (added by , 16 years ago) |
---|
-
django/forms/models.py
7 7 from django.utils.datastructures import SortedDict 8 8 from django.utils.text import get_text_list, capfirst 9 9 from django.utils.translation import ugettext_lazy as _ 10 from django.core.files.uploadedfile import UploadedFile 11 from django.core.files import directories 10 12 11 13 from util import ValidationError, ErrorList 12 14 from forms import BaseForm, get_declared_fields 13 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES15 from fields import Field, ChoiceField, IntegerField, FileField, DeletableFileField, EMPTY_VALUES 14 16 from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 15 17 from widgets import media_property 16 18 from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME … … 20 22 except NameError: 21 23 from sets import Set as set # Python 2.3 fallback 22 24 25 import os 26 23 27 __all__ = ( 24 28 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', 25 29 'save_instance', 'form_for_fields', 'ModelChoiceField', … … 27 31 ) 28 32 29 33 34 def delete_file_field_file(file_field, model_instance, delete_from_disk=False, delete_empty_directories=False): 35 """ 36 Deletes the file of a model instances FileField. But only if no other 37 instances are pointing to it and it is not the default. 38 """ 39 file_obj = getattr(model_instance, file_field.attname) 40 if not file_obj: 41 return 42 43 # check whether we need to delete the file from disk or just clear the field 44 if delete_from_disk: 45 # create a queryset to determine if the file is being referenced 46 # by *another* instance. 47 queryset = model_instance.__class__._default_manager.\ 48 filter(**{file_field.attname: file_obj.name}).\ 49 exclude(pk=model_instance.pk) 50 # delete the file if is not referenced elsewhere and is not the default 51 if not queryset and file_obj.name != file_field.default: 52 file_path = file_obj.path 53 file_obj.delete(save=False) 54 if delete_empty_directories: 55 from django.conf import settings 56 directories.delete_empty_directories(stop_dir=settings.MEDIA_ROOT, dir=os.path.dirname(file_path)) 57 58 # TODO: look into this more. initially it was using None, but that 59 # was not working as the model field is null=False since there 60 # seems to be no reason why when the database field is a varchar. 61 setattr(model_instance, file_field.attname, u"") 62 63 30 64 def save_instance(form, instance, fields=None, fail_message='saved', 31 65 commit=True, exclude=None): 32 66 """ … … 52 86 continue 53 87 # Defer saving file-type fields until after the other fields, so a 54 88 # callable upload_to can use the values from other fields. 55 if isinstance(f, models.FileField) :89 if isinstance(f, models.FileField) or isinstance(f, DeletableFileField): 56 90 file_field_list.append(f) 57 91 else: 58 92 f.save_form_data(instance, cleaned_data[f.name]) 59 93 60 94 for f in file_field_list: 61 f.save_form_data(instance, cleaned_data[f.name]) 95 cleaned_field_data = cleaned_data[f.name] 96 # if this fields form field is a DeletableFileField instance we may need to delete the file 97 delete_file = False 98 if isinstance(form.fields[f.name], DeletableFileField): 99 # unpack the actual data and the delete checkbox value 100 cleaned_field_data, delete_file = cleaned_field_data 101 # replace file 102 if isinstance(cleaned_field_data, UploadedFile): 103 # TODO: if a user is uploading a file and checked the box that will 104 # call delete_file twice. 1) to delete the current file and 2) to 105 # delete the just uploaded file. maybe make this more clear or have a 106 # better sensible default. 107 delete_file_field_file(f, instance, form._meta.files_delete_from_disk, form._meta.files_delete_empty_dirs) 108 f.save_form_data(instance, cleaned_field_data) 109 if delete_file and f.blank: 110 delete_file_field_file(f, instance, form._meta.files_delete_from_disk, form._meta.files_delete_empty_dirs) 62 111 63 112 # Wrap up the saving of m2m data as a function. 64 113 def save_m2m(): … … 169 218 self.model = getattr(options, 'model', None) 170 219 self.fields = getattr(options, 'fields', None) 171 220 self.exclude = getattr(options, 'exclude', None) 221 self.files_add_delete_option = getattr(options, 'files_add_delete_option', True) 222 self.files_delete_from_disk = getattr(options, 'files_delete_from_disk', True) 223 self.files_delete_empty_dirs = getattr(options, 'files_delete_empty_dirs', False) 172 224 173 174 225 class ModelFormMetaclass(type): 175 226 def __new__(cls, name, bases, attrs): 176 227 formfield_callback = attrs.pop('formfield_callback', … … 196 247 # Override default model fields with any custom declared ones 197 248 # (plus, include all the other declared fields). 198 249 fields.update(declared_fields) 250 251 if opts.files_add_delete_option: 252 # wrap form FileFields in DeletableFileFields to show current file and get delete checkboxes (but only if blank=True) 253 for field_name, field in fields.items(): 254 if isinstance(field, FileField): 255 fields[field_name] = DeletableFileField(file_field_to_wrap=field, show_delete_checkbox=opts.model._meta.get_field(field_name).blank) 199 256 else: 200 257 fields = declared_fields 201 258 new_class.declared_fields = declared_fields … … 322 379 __metaclass__ = ModelFormMetaclass 323 380 324 381 def modelform_factory(model, form=ModelForm, fields=None, exclude=None, 325 formfield_callback=lambda f: f.formfield() ):382 formfield_callback=lambda f: f.formfield(), **kwargs): 326 383 # HACK: we should be able to construct a ModelForm without creating 327 384 # and passing in a temporary inner class 328 385 class Meta: … … 330 387 setattr(Meta, 'model', model) 331 388 setattr(Meta, 'fields', fields) 332 389 setattr(Meta, 'exclude', exclude) 390 # only add the options to the Meta class that are actually passed in via kwargs as the defaults are already in ModelFormOptions! 391 for attr in ('files_add_delete_option', 'files_delete_from_disk', 'files_delete_empty_dirs'): 392 if attr in kwargs: 393 setattr(Meta, attr, kwargs[attr]) 333 394 class_name = model.__name__ + 'Form' 334 395 return ModelFormMetaclass(class_name, (form,), {'Meta': Meta, 335 396 'formfield_callback': formfield_callback}) -
django/forms/fields.py
28 28 from django.utils.encoding import smart_unicode, smart_str 29 29 30 30 from util import ErrorList, ValidationError 31 from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput, TimeInput, SplitHiddenDateTimeWidget 31 from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput, TimeInput, SplitHiddenDateTimeWidget, DeletableFileInput, DeleteCheckboxInput 32 32 from django.core.files.uploadedfile import SimpleUploadedFile as UploadedFile 33 33 34 34 __all__ = ( … … 36 36 'DEFAULT_DATE_INPUT_FORMATS', 'DateField', 37 37 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', 38 38 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', 'TimeField', 39 'RegexField', 'EmailField', 'FileField', ' ImageField', 'URLField',39 'RegexField', 'EmailField', 'FileField', 'DeletableFileField', 'ImageField', 'URLField', 40 40 'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField', 41 41 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', 42 42 'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField', … … 525 525 f.seek(0) 526 526 return f 527 527 528 class DeletableFileField(Field): 529 """ 530 A Field that adds a delete checkbox to a FileField and shows the current value. file_field_to_wrap is the 531 FileField subclass or its instance to be wrapped. If show_delete_checkbox is set to False the field just 532 displays the current value and no delete checkbox. It returns a tupel with the actual FileField value and 533 the state of the delete checkbox. 534 """ 535 def __init__(self, file_field_to_wrap=FileField, show_delete_checkbox=True, *args, **kwargs): 536 self.fields = ( 537 isinstance(file_field_to_wrap, type) and file_field_to_wrap() or file_field_to_wrap, # instantiate if it is a class 538 BooleanField(widget=DeleteCheckboxInput, required=False), 539 ) 540 # get widgets from the fields and pass them to our MultiWidget 541 defaults = { 542 'widget': DeletableFileInput([f.widget for f in self.fields], show_delete_checkbox=show_delete_checkbox), 543 'required': self.fields[0].required, # this is just for display purposes, the actual check is performed by the FileField 544 } 545 defaults.update(kwargs) 546 super(DeletableFileField, self).__init__(*args, **defaults) 547 548 def clean(self, value, initial=None): 549 return (self.fields[0].clean(value[0], initial), self.fields[1].clean(value[1])) 550 528 551 url_re = re.compile( 529 552 r'^https?://' # http:// or https:// 530 553 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 … … 224 224 # widgets split data over several HTML fields. 225 225 value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) 226 226 try: 227 if isinstance(field, FileField):227 if isinstance(field, (FileField, DeletableFileField)): 228 228 initial = self.initial.get(name, field.initial) 229 229 value = field.clean(value, initial) 230 230 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 datetime import time 20 22 from util import flatatt 21 23 from urlparse import urljoin 22 24 23 25 __all__ = ( 24 26 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput', 25 'HiddenInput', 'MultipleHiddenInput', 27 'HiddenInput', 'MultipleHiddenInput', 'DeletableFileInput', 'DeleteCheckboxInput', 26 28 'FileInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput', 27 29 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', 28 30 'CheckboxSelectMultiple', 'MultiWidget', … … 650 652 return media 651 653 media = property(_get_media) 652 654 655 class DeletableFileInput(MultiWidget): 656 """ 657 A MultiWidget for the use with the DeletableFileField. The contained widgets are 658 specified by the fields constructor. If the value of the model FileField is None or 659 UploadedFile the rendered widget of the form FileField is just passed through. 660 """ 661 def __init__(self, widgets, attrs=None, show_delete_checkbox=True): 662 self.show_delete_checkbox = show_delete_checkbox 663 super(DeletableFileInput, self).__init__(widgets, attrs) 664 665 def render(self, name, value, attrs=None): 666 self.show_checkbox = False 667 return super(DeletableFileInput, self).render(name, value, attrs) 668 669 def decompress(self, value): 670 # decompress() is only called when we have initial data (not POST data) 671 # so we can be sure we have a proper File object here 672 if value: 673 self.show_checkbox = True 674 self.current_file = value 675 return [value, False] 676 # the value for DeleteCheckboxInput doesn't really matter as we won't show it anyway 677 return [None, None] 678 679 def format_output(self, rendered_widgets): 680 """ 681 Adds a link to the current file and wraps everything in spans to afford formatting. 682 """ 683 # this is a bit strange as we to do the actual test in decompress() 684 # but we don't have the value of the model FileField here so ... 685 if not self.show_checkbox: 686 return rendered_widgets[0] 687 # current file 688 output = [u'<span class="%s"><span class="%s">%s <a target="_blank" href="%s">%s</a></span><br/>' % ( 689 u'deletable-file-input', 690 u'deletable-file-input-current', 691 _('Currently:'), 692 self.current_file.url, 693 self.current_file, 694 )] 695 # file input 696 output.append('<span class="%s">%s %s</span><br/>' % ( 697 u'deletable-file-input-file-field', 698 _('Change:'), 699 rendered_widgets[0], # the FileFields widget 700 )) 701 # delete checkbox 702 if self.show_delete_checkbox: 703 output.append('<span class="%s">%s</span>' % ( 704 u'deletable-file-input-delete-checkbox', 705 rendered_widgets[1], # the checkbox 706 )) 707 output.append('</span>') 708 return u''.join(output) 709 710 class DeleteCheckboxInput(CheckboxInput): 711 """ 712 A CheckboxInput with a delete label next to it. It only renders if value is not None. 713 """ 714 def render(self, name, value, attrs=None): 715 if value == None: 716 return u'' 717 return u'%s <label for="%s">%s</label>' % ( 718 super(DeleteCheckboxInput, self).render(name, value, attrs), 719 attrs["id"], 720 _("Delete"), 721 ) 722 653 723 class SplitDateTimeWidget(MultiWidget): 654 724 """ 655 725 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
146 146 display: inline !important; 147 147 } 148 148 149 /* DELETABLE FILE INPUTS */ 150 151 .deletable-file-input { 152 display: block; 153 padding-left: 9.818181em; /* should match the 9em that inputs get pushed to the side by labels */ 154 } 155 156 .deletable-file-input-delete-checkbox { 157 display: block; 158 } 159 160 .deletable-file-input-delete-checkbox label { 161 display:inline; float:none; 162 } 163 149 164 /* MONOSPACE TEXTAREAS */ 150 165 151 166 fieldset.monospace textarea { -
django/contrib/admin/options.py
105 105 kwargs['widget'] = widgets.AdminTextInputWidget 106 106 return db_field.formfield(**kwargs) 107 107 108 # For FileFields and ImageFields add a link to the current file.109 if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField):110 kwargs['widget'] = widgets.AdminFileWidget111 return db_field.formfield(**kwargs)112 113 108 # For ForeignKey or ManyToManyFields, use a special widget. 114 109 if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)): 115 110 if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields: -
django/contrib/admin/widgets.py
80 80 class AdminRadioSelect(forms.RadioSelect): 81 81 renderer = AdminRadioFieldRenderer 82 82 83 class AdminFileWidget(forms.FileInput):84 """85 A FileField Widget that shows its current value if it has one.86 """87 def __init__(self, attrs={}):88 super(AdminFileWidget, self).__init__(attrs)89 90 def render(self, name, value, attrs=None):91 output = []92 if value and hasattr(value, "url"):93 output.append('%s <a target="_blank" href="%s">%s</a> <br />%s ' % \94 (_('Currently:'), value.url, value, _('Change:')))95 output.append(super(AdminFileWidget, self).render(name, value, attrs))96 return mark_safe(u''.join(output))97 98 83 class ForeignKeyRawIdWidget(forms.TextInput): 99 84 """ 100 85 A Widget for displaying ForeignKeys in the "raw_id" interface rather than -
docs/topics/forms/modelforms.txt
382 382 Chances are these notes won't affect you unless you're trying to do something 383 383 tricky with subclassing. 384 384 385 Making ``FileField``s deletable 386 =============================== 387 388 ``files_add_delete_option`` 389 --------------------------- 390 391 This option is ``True`` by default. 392 393 When set to ``True``, all ``FileField``s get delete checkboxes if they have the 394 option ``blank=True``. The current file is also displayed above the field, regardless 395 of the value of ``blank``. 396 397 ``files_delete_empty_dirs`` 398 --------------------------- 399 400 This option is ``False`` by default. 401 402 When set to ``True``, Django removes *empty* directories after each file is deleted. It 403 starts with the directory of the file and then moves it's way up till it reaches the 404 ``MEDIA_ROOT`` folder. **Warning**: This may delete more directories than you want. 405 Use with caution! 406 407 For example, if Django deletes the following file: 408 409 /media-root/foo/bar/file.png 410 411 It will first try to delete ``/media-root/foo/bar/`` and then ``/media-root/foo/``. 412 413 ``files_delete_from_disk`` 414 --------------------------- 415 416 This option is ``True`` by default. 417 418 When set to ``True``, files of ``FileField`` are deleted from the disk. This happens 419 when a FileField is updated or when it is cleared. The latter is only possible if 420 ``files_add_delete_option`` is ``True``. 421 385 422 .. _model-formsets: 386 423 387 424 Model Formsets