Login

Updated FileField / ImageField with a delete checkbox

Author:
tomZ
Posted:
March 10, 2008
Language:
Python
Version:
.96
Score:
1 (after 1 ratings)

Example model:

class MyModel(models.Model):
    file = RemovableFileField(upload_to='files', \
        null=True, blank=True)
    image = RemovableImageField(upload_to='images', \
        null=True, blank=True)

A delete checkbox will be automatically rendered when using ModelForm or editing it using form_for_instance. Also, the filename or the image will be displayed below the form field. You can edit the render method of the DeleteCheckboxWidget or add one to RemovableFileFormWidget to customize the rendering.

UPDATE: 5. April 2009. Making it work with latest Django (thanks for the comments).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
from django import forms
from django.conf import settings
from django.db import models
from django.utils.translation import ugettext as _

import os


class DeleteCheckboxWidget(forms.CheckboxInput):
    def __init__(self, *args, **kwargs):
        self.is_image = kwargs.pop('is_image')
        self.value = kwargs.pop('initial')
        super(DeleteCheckboxWidget, self).__init__(*args, **kwargs)

    def render(self, name, value, attrs=None):
        value = value or self.value
        if value:
            s = u'<label for="%s">%s %s</label>' % (
                    attrs['id'],
                    super(DeleteCheckboxWidget, self).render(name, False, attrs),
                    _('Delete')
                )
            if self.is_image:
                s += u'<br><img src="%s%s" width="50">' % (settings.MEDIA_URL, value)
            else:
                s += u'<br><a href="%s%s">%s</a>' % (settings.MEDIA_URL, value, os.path.basename(value))
            return s
        else:
            return u''


class RemovableFileFormWidget(forms.MultiWidget):
    def __init__(self, is_image=False, initial=None, **kwargs):
        widgets = (forms.FileInput(), DeleteCheckboxWidget(is_image=is_image, initial=initial))
        super(RemovableFileFormWidget, self).__init__(widgets)

    def decompress(self, value):
        return [None, value]

class RemovableFileFormField(forms.MultiValueField):
    widget = RemovableFileFormWidget
    field = forms.FileField
    is_image = False

    def __init__(self, *args, **kwargs):
        fields = [self.field(*args, **kwargs), forms.BooleanField(required=False)]
        # Compatibility with form_for_instance
        if kwargs.get('initial'):
            initial = kwargs['initial']
        else:
            initial = None
        self.widget = self.widget(is_image=self.is_image, initial=initial)
        super(RemovableFileFormField, self).__init__(fields, label=kwargs.pop('label'), required=False)

    def compress(self, data_list):
        return data_list


class RemovableImageFormField(RemovableFileFormField):
    field = forms.ImageField
    is_image = True


class RemovableFileField(models.FileField):
    def delete_file(self, instance, *args, **kwargs):
        if getattr(instance, self.attname):
            image = getattr(instance, '%s' % self.name)
            file_name = image.path
            # If the file exists and no other object of this type references it,
            # delete it from the filesystem.
            if os.path.exists(file_name) and \
                not instance.__class__._default_manager.filter(**{'%s__exact' % self.name: getattr(instance, self.attname)}).exclude(pk=instance._get_pk_val()):
                os.remove(file_name)

    def get_internal_type(self):
        return 'FileField'

    def save_form_data(self, instance, data):
        if data and data[0]: # Replace file
            self.delete_file(instance)
            super(RemovableFileField, self).save_form_data(instance, data[0])
        if data and data[1]: # Delete file
            self.delete_file(instance)
            setattr(instance, self.name, None)

    def formfield(self, **kwargs):
        defaults = {'form_class': RemovableFileFormField}
        defaults.update(kwargs)
        return super(RemovableFileField, self).formfield(**defaults)


class RemovableImageField(models.ImageField, RemovableFileField):
    def formfield(self, **kwargs):
        defaults = {'form_class': RemovableImageFormField}
        defaults.update(kwargs)
        return super(RemovableFileField, self).formfield(**defaults)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 1 week ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

rmnl (on March 18, 2008):

It sounds like a great snippet, but when I make a model with the following code:

class MyModel(models.Model):
    file = RemovableFileField(upload_to='files', \
        null=True, blank=True)
    image = RemovableImageField(upload_to='images', \
        null=True, blank=True)
    class Admin:
        pass

No deletbox shows up in the admin website. Am I forgetting something?

#

mjr578 (on March 27, 2008):

This doesn't show up for me as well. Anything else that needs to be done to get it to show up?

#

crucialfelix (on June 18, 2008):

note: only works with ModelForm and form_for_instance under newforms.

so the admin does not YET work with this. but it will.

#

brunobord (on December 16, 2008):

this snippet is slightly out of date... The "delete_file" in the "RemovableFileField" class needs to be patched that way:

