dynamic file upload path with current instance id - django

I have a form which gets current logged in user, some inputs and a file:
class AddItemForm(ModelForm):
class Meta:
model = Item
exclude = ['user']
For this form a have a view:
item_form = AddItemForm(request.POST, request.FILES)
if item_form.is_valid():
item = item_form.save(commit=False)
item.user = request.user
item.save()
for this item's file field i am using upload_to feature. here is my modal:
class Item(models.Model):
user = models.ForeignKey(User)
cover_image = models.FileField(upload_to=get_upload_path)
def get_upload_path(instance, filename):
return "items/user_{user_id}/item_{item_id}/{filename}".format(user_id=instance.user.id, item_id=instance.id,filename=filename)
problem is that i can't see the current instance id in uploaded path because of following line:
item = item_form.save(commit=False)
it has not instance id yet and instead of current item id it create user_1/item_NONE/file
how can i set id to this path?
thanks in advance

Here I've found idea && code based on using the post_save signal, when object created move from temp directory to specified one in model class:
use_key and upload_to are optional. use_key defaults to False. If it is True then the id of the instance will be used as a prefix for the new file as there is the potential for overwriting now that we are moving the file. upload_to will simply define the temporary directory to upload the files to initially.
from django.db.models import ImageField, FileField, signals
from django.dispatch import dispatcher
from django.conf import settings
import shutil, os, glob, re
from distutils.dir_util import mkpath
class CustomImageField(ImageField):
"""Allows model instance to specify upload_to dynamically.
Model class should have a method like:
def get_upload_to(self, attname):
return 'path/to/{0}'.format(self.id)
"""
def __init__(self, *args, **kwargs):
kwargs['upload_to'] = kwargs.get('upload_to', 'tmp')
try:
self.use_key = kwargs.pop('use_key')
except KeyError:
self.use_key = False
super(CustomImageField, self).__init__(*args, **kwargs)
def contribute_to_class(self, cls, name):
"""Hook up events so we can access the instance."""
super(CustomImageField, self).contribute_to_class(cls, name)
dispatcher.connect(self._move_image, signal=signals.post_save, sender=cls)
def _move_image(self, instance=None):
"""
Function to move the temporarily uploaded image to a more suitable directory
using the model's get_upload_to() method.
"""
if hasattr(instance, 'get_upload_to'):
src = getattr(instance, self.attname)
if src:
m = re.match(r"%s/(.*)" % self.upload_to, src)
if m:
if self.use_key:
dst = "%s/%d_%s" % (instance.get_upload_to(self.attname), instance.id, m.groups()[0])
else:
dst = "%s/%s" % (instance.get_upload_to(self.attname), m.groups()[0])
basedir = "%s%s/" % (settings.MEDIA_ROOT, os.path.dirname(dst))
mkpath(basedir)
shutil.move("%s%s" % (settings.MEDIA_ROOT, src),"%s%s" % (settings.MEDIA_ROOT, dst))
setattr(instance, self.attname, dst)
instance.save()
def db_type(self):
"""Required by Django for ORM."""
return 'varchar(100)'
class Image(models.Model):
file = CustomImageField(use_key=True, upload_to='tmp')
def get_upload_to(self, attname):
return 'path/to/{0}'.format(self.id)

Just add this function to the model :
def save(self, *args, **kwargs):
if self.pk is None:
saved_image = self.cover_image
self.cover_image = None
super(Item, self).save(*args, **kwargs)
self.cover_image = saved_image
else:
super(Item, self).save(*args, **kwargs)

Updated to the new way of use signals for newer versions of django:
from django.db.models import ImageField, FileField, signals
from django.conf import settings
import shutil, os, glob, re
from distutils.dir_util import mkpath
class CustomImageField(ImageField):
"""Allows model instance to specify upload_to dynamically.
Model class should have a method like:
def get_upload_to(self, attname):
return 'path/to/{0}'.format(self.id)
"""
def __init__(self, *args, **kwargs):
kwargs['upload_to'] = kwargs.get('upload_to', 'tmp')
try:
self.use_key = kwargs.pop('use_key')
except KeyError:
self.use_key = False
super(CustomImageField, self).__init__(*args, **kwargs)
def contribute_to_class(self, cls, name):
"""Hook up events so we can access the instance."""
super(CustomImageField, self).contribute_to_class(cls, name)
signals.post_save.connect(self._move_image, sender=cls)
def _move_image(self, instance, **kwargs):
"""
Function to move the temporarily uploaded image to a more suitable directory
using the model's get_upload_to() method.
"""
if hasattr(instance, 'get_upload_to'):
src = getattr(instance, self.attname)
if src:
m = re.match(r"%s/(.*)" % self.upload_to, str(src))
if m:
if self.use_key:
dst = "%s/%d_%s" % (instance.get_upload_to(self.attname), instance.id, m.groups()[0])
else:
dst = "%s/%s" % (instance.get_upload_to(self.attname), m.groups()[0])
basedir = "%s/%s/" % (settings.MEDIA_ROOT, os.path.dirname(dst))
mkpath(basedir)
shutil.move("%s/%s" % (settings.MEDIA_ROOT, src),"%s/%s" % (settings.MEDIA_ROOT, dst))
setattr(instance, self.attname, dst)
instance.save()
def db_type(self):
"""Required by Django for ORM."""
return 'varchar(100)'

