Krizsán Balazs
Krizsán Balazs

Reputation: 385

Django admin file upload with current model id

I'm trying to create a simple photo gallery with the default Django admin. I'd like to save a sample photo for each gallery, but I don't want to keep the filname. Instead of the filename, I'd like to save the id of the model (N.jpg). But the first time I want to save the object the id does not exist. How could I know the next auto increment in the model, or somehow save the model data before the upload with super.save and after upload the file when self.id is exists? Is there a cool solution?

Something like this:

def upload_path_handler(instance, filename):
    ext = filename extension
    return "site_media/images/gallery/{id}.{ext}".format(id=instance.nextincrement, ext=ext)

class Gallery(models.Model):
    name  = models.TextField()
    image = models.FileField(upload_to=upload_path_handler)

And maybe store the filename in a different field.

Upvotes: 20

Views: 12482

Answers (6)

Peter Nasser Mosaad
Peter Nasser Mosaad

Reputation: 1

you can create a model instance by passing cleaned data from your form as **kwargs to django model i did it that way & its much easier than anything else

in your views post method add this (this code is from my project not adapted to this question)

    pk = request.session['_auth_user_id']
    user_obj = User.objects.get(pk=pk)


    lab_form_instance = lab_form(request.POST,request.FILES)
    lab_form_instance.save(commit=False)
    # here you can put the form.is_valid() statement
    lab_form_instance.cleaned_data['owner'] =user_obj # here iam adding additional needed data for the model
    obj = lab(**lab_form_instance.cleaned_data)
    obj.save()

*django==4.1

Upvotes: 0

Jatin Goyal
Jatin Goyal

Reputation: 497

For Django 2.2, follow the below code.

def save(self, *args, **kwargs):
    if self.pk is None:
        saved_image = self.image
        self.image = None
        super(Gallery, self).save(*args, **kwargs)
        self.image = saved_image
        if 'force_insert' in kwargs:
            kwargs.pop('force_insert')

    super(Gallery, self).save(*args, **kwargs)

Add the above code snippet to your 'class Gallery'.

P.S.: This will work for DRF as well when you save via views.py. Note that second if (condition) is required for DRF.

Upvotes: 4

Louis
Louis

Reputation: 151370

I ran into the same problem. Okm's answer sent me on the right path but it seems to me it is possible to get the same functionality by just overriding the save() method of your Model.

def save(self, *args, **kwargs):
    if self.pk is None:
        saved_image = self.image
        self.image = None
        super(Material, self).save(*args, **kwargs)
        self.image = saved_image

    super(Material, self).save(*args, **kwargs)

This definitely saves the information correctly.

Upvotes: 45

john.don83
john.don83

Reputation: 123

Using Louis's answer, here is recipe to process all FileField in the model:

class MyModel(models.Model):

    file_field = models.FileField(upload_to=upload_to, blank=True, null=True)

    def save(self, *args, **kwargs):
        if self.id is None:
            saved = []
            for f in self.__class__._meta.get_fields():
                if isinstance(f, models.FileField):
                    saved.append((f.name, getattr(self, f.name)))
                    setattr(self, f.name, None)

            super(self.__class__, self).save(*args, **kwargs)

            for name, val in saved:
                setattr(self, name, val)
        super(self.__class__, self).save(*args, **kwargs)

Upvotes: 1

Kukosk
Kukosk

Reputation: 3012

In django 1.7, the proposed solutions didn't seem to work for me, so I wrote my FileField subclasses along with a storage subclass which removes the old files.

Storage:

class OverwriteFileSystemStorage(FileSystemStorage):
    def _save(self, name, content):
        self.delete(name)
        return super()._save(name, content)

    def get_available_name(self, name):
        return name

    def delete(self, name):
        super().delete(name)

        last_dir = os.path.dirname(self.path(name))

        while True:
            try:
                os.rmdir(last_dir)
            except OSError as e:
                if e.errno in {errno.ENOTEMPTY, errno.ENOENT}:
                    break

                raise e

            last_dir = os.path.dirname(last_dir)

FileField:

