Django: Access Primary Key In Models.filefield(upload_to) Location
Solution 1:
It seems you'll need to pre-generate your File
models with empty file fields first. Then pick up one and save it with the given file object.
You can have a custom manager method like this;
defcreate_with_pk(self):
instance = self.create()
instance.save() # probably this line is unneededreturn instance
But this will be troublesome if either of your fields is required. Because you are initially creating a null object, you can't enforce required fields on the model level.
EDIT
create_with_pk
is supposed to be a custom manager method, in your code it is just a regular method. Hence self
is meaningless. It is all properly documented with examples.
Solution 2:
You can do this by setting upload_to
to a temporary location and by creating a custom save method.
The save method should call super first, to generate the primary key (this will save the file to the temporary location). Then you can rename the file using the primary key and move it to it's proper location. Call super one more time to save the changes and you are good to go! This worked well for me when I came across this exact issue.
For example:
classFile( models.Model ):
nzb = models.FileField( upload_to='temp' )
defsave( self, *args, **kwargs ):
# Call save first, to create a primary keysuper( File, self ).save( *args, **kwargs )
nzb = self.nzb
if nzb:
# Create new filename, using primary key and file extension
oldfile = self.nzb.name
dot = oldfile.rfind( '.' )
newfile = str( self.pk ) + oldfile[dot:]
# Create new file and remove old oneif newfile != oldfile:
self.nzb.storage.delete( newfile )
self.nzb.storage.save( newfile, nzb )
self.nzb.name = newfile
self.nzb.close()
self.nzb.storage.delete( oldfile )
# Save again to keep changessuper( File, self ).save( *args, **kwargs )
Solution 3:
Context
Had the same issue. Solved it attributing an id to the current object by saving the object first.
Method
- create a custom upload_to function
- detect if object has pk
- if not, save instance first, retrieve the pk and assign it to the object
- generate your path with that
Sample working code :
classImage(models.Model):
defupload_path(self, filename):
ifnot self.pk:
i = Image.objects.create()
self.id = self.pk = i.idreturn"my/path/%s" % str(self.id)
file = models.ImageField(upload_to=upload_path)
Solution 4:
You can create pre_save and post_save signals. Actual file saving will be in post_save, when pk is already created. Do not forget to include signals in app.py so they work. Here is an example:
_UNSAVED_FILE_FIELD = 'unsaved_file'@receiver(pre_save, sender=File)defskip_saving_file_field(sender, instance: File, **kwargs):
ifnot instance.pk andnothasattr(instance, _UNSAVED_FILE_FIELD):
setattr(instance, _UNSAVED_FILE_FIELD, instance.image)
instance.nzb = None@receiver(post_save, sender=File)defsave_file_field(sender, instance: Icon, created, **kwargs):
if created andhasattr(instance, _UNSAVED_FILE_FIELD):
instance.nzb = getattr(instance, _UNSAVED_FILE_FIELD)
instance.save()
Solution 5:
Here are 2 possible solutions:
Retrieve id
before inserting a row
For simplicity I use postgresql db, although it is possible to adjust implementation for your db backend.
By default django creates id
as bigserial
(or serial
depending on DEFAULT_AUTO_FIELD
). For example, this model:
classFile(models.Model):
nzb = models.FileField(upload_to=get_nzb_filename)
name = models.CharField(max_length=256)
Produces the following DDL:
CREATETABLE "example_file" ("id" bigserial NOTNULLPRIMARY KEY, "nzb" varchar(100) NOTNULL, "name" varchar(256) NOTNULL);
There is no explicit sequence specification. By default bigserial
creates sequence name in the form of tablename_colname_seq
(example_file_id_seq
in our case)
The solution is to retrieve this id using nextval
:
def get_nextval(model, using=None):
seq_name = f"{model._meta.db_table}_id_seq"
if usingisNone:
using= "default"
with connections[using].cursor() ascursor:
cursor.execute("select nextval(%s)", [seq_name])
return cursor.fetchone()[0]
And set it before saving the model:
classFile(models.Model):
# fields definitiondefsave(
self, force_insert=False, force_update=False, using=None, update_fields=None):
ifnot self.pk:
self.pk = get_nextval(self, using=using)
force_insert = Truesuper().save(
force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields,
)
Note that we rely on force_insert
behavior, so make sure to read documentation and cover your code with tests:
from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms import ModelForm
from django.test import TestCase
from example import models
classFileForm(ModelForm):
classMeta:
model = models.File
fields = (
"nzb",
"name",
)
classFileTest(TestCase):
deftest(self):
form = FileForm(
{
"name": "picture",
},
{
"nzb": SimpleUploadedFile("filename", b"content"),
},
)
self.assertTrue(form.is_valid())
form.save()
self.assertEqual(models.File.objects.count(), 1)
f = models.File.objects.first()
self.assertRegexpMatches(f.nzb.name, rf"files/{f.pk}_picture(.*)\.nzb")
Insert without nzt
then update with actual nzt
value
The idea is self-explanatory - we basically pop nzt
on the object creation and save object again after we know id
:
defsave(
self, force_insert=False, force_update=False, using=None, update_fields=None):
nzb = Noneifnot self.pk:
nzb = self.nzb
self.nzb = Nonesuper().save(
force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields,
)
if nzb:
self.nzb = nzb
super().save(
force_insert=False,
force_update=True,
using=using,
update_fields=["nzb"],
)
Test is updated to check actual queries:
def test(self):
form = FileForm(
{
"name": "picture",
},
{
"nzb": SimpleUploadedFile("filename", b"content"),
},
)
self.assertTrue(form.is_valid())
with CaptureQueriesContext(connection) as ctx:
form.save()
self.assertEqual(models.File.objects.count(), 1)
f = models.File.objects.first()
self.assertRegexpMatches(f.nzb.name, rf"files/{f.pk}_picture(.*)\.nzb")
self.assertEqual(len(ctx.captured_queries), 2)
insert, update = ctx.captured_queries
self.assertEqual(
insert["sql"],
'''INSERT INTO "example_file" ("nzb", "name") VALUES ('', 'picture') RETURNING "example_file"."id"''',
)
self.assertRegexpMatches(
update["sql"],
rf"""UPDATE "example_file" SET "nzb" = 'files/{f.pk}_picture(.*)\.nzb' WHERE "example_file"."id" = {f.pk}""",
)
Post a Comment for "Django: Access Primary Key In Models.filefield(upload_to) Location"