Related

RadioSelect widget does not show labels in MultiWidget

I am trying to build a custom MultiValue field in django that consists of two widgets: RadioSelect and TextInput: if a user chooses 'Other' then they can insert the value there.
Everything works, with one weird exception: the labels for radio buttons are not shown (see picture). Values are rendered ok, but the labels are just not there. What I am doing wrong?
fields.py
from .widgets import OtherSelectorWidget
class OtherModelField(models.CharField):
def __init__(self, *args, **kwargs):
self.inner_choices = kwargs.pop('choices', None)
super().__init__(*args, **kwargs)
def formfield(self, **kwargs):
return OtherFormField(choices=self.inner_choices, **kwargs)
class OtherFormField(MultiValueField):
def __init__(self, **kwargs):
self.choices = kwargs.pop('choices')
self.widget = OtherSelectorWidget(choices=self.choices)
fields = (CharField(), CharField(),)
super().__init__(fields=fields, require_all_fields=False, **kwargs)
def compress(self, data_list):
return str(data_list)
widgets.py
from datetime import date
from django.forms import widgets
class OtherSelectorWidget(widgets.MultiWidget):
def __init__(self, choices=None, attrs=None):
self.choices = choices
_widgets = (
widgets.RadioSelect(choices=choices),
widgets.TextInput(attrs=attrs),
)
super().__init__(_widgets, attrs)
def decompress(self, value):
if value:
return [value[0], value[1]]
return [None, None, ]
def format_output(self, rendered_widgets):
return ''.join(rendered_widgets)
def value_from_datadict(self, data, files, name):
datelist = [
widget.value_from_datadict(data, files, name + '_%s' % i)
for i, widget in enumerate(self.widgets)]
radio_data = self.widgets[0].value_from_datadict(data, files, name + '_0')
text_data = self.widgets[1].value_from_datadict(data, files, name + '_1')
try:
D = [radio_data, text_data]
except ValueError:
return ''
else:
return D
it seems to be a glitch in Django. Here is a link to a ticket: https://code.djangoproject.com/ticket/29200
I dealt with it by adding wrap_label to widget's context:
class OtherSelectorWidget(widgets.MultiWidget):
def get_context(self, name, value, attrs):
con = super().get_context(name, value, attrs)
con['wrap_label'] = True
return con
Then everything is rendered properly

POST not working Django Rest Framework

