Django FileField storage option

Mihai Zamfir picture Mihai Zamfir · Mar 28, 2014 · Viewed 7.8k times · Source

I have this model:

class UserProfile(models.Model):
    #..........
    photo = models.ImageField(upload_to = get_upload_file_name,
                              storage = OverwriteStorage(),
                              blank = True, null = True,
                              height_field = 'photo_height',
                              width_field = 'photo_width')

And this is my storage function:

class OverwriteStorage(FileSystemStorage):
    def _save(self, name, content):
        self.delete(r'.*')
        return super(OverwriteStorage, self)._save(name, content)

    def get_available_name(self, name):
        return name

How can I do the following 2 things:

  1. Whenever a user uploads a file (i.e. an image), I want to delete the old one, no matter if the name is the same or not. I tried to delete anything that matches the regex above, but this is not working.

  2. If the user uploads an image called "me.jpg" I want to rename it differently, depending on the user username for example. So I will do something like return super(OverwriteStorage, self)._save(SOMETHING_ELSE_HERE, content) How to do this? can I pass an additional parameter to the OverwriteStorage funcion?

And an additional third question: I've created a ModelForm for this form. So a user can upload an image. So when someone presses 'choose file', a windows window pops up in order to browse and choose a photo. How can I only display certain files here? (eg. only .jpg and .jpeg files)

Thanks!

EDIT: the get_upload_file_name function

def get_upload_file_name(instance, filename):
    return "%s/%s/profile_photo/%s" % (instance.user.username[0].lower(), instance.user.username, filename)

EDIT2: I have included my models.py

import datetime
import os
import urllib2, urlparse
import re

from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill
from django.core.files.storage import FileSystemStorage
from django.contrib.staticfiles import finders
from django.conf import settings
from django.core.files.base import ContentFile
from django.forms import widgets

now = datetime.datetime.now()

def get_upload_file_name(instance, filename):
    now = datetime.datetime.now()

    file_name = str(now.year)  + '_' + \
                str(now.month) + '_' + \
                str(now.day)   + '_' + \
                str(now.hour)  + '_' + \
                str(now.minute)+ '_' + \
                str(now.second)+ '.' + \
                filename.split('.')[-1]

    return "%s/%s/profile_photo/%s" % (instance.user.username[0].lower(),
                                       instance.user.username,
                                       file_name)

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

class UserProfileManager(models.Manager):

    def create_user_profile(self, user):
        user_profile = self.create(user = user)
        return user_profile

class UserProfile(models.Model):

    ### it is now.year - 13 because legitimate persons on this website should be over 14 years old
    YEARS = tuple(
                  zip([format(x,'04d') for x in range(now.year-120, now.year-13)],
                      [format(x,'04d') for x in range(now.year-120, now.year-13)]
                      )
                  )
    MONTHS = (
              ('January','January'),('February','February'),('March','March'),('April','April'),
              ('May','May'), ('June','June'),('July','July'),('August','August'),
              ('September','September'),('October','October'),('November','November'), ('December', 'December')

             )
    GENDERS = (('M', 'Male'), ('F', 'Female'))

    user = models.OneToOneField(User, related_name = 'MoreAboutUser', unique=True, verbose_name=_('user'))
    year_of_birth = models.CharField(max_length=10, blank = True,  null = True, choices=YEARS)
    month_of_birth = models.CharField(max_length=10, blank = True,  null = True, choices=MONTHS)
    gender = models.CharField(max_length=1, blank = True,  null = True, choices=GENDERS)
    photo = models.ImageField(upload_to = get_upload_file_name,
                              blank = True, null = True,
                              height_field = 'photo_height',
                              width_field = 'photo_width',
                              #widget = widgets.FileInput(attrs={'accept': 'image/gif,image/png,image/jpeg'})
                              )
    photo_height = models.PositiveIntegerField(blank = True, default = 0)
    photo_width = models.PositiveIntegerField(blank = True, default = 0)
    creation_time = models.DateTimeField(auto_now_add = True, auto_now = False)
    update_time = models.DateTimeField(auto_now_add = False, auto_now = True)

    class Meta:
            verbose_name = _('user profile')
            verbose_name_plural = _('user profiles')

    def __unicode__(self):
        return self.user.username

    objects = UserProfileManager()

    def get_profile_photo_url(self):
        if self.photo and hasattr(self.photo, 'url'):
            return self.photo.url
        else:
            return '/static/images/generic_profile_photo.jpg'

def create_user_profile(sender, instance, created, **kwargs):

    if created:
        try:
            profile = UserProfile.objects.create_user_profile(user = instance)
            profile.save()
        except:
            pass

post_save.connect(create_user_profile, sender=User)

Answer

lanzz picture lanzz · Apr 4, 2014

The storage API doesn't know anything about your model, so it cannot take other fields' values in consideration — it doesn't know the name of the old file stored in that field, nor it knows which user owns the model record.

You seem on the right track with providing an upload_to = get_upload_file_name option to your ImageField; the get_upload_file_name function (which you haven't posted in your question) will be able to construct the user-based filename for the image, as it gets a reference to the model instance, so it knows the user who owns the model instance.

As for the deletion of the old file, you will likely need to implement that in your model's save() method. It is too late at that point to find the old filename from your existing model instance, as its photo field will be already updated with its new value; still you can retrieve a copy of the existing record from the database and delete the old file through it. Here is an example implementation:

class UserProfile(models.Model):

    ...

    def save(self, *args, **kwargs):

        # grab a copy of the old record from the database
        oldrec = type(self).objects.get(pk=self.pk)

        # save the current instance, and keep the result
        result = super(UserProfile, self).save(*args, **kwargs)

        # if the path is the same, the file is overwritten already
        # and no deletion is necessary
        if oldrec.photo.path != self.photo.path:
            oldrec.photo.delete()

        # return the result received from the parent save() method
        return result

To specify acceptable types for your file selection, you will need to provide an accept attribute to the <input> tag that will be rendered for your field; you'll need to override the widget in a custom form.