class RemovableFileField(models.FileField):
    def delete_file(self, instance, *args, **kwargs):
        if getattr(instance, self.attname):
            image = getattr(instance, '%s' % self.name)
            file_name = image.path
            # If the file exists and no other object of this type references it,
            # delete it from the filesystem.
            if os.path.exists(file_name) and \
                not instance.__class__._default_manager.filter(**{'%s__exact' % self.name: getattr(instance, self.attname)}).exclude(pk=instance._get_pk_val()):
                os.remove(file_name)

it looks like the rest of the snippets doesn't need to be changed so far. The "args, *kwargs" is needed when you want to use signals, or the model doesn't validate. Using Django 1.0, any File/Image Field comes with a lot of very handy properties, 'path' being very helpful when you want to access the path of a file in the filesystem.

Hope this helps.

#

djesus (on January 4, 2009):

If there is a verbose name like:

class MyModel(models.Model):

file = RemovableFileField('Hello', upload_to='files', \
    null=True, blank=True)

the label returned is file not Hello.

To fix the problem change the line in the class RemovableFileFormField

super(RemovableFileFormField, self).init(fields, required=False)

for

super(RemovableFileFormField, self).init(fields, label=kwargs.pop('label'), required=False)

#

tomZ (on April 5, 2009):

Thanks for the comments. I updated the snippet.

#

sweecoo (on June 2, 2009):

Now files are not always represented as strings. Add these lines to DeleteCheckboxWidget:

        if hasattr(value, 'name'):
            value = value.name

#

gregb (on June 3, 2009):

I was trying to use the RemovableFileField in a model and it was causing the whole tabularInline admin section for that model to disappear - whereas RemovableImageField worked fine. I traced the problem to line 26:

                s += u'<br><a href="%s%s">%s</a>' % (settings.MEDIA_URL, value, os.path.basename(value))

changing this to

                s += u'<br><a href="%s%s">%s</a>' % (settings.MEDIA_URL, value, os.path.basename(unicode(value)))

seemed to fix the problem.

#

gregb (on June 3, 2009):

Also, if you want the delete checkbox to work when used in an admin inline formset, you'll need to add a _has_changed method to the DeleteCheckboxWidget:

class DeleteCheckboxWidget(forms.CheckboxInput):
    ...
    def _has_changed(self, initial, data):
        return data

This tells the formset to save if the checkbox is checked. Otherwise your file won't be deleted unless you change another field in the inline formset.

#

mpstevens_uk (on July 16, 2009):

Hi.

I can't get this working on current django. I get a stack trace:

Traceback:
File "/opt/dev/python/modules/django/core/handlers/base.py" in get_response
  92.                 response = callback(request,         *callback_args, **callback_kwargs)
File "/opt/dev/python/django/smartad_manager/manager  /views.py" in smartad_edit
  100.          variant_formset.save()
File "/opt/dev/python/modules/django/forms/models.py" in save
  522.         return           self.save_existing_objects(commit) +        self.save_new_objects(commit)
File "/opt/dev/python/modules/django/forms/models.py"     in    save_existing_objects
     640.                    saved_instances.append(self.save_existing(form, obj, commit=commit))
File "/opt/dev/python/modules/django/forms/models.py" in save_existing
  510.         return     form.save(commit=commit)
File "/opt/dev/python/modules/django/forms/models.py" in save
407.                         fail_message, commit,    exclude=self._meta.exclude)
File "/opt/dev/python/modules/django/forms/models.py" in save_instance
  1. instance.save() File "/opt/dev/python/modules/django/db/models/base.py" in save
  2. self.save_base(force_insert=force_insert, force_update=force_update) File "/opt/dev/python/modules/django/db/models/base.py" in save_base
  3. values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks] File "/opt/dev/python/modules/django/db/models/fields/files.py" in pre_save
  4. if file and not file._committed:

    Exception Type: AttributeError at /manager/edit_smartad/1 Exception Value: 'list' object has no attribute '_committed'

#

florentin (on December 1, 2010):

tezro, to fix the rfind error you must replace "os.path.basename(value)" with "os.path.basename(str(value))"

#

idnael (on December 23, 2010):

When I check the box to delete a file, but then the form fails validation (for some other field that should be not blank) then the checkbox doesn't keep the value.

#

wimf (on June 20, 2011):

Hi,

line 80 might cause one user to delete another users' files. Therefore, I would strongly recommend to delete line 80 and just don't delete files.

It occurs when an image is saved with the same name as an existing image: the existing image is removed and replaced by the new image. If the existing image was uploaded by another user it gets replaced by the new image!

The same would happen with files, so potentially a user could get access to another one's file! And his original would be destroyed.

Django normally detects this automatically and adds underscores (_) at the end of a file name to avoid duplication. But in the above snippet, the original image is destroyed first and then there is no underscore added resulting in a new image having the same name as the destroyed file.

def save_form_data(self, instance, data):
    if data and data[0]: # Replace file
#        self.delete_file(instance)
        super(RemovableFileField, self).save_form_data(instance, data[0])
    if data and data[1]: # Delete file
        self.delete_file(instance)
        setattr(instance, self.name, None)

#

Please login first before commenting.