I find message return in google, not find.
Whats my code not post values correct?
I need help for solution correct.
As use form based generic views?
Im desenv an restAPI, i not understanding problem in my code, i running and return:
I retrieve message, flow.
views.py :
from snippets.models import Equipamento, Colaborador
from snippets.serializers import EquipamentoSerializer, ColaboradorSerializer
from rest_framework import mixins
from rest_framework import generics
class EquipamentoList(generics.ListCreateAPIView):
serializer_class = EquipamentoSerializer
def get_queryset(self):
queryset = Equipamento.objects.all()
id = self.request.query_params.get('id', None)
if id is not None:
queryset = queryset.filter(id=id)
return queryset
# class ColaboradorList(generics.CreateAPIView):
# queryset = Colaborador.objects.all()
# serializer_class = ColaboradorSerializer
# def get_queryset(self):
# queryset = Colaborador.objects.all()
# id = self.request.query_params.get('id', None)
# if id is not None:
# queryset = queryset.filter(pk=pk)
# return queryset
# def create(self, request, pk):
# queryset = Colaborador.objects.all()
# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# class ColaboradorDetail(generics.RetrieveUpdateDestroyAPIView):
# queryset = Colaborador.objects.all()
# serializer_class = ColaboradorSerializer
class ColaboradorList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Colaborador.objects.all()
serializer_class = ColaboradorSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
class ColaboradorDetail(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
queryset = Colaborador.objects.all()
serializer_class = ColaboradorSerializer
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
serializers.py
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from snippets.models import Equipamento, Colaborador, Propriedade, MotivoParada, Apontamento
class EquipamentoSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
cod_equip = serializers.IntegerField(validators=[UniqueValidator(queryset=Equipamento.objects.all())])
desc_equip = serializers.CharField(allow_blank=True, max_length=15, required=False)
def restore_object(self, attrs, instance=None):
"""
Create or update a new snippet instance, given a dictionary
of deserialized field values.
Note that if we don't define this method, then deserializing
data will simply return a dictionary of items.
"""
if instance:
# Update existing instance
instance.id = attrs.get('id', instance.id)
instance.cod_equip = attrs.get('cod_equip', instance.cod_equip)
instance.des_equip = attrs.get('desc_equip', instance.desc_equip)
return instance
# Create new instance
return Equipamento(**attrs)
class ColaboradorSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
cod_colab = serializers.IntegerField(validators=[UniqueValidator(queryset=Colaborador.objects.all())])
nome_colab = serializers.CharField(max_length=30)
def restore_object(self, attrs, instance=None):
"""
Create or update a new snippet instance, given a dictionary
of deserialized field values.
Note that if we don't define this method, then deserializing
data will simply return a dictionary of items.
"""
if instance:
# Update existing instance
instance.id = attrs.get('id', instance.id)
instance.cod_colab = attrs.get('cod_colab', instance.cod_colab)
instance.nome_colab = attrs.get('nome_colab', instance.nome_colab)
return instance
# Create new instance
return Colaborador(**attrs)
class ApontamentoSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
criado = serializers.DateTimeField(read_only=True)
apont_inicio = serializers.TimeField()
apont_fim = serializers.TimeField()
duracao = serializers.TimeField()
equipamento = serializers.PrimaryKeyRelatedField(queryset=Equipamento.objects.all())
colaborador = serializers.PrimaryKeyRelatedField(queryset=Colaborador.objects.all())
propriedade = serializers.PrimaryKeyRelatedField(queryset=Propriedade.objects.all())
m_parada = serializers.PrimaryKeyRelatedField(queryset=MotivoParada.objects.all())
def restore_object(self, attrs, instance=None):
"""
Create or update a new snippet instance, given a dictionary
of deserialized field values.
Note that if we don't define this method, then deserializing
data will simply return a dictionary of items.
"""
if instance:
# Update existing instance
instance.id = attrs.get('id', instance.id)
instance.criado = attrs.get('criado', instance.criado)
instance.apont_inicio = attrs.get('apont_inicio', instance.apont_inicio)
instance.apont_fim = attrs.get('apont_fim', instance.apont_fim)
instance.duracao = attrs.get('duracao', instance.duracao)
instance.equipamento = attrs.get('equipamento', instance.equipamento)
instance.colaborador = attrs.get('colaborador', instance.colaborador)
instance.propriedade = attrs.get('propriedade', instance.propriedade)
instance.m_parada = attrs.get('m_parada', instance.m_parada)
return instance
# Create new instance
return Apontamento(**attrs)
class PropriedadeSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
cod_prop = serializers.IntegerField(validators=[UniqueValidator(queryset=Propriedade.objects.all())])
desc_prop = serializers.CharField(max_length=30)
def restore_object(self, attrs, instance=None):
"""
Create or update a new snippet instance, given a dictionary
of deserialized field values.
Note that if we don't define this method, then deserializing
data will simply return a dictionary of items.
"""
if instance:
# Update existing instance
instance.id = attrs.get('id', instance.id)
instance.cod_prop = attrs.get('cod_prop', instance.cod_prop)
instance.des_prop = attrs.get('desc_prop', instance.desc_prop)
return instance
# Create new instance
return Propriedade(**attrs)
class MotivoParadaSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
cod_mparada = serializers.IntegerField(validators=[UniqueValidator(queryset=MotivoParada.objects.all())])
desc_mparada = serializers.CharField(max_length=30)
def restore_object(self, attrs, instance=None):
"""
Create or update a new snippet instance, given a dictionary
of deserialized field values.
Note that if we don't define this method, then deserializing
data will simply return a dictionary of items.
"""
if instance:
# Update existing instance
instance.id = attrs.get('id', instance.id)
instance.cod_mparada = attrs.get('cod_mparada', instance.cod_mparada)
instance.des_mparada = attrs.get('desc_mparada', instance.desc_mparada)
return instance
# Create new instance
return MotivoParada(**attrs)
urls.py
from django.conf.urls import url
# from snippets.views import EquipamentoList, ColaboradorList, ColaboradorDetail
from snippets import views
from rest_framework.urlpatterns import format_suffix_patterns
urlpatterns = [
# url(r'^snippets/$', views.snippet_list),
# url(r'^snippets/(?P<pk>[0-9]+)/$', views.snippet_detail),
# url('^equipamento/(?P<id>.+)/$', EquipamentoList.as_view()),
#url('^colab/(?P<id>.+)/$', ColaboradorList.as_view()),
url('^colab/$', views.ColaboradorList.as_view()),
url('^colab_add/(?P<pk>[0-9]+)/$', views.ColaboradorDetail.as_view()),
]
urlpatterns = format_suffix_patterns(urlpatterns)I find message return in google, not find solv problem?
Help.
Whats my code not post values?
I need help for solution correct.
The error message is pretty straightforward. Your serializers use the restore_object method, which is deprecated in Rest Framework 3. Either downgrade your Django Rest Framework version to 2.x, or (recommended) rewrite your code to make it compatible with Rest Framework's latest version.

How can I make all CharField in uppercase direct in model?

I tried to use UpperCase in all my CharField, in all my Django Model.
Today I have some code in my save method:
def save(self, *args, **kwargs):
for field_name in ['razao_social', 'nome_fantasia', 'cidade', 'endereco','bairro', 'uf', 'cli_parc_nomeparc', 'cli_repr_nomerepr']:
val = getattr(self, field_name, False)
if val:
setattr(self, field_name, val.upper())
super(Pessoa, self).save(*args, **kwargs)
But its take some time. There`s any method to put some uppercase=True in my models?
Thanks.
Here is how to override a Django Model Field and make it upper-case as of Django 1.8.
This will:
work by saving the upper-cased value to the database
returns an upper-cased value in the save response.
Here's the code:
from django.db import models
class UpperCaseCharField(models.CharField):
def __init__(self, *args, **kwargs):
super(UpperCaseCharField, self).__init__(*args, **kwargs)
def pre_save(self, model_instance, add):
value = getattr(model_instance, self.attname, None)
if value:
value = value.upper()
setattr(model_instance, self.attname, value)
return value
else:
return super(UpperCaseCharField, self).pre_save(model_instance, add)
If you want to do this in Django rest framework, here's the code:
from rest_framework import serializers
class UpperCaseSerializerField(serializers.CharField):
def __init__(self, *args, **kwargs):
super(UpperCaseSerializerField, self).__init__(*args, **kwargs)
def to_representation(self, value):
value = super(UpperCaseSerializerField, self).to_representation(value)
if value:
return value.upper()
The correct way would be to define custom model field:
from django.db import models
from django.utils.six import with_metaclass
class UpperCharField(with_metaclass(models.SubfieldBase, models.CharField)):
def __init__(self, *args, **kwargs):
self.is_uppercase = kwargs.pop('uppercase', False)
super(UpperCharField, self).__init__(*args, **kwargs)
def get_prep_value(self, value):
value = super(UpperCharField, self).get_prep_value(value)
if self.is_uppercase:
return value.upper()
return value
and use it like so:
class MyModel(models.Model):
razao_social = UpperCharField(max_length=50, uppercase=True)
# next field will not be upper-cased by default (it's the same as CharField)
nome_fantasia = UpperCharField(max_length=50)
# etc..
you also need to resolve south migration issues (if necessary), by adding this code:
from south.modelsinspector import add_introspection_rules
add_introspection_rules([
(
[UpperCharField],
[],
{
"uppercase": ["uppercase", {"default": False}],
},
),
], ["^myapp\.models\.UpperCharField"])
(path in the last line depends on the field class localization. Please read the south docs for explanation.)
Although there's a small downside when you use shell for instance to create model object and save it in variable:
my_object = MyModel.objects.create(razao_social='blah')
print my_object.razao_social
you won't get upper-cased value. You need to retrieve the object from the database. I will update this post, when I find out how to resolve this issue as well.
Instead of defining a custom field, you can also use the RegexValidator:
from django.core.validators import RegexValidator
...
my_field = models.CharField(
max_length=255,
validators=[RegexValidator('^[A-Z_]*$',
'Only uppercase letters and underscores allowed.')],
)
(see Docs)
Here is my dirty and easier solution without having to deal with migrations:
char_fields = [f.name for f in self._meta.fields if isinstance(f, models.CharField) and not getattr(f, 'choices')]
for f in char_fields:
val = getattr(self, f, False)
if val:
setattr(self, f, val.upper())
super(Cliente, self).save(*args, **kwargs)
django 4, just override the save() method of the model
from django.db import models
class MyModel(models.Model):
my_field = models.CharField(max_length=255)
def save(self, *args, **kwargs):
self.my_field = self.my_field.upper()
super().save(*args, **kwargs)

Django - Model field keeps the same value!

When I assign a value to an variable of a Field object, why when I reload the ModelForm isn't reassigned to default?
File
class CustomFile(ImageFile, FieldFile):
def save(self, name, content, save = True):
if self.field.override_name:
self(CustomFile, self).save(self.field.override_name, content, save = save)
else:
self(CustomFile, self).save(generate_name(self.instance, name), content, save = save)
Field
class CustomImageField(ImageField):
attr_class = CustomFile
def __init__(self, overrided_name, *args, **kwargs):
self.overrided_name = overrided_name
super(CustomImageField, self).__init__(*args, **kwargs)
Model
class Test(models.Model):
email = models.EmailField()
file = CustomImageField()
AdminForm
class TestForm(ModelForm):
def __init__(self, *args, **kwargs):
super(TestForm, self).__init__(*args, **kwargs)
self.old_instance = self.instance
Admin
class TestAdmin(Test):
form = TestForm
def save_model(self, request, obj, form, change):
if form.old_instance:
form.old_instance.file.delete(save = True)
form.old_instance.file.field.override_name = form.old_instance.name
obj.save()
admin.site.register(Test, TestAdmin)
My problem is that every image I will upload will have the same name, until I restart the server!..
Why the object doesn't change?! In particular the Field object... when I trace it will result the same object .
I've solved it like so:
class CustomFile(ImageFile, FieldFile):
def save(self, name, content, save = True):
if self.field.override_name:
self(CustomFile, self).save(self.field.override_name, content, save = save)
self.field.override_name = None
else:
self(CustomFile, self).save(generate_name(self.instance, name), content, save = save)
I'm using Django 1.2.6, Python 2.6 and Windows!
I am just guessing, but isnt this part of code wrong?
form = TestForm
Should not it be creating an instance instead of referencing to class?
form = TestForm()
or same thing here?
attr_class = CustomFile - > attr_class = CustomFile()

Dynamic File Path in Django

I'm trying to generate dynamic file paths in django. I want to make a file system like this:
-- user_12
--- photo_1
--- photo_2
--- user_ 13
---- photo_1
I found a related question : Django Custom image upload field with dynamic path
Here, they say we can change the upload_to path and leads to https://docs.djangoproject.com/en/stable/topics/files/ doc. In the documentation, there is an example :
from django.db import models
from django.core.files.storage import FileSystemStorage
fs = FileSystemStorage(location='/media/photos')
class Car(models.Model):
...
photo = models.ImageField(storage=fs)
But, still this is not dynamic, I want to give Car id to the image name, and I cant assign the id before Car definition completed. So how can I create a path with car ID ??
You can use a callable in the upload_to argument rather than using custom storage. See the docs, and note the warning there that the primary key may not yet be set when the function is called. This can happen because the upload may be handled before the object is saved to the database, so using ID might not be possible. You might want to consider using another field on the model such as slug. E.g:
import os
def get_upload_path(instance, filename):
return os.path.join(
"user_%d" % instance.owner.id, "car_%s" % instance.slug, filename)
then:
photo = models.ImageField(upload_to=get_upload_path)
You can use lambda function as below, take note that if instance is new then it won't have the instance id, so use something else:
logo = models.ImageField(upload_to=lambda instance, filename: 'directory/images/{0}/{1}'.format(instance.owner.id, filename))
https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.FileField.upload_to
def upload_path_handler(instance, filename):
return "user_{id}/{file}".format(id=instance.user.id, file=filename)
class Car(models.Model):
...
photo = models.ImageField(upload_to=upload_path_handler, storage=fs)
There is a warning in the docs, but it shouldn't affect you since we're after the User ID and not the Car ID.
In most cases, this object will not
have been saved to the database yet,
so if it uses the default AutoField,
it might not yet have a value for its
primary key field.
My solution is not elegant, but it works:
In the model, use a the standard function that will need the id/pk
def directory_path(instance, filename):
return 'files/instance_id_{0}/{1}'.format(instance.pk, filename)
in views.py save the form like this:
f=form.save(commit=False)
ftemp1=f.filefield
f.filefield=None
f.save()
#And now that we have crated the record we can add it
f.filefield=ftemp1
f.save()
It worked for me.
Note: My filefield in models and allowed for Null values. Null=True
Well very late to the party but this one works for me.
def content_file_name(instance, filename):
upload_dir = os.path.join('uploads',instance.albumname)
if not os.path.exists(upload_dir):
os.makedirs(upload_dir)
return os.path.join(upload_dir, filename)
Model like this only
class Album(models.Model):
albumname = models.CharField(max_length=100)
audiofile = models.FileField(upload_to=content_file_name)
There are two solutions on DjangoSnippets
Two-stage save: https://djangosnippets.org/snippets/1129/
Prefetch the ID (PostgreSQL only): https://djangosnippets.org/snippets/2731/
This guy has a way to do dynamic path. The idea is to set your favourite storage and customise "upload_to()" parameter with a function.
Hope this helps.
I found out a different solution, which is dirty, but it works. You should create a new dummy model, which is self synchronized with the original one. I'm not proud of this, but didn't find another solution. In my case I want to upload files, and store each in a directory named after the model id (because I'll generate there more files).
the model.py
class dummyexperiment(models.Model):
def __unicode__(self):
return str(self.id)
class experiment(models.Model):
def get_exfile_path(instance, filename):
if instance.id == None:
iid = instance.dummye.id
else:
iid = instance.id
return os.path.join('experiments', str(iid), filename)
exfile = models.FileField(upload_to=get_exfile_path)
def save(self, *args, **kwargs):
if self.id == None:
self.dummye = dummyexperiment()
self.dummye.save()
super(experiment, self).save(*args, **kwargs)
I'm very new in python and in django, but it seems like ok for me.
another solution:
def get_theme_path(instance, filename):
id = instance.id
if id == None:
id = max(map(lambda a:a.id,Theme.objects.all())) + 1
return os.path.join('experiments', str(id), filename)
As the primary key (id) may not be available if the model instance was not saved to the database yet, I wrote my FileField subclasses which move the file on model save, and 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))
MEDIA_ROOT/
/company_Company1/company.png
/shop_Shop1/shop.png
/bikes/bike.png
def photo_path_company(instance, filename):
# file will be uploaded to MEDIA_ROOT/company_<name>/
return 'company_{0}/{1}'.format(instance.name, filename)
class Company(models.Model):
name = models.CharField()
photo = models.ImageField(max_length=255, upload_to=photo_path_company)
def photo_path_shop(instance, filename):
# file will be uploaded to MEDIA_ROOT/company_<name>/shop_<name>/
parent_path = instance.company._meta.get_field('photo').upload_to(instance.company, '')
return parent_path + 'shop_{0}/{1}'.format(instance.name, filename)
class Shop(models.Model):
name = models.CharField()
photo = models.ImageField(max_length=255, upload_to=photo_path_shop)
def photo_path_bike(instance, filename):
# file will be uploaded to MEDIA_ROOT/company_<name>/shop_<name>/bikes/
parent_path = instance.shop._meta.get_field('photo').upload_to(instance.shop, '')
return parent_path + 'bikes/{0}'.format(filename)
class Bike(models.Model):
name = models.CharField()
photo = models.ImageField(max_length=255, upload_to=photo_path_bike)
You can override model's save method:
def save_image(instance, filename):
instance_id = f'{instance.id:03d}' # 001
return f'{instance_id}-{filename.lower()}' # 001-foo.jpg
class Resource(models.Model):
photo = models.ImageField(upload_to=save_image)
def save(self, *args, **kwargs):
if self.id is None:
photo = self.photo
self.photo = None
super().save(*args, **kwargs)
self.photo = photo
if 'force_insert' in kwargs:
kwargs.pop('force_insert')
super().save(*args, **kwargs)
The method will be
def user_directory_path(field_name):
def upload_path(instance, filename):
year = datetime.now().year
name, ext = instance.user, os.path.splitext(filename)[1]
return f'photos/{year}/{instance._meta.model_name}s/{instance.user}/{field_name}_{name}{ext}'
return upload_path
And in your models you can have as many ImageField as you like. example
photo = models.ImageField(upload_to=user_directory_path('photo'), null=True, blank=True,)
passport_photo = models.ImageField(upload_to=user_directory_path('passport_photo'), null=True, blank=True,)