Django

Code

Ticket #7048: DeletableFileFields-02.diff

File DeletableFileFields-02.diff, 19.6 kB (added by jarrow, 2 months ago)

Moved documentation to the correct file

  • django/forms/models.py

    old new  
    77from django.utils.datastructures import SortedDict 
    88from django.utils.text import get_text_list, capfirst 
    99from django.utils.translation import ugettext_lazy as _ 
     10from django.core.files.uploadedfile import UploadedFile 
     11from django.core.files import directories 
    1012 
    1113from util import ValidationError, ErrorList 
    1214from forms import BaseForm, get_declared_fields 
    13 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES 
     15from fields import Field, ChoiceField, IntegerField, FileField, DeletableFileField, EMPTY_VALUES 
    1416from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 
    1517from widgets import media_property 
    1618from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME 
     
    2022except NameError: 
    2123    from sets import Set as set     # Python 2.3 fallback 
    2224 
     25import os 
     26 
    2327__all__ = ( 
    2428    'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', 
    2529    'save_instance', 'form_for_fields', 'ModelChoiceField', 
     
    2731) 
    2832 
    2933 
     34def 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 
    3064def save_instance(form, instance, fields=None, fail_message='saved', 
    3165                  commit=True, exclude=None): 
    3266    """ 
     
    5286            continue 
    5387        # Defer saving file-type fields until after the other fields, so a 
    5488        # 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)
    5690            file_field_list.append(f) 
    5791        else: 
    5892            f.save_form_data(instance, cleaned_data[f.name]) 
    5993             
    6094    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) 
    62111         
    63112    # Wrap up the saving of m2m data as a function. 
    64113    def save_m2m(): 
     
    169218        self.model = getattr(options, 'model', None) 
    170219        self.fields = getattr(options, 'fields', None) 
    171220        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) 
    172224 
    173  
    174225class ModelFormMetaclass(type): 
    175226    def __new__(cls, name, bases, attrs): 
    176227        formfield_callback = attrs.pop('formfield_callback', 
     
    196247            # Override default model fields with any custom declared ones 
    197248            # (plus, include all the other declared fields). 
    198249            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) 
    199256        else: 
    200257            fields = declared_fields 
    201258        new_class.declared_fields = declared_fields 
     
    322379    __metaclass__ = ModelFormMetaclass 
    323380 
    324381def modelform_factory(model, form=ModelForm, fields=None, exclude=None, 
    325                        formfield_callback=lambda f: f.formfield()): 
     382                       formfield_callback=lambda f: f.formfield(), **kwargs): 
    326383    # HACK: we should be able to construct a ModelForm without creating 
    327384    # and passing in a temporary inner class 
    328385    class Meta: 
     
    330387    setattr(Meta, 'model', model) 
    331388    setattr(Meta, 'fields', fields) 
    332389    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]) 
    333394    class_name = model.__name__ + 'Form' 
    334395    return ModelFormMetaclass(class_name, (form,), {'Meta': Meta, 
    335396                              'formfield_callback': formfield_callback}) 
  • django/forms/fields.py

    old new  
    2828from django.utils.encoding import smart_unicode, smart_str 
    2929 
    3030from util import ErrorList, ValidationError 
    31 from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput, TimeInput, SplitHiddenDateTimeWidget 
     31from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput, TimeInput, SplitHiddenDateTimeWidget, DeletableFileInput, DeleteCheckboxInput 
    3232from django.core.files.uploadedfile import SimpleUploadedFile as UploadedFile 
    3333 
    3434__all__ = ( 
     
    3636    'DEFAULT_DATE_INPUT_FORMATS', 'DateField', 
    3737    'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', 
    3838    'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', 'TimeField', 
    39     'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField', 
     39    'RegexField', 'EmailField', 'FileField', 'DeletableFileField', 'ImageField', 'URLField', 
    4040    'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField', 
    4141    'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', 
    4242    'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField', 
     
    525525            f.seek(0) 
    526526        return f 
    527527 
     528class 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 
    528551url_re = re.compile( 
    529552    r'^https?://' # http:// or https:// 
    530553    r'(?:(?:[A-Z0-9-]+\.)+[A-Z]{2,6}|' #domain... 
  • django/forms/forms.py

    old new  
    99from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode 
    1010from django.utils.safestring import mark_safe 
    1111 
    12 from fields import Field, FileField 
     12from fields import Field, FileField, DeletableFileField 
    1313from widgets import Media, media_property, TextInput, Textarea 
    1414from util import flatatt, ErrorDict, ErrorList, ValidationError 
    1515 
     
    224224            # widgets split data over several HTML fields. 
    225225            value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) 
    226226            try: 
    227                 if isinstance(field, FileField): 
     227                if isinstance(field, (FileField, DeletableFileField)): 
    228228                    initial = self.initial.get(name, field.initial) 
    229229                    value = field.clean(value, initial) 
    230230                else: 
  • django/forms/widgets.py

    old new  
    1616from django.utils.encoding import StrAndUnicode, force_unicode 
    1717from django.utils.safestring import mark_safe 
    1818from django.utils import datetime_safe 
     19from django.utils.translation import ugettext as _ 
     20from django.core.files.uploadedfile import UploadedFile 
    1921from datetime import time 
    2022from util import flatatt 
    2123from urlparse import urljoin 
    2224 
    2325__all__ = ( 
    2426    'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput', 
    25     'HiddenInput', 'MultipleHiddenInput', 
     27    'HiddenInput', 'MultipleHiddenInput', 'DeletableFileInput', 'DeleteCheckboxInput', 
    2628    'FileInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput', 
    2729    'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', 
    2830    'CheckboxSelectMultiple', 'MultiWidget', 
     
    650652        return media 
    651653    media = property(_get_media) 
    652654 
     655class 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 
     710class 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 
    653723class SplitDateTimeWidget(MultiWidget): 
    654724    """ 
    655725    A Widget that splits datetime input into two <input type="text"> boxes. 
  • django/core/files/directories.py

    old new  
     1import os 
     2 
     3def 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

    old new  
    146146    display: inline !important; 
    147147} 
    148148 
     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 
    149164/* MONOSPACE TEXTAREAS */ 
    150165 
    151166fieldset.monospace textarea { 
  • django/contrib/admin/options.py

    old new  
    105105            kwargs['widget'] = widgets.AdminTextInputWidget 
    106106            return db_field.formfield(**kwargs) 
    107107     
    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.AdminFileWidget 
    111             return db_field.formfield(**kwargs) 
    112  
    113108        # For ForeignKey or ManyToManyFields, use a special widget. 
    114109        if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)): 
    115110            if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields: 
  • django/contrib/admin/widgets.py

    old new  
    8080class AdminRadioSelect(forms.RadioSelect): 
    8181    renderer = AdminRadioFieldRenderer 
    8282 
    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  
    9883class ForeignKeyRawIdWidget(forms.TextInput): 
    9984    """ 
    10085    A Widget for displaying ForeignKeys in the "raw_id" interface rather than 
  • docs/topics/forms/modelforms.txt

    old new  
    382382Chances are these notes won't affect you unless you're trying to do something 
    383383tricky with subclassing. 
    384384 
     385Making ``FileField``s deletable 
     386=============================== 
     387 
     388``files_add_delete_option`` 
     389--------------------------- 
     390 
     391This option is ``True`` by default. 
     392 
     393When set to ``True``, all ``FileField``s get delete checkboxes if they have the 
     394option ``blank=True``. The current file is also displayed above the field, regardless 
     395of the value of ``blank``. 
     396 
     397``files_delete_empty_dirs`` 
     398--------------------------- 
     399 
     400This option is ``False`` by default. 
     401 
     402When set to ``True``, Django removes *empty* directories after each file is deleted. It 
     403starts 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. 
     405Use with caution! 
     406 
     407For example, if Django deletes the following file: 
     408 
     409    /media-root/foo/bar/file.png 
     410     
     411It will first try to delete ``/media-root/foo/bar/`` and then ``/media-root/foo/``. 
     412 
     413``files_delete_from_disk`` 
     414--------------------------- 
     415 
     416This option is ``True`` by default. 
     417 
     418When set to ``True``, files of ``FileField`` are deleted from the disk. This happens 
     419when a FileField is updated or when it is cleared. The latter is only possible if 
     420``files_add_delete_option`` is ``True``. 
     421 
    385422.. _model-formsets: 
    386423 
    387424Model Formsets