def tweak_field_save(cls, field):
    field_defined_in_this_class = field.name in cls.__dict__ and field.name not in cls.__bases__[0].__dict__

    if field_defined_in_this_class:
        orig_save = cls.save

        if orig_save and callable(orig_save):
            assert isinstance(field.storage, OverwriteFileSystemStorage), "Using other storage than '{0}' may cause unexpected behavior.".format(OverwriteFileSystemStorage.__name__)

            def save(self, *args, **kwargs):
                if self.pk is None:
                    orig_save(self, *args, **kwargs)

                    field_file = getattr(self, field.name)

                    if field_file:
                        old_path = field_file.path
                        new_filename = field.generate_filename(self, os.path.basename(old_path))
                        new_path = field.storage.path(new_filename)
                        os.makedirs(os.path.dirname(new_path), exist_ok=True)
                        os.rename(old_path, new_path)
                        setattr(self, field.name, new_filename)

                    # for next save
                    if len(args) > 0:
                        args = tuple(v if k >= 2 else False for k, v in enumerate(args))

                    kwargs['force_insert'] = False
                    kwargs['force_update'] = False

                orig_save(self, *args, **kwargs)

            cls.save = save


def tweak_field_class(orig_cls):
    orig_init = orig_cls.__init__

    def __init__(self, *args, **kwargs):
        if 'storage' not in kwargs:
            kwargs['storage'] = OverwriteFileSystemStorage()

        if orig_init and callable(orig_init):
            orig_init(self, *args, **kwargs)

    orig_cls.__init__ = __init__

    orig_contribute_to_class = orig_cls.contribute_to_class

    def contribute_to_class(self, cls, name):
        if orig_contribute_to_class and callable(orig_contribute_to_class):
            orig_contribute_to_class(self, cls, name)

        tweak_field_save(cls, self)

    orig_cls.contribute_to_class = contribute_to_class

    return orig_cls


def tweak_file_class(orig_cls):
    """
    Overriding FieldFile.save method to remove the old associated file.
    I'm doing the same thing in OverwriteFileSystemStorage, but it works just when the names match.
    I probably want to preserve both methods if anyone calls Storage.save.
    """

    orig_save = orig_cls.save

    def new_save(self, name, content, save=True):
        self.delete(save=False)

        if orig_save and callable(orig_save):
            orig_save(self, name, content, save=save)

    new_save.__name__ = 'save'
    orig_cls.save = new_save

    return orig_cls


@tweak_file_class
class OverwriteFieldFile(models.FileField.attr_class):
    pass


@tweak_file_class
class OverwriteImageFieldFile(models.ImageField.attr_class):
    pass


@tweak_field_class
class RenamedFileField(models.FileField):
    attr_class = OverwriteFieldFile


@tweak_field_class
class RenamedImageField(models.ImageField):
    attr_class = OverwriteImageFieldFile

and my upload_to callables look like this:

def user_image_path(instance, filename):
    name, ext = 'image', os.path.splitext(filename)[1]

    if instance.pk is not None:
        return os.path.join('users', os.path.join(str(instance.pk), name + ext))

    return os.path.join('users', '{0}_{1}{2}'.format(uuid1(), name, ext))

Upvotes: 0

okm
okm

Reputation: 23871

The image file gets saved before Gallery instance. So you have to split the saving to two phases by using signals w/ Gallery instance itself carrying the state:

from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver

_UNSAVED_FILEFIELD = 'unsaved_filefield'

@receiver(pre_save, sender=Image)
def skip_saving_file(sender, instance, **kwargs):
    if not instance.pk and not hasattr(instance, _UNSAVED_FILEFIELD):
        setattr(instance, _UNSAVED_FILEFIELD, instance.image)
        instance.image = None

@receiver(post_save, sender=Image)
def save_file(sender, instance, created, **kwargs):
    if created and hasattr(instance, _UNSAVED_FILEFIELD):
        instance.image = getattr(instance, _UNSAVED_FILEFIELD)
        instance.save()        
        # delete it if you feel uncomfortable...
        # instance.__dict__.pop(_UNSAVED_FILEFIELD)

The upload_path_handler looks like

def upload_path_handler(instance, filename):
    import os.path
    fn, ext = os.path.splitext(filename)
    return "site_media/images/gallery/{id}{ext}".format(id=instance.pk, ext=ext)

I suggest using ImageField instead of FileField for type-checking if the field is for image uploading only. Also, you may want to normalize filename extension (which is unnecessary because of the mimetype) like

def normalize_ext(image_field):
    try:
        from PIL import Image
    except ImportError:
        import Image
    ext = Image.open(image_field).format
    if hasattr(image_field, 'seek') and callable(image_field.seek):
       image_field.seek(0)
    ext = ext.lower()
    if ext == 'jpeg':
        ext = 'jpg'
    return '.' + ext

Upvotes: 11

Related Questions