[英]Django: Access primary key in models.filefield(upload_to) location
I'd like to save my files using the primary key of the entry.我想使用条目的主键保存我的文件。
Here is my code:这是我的代码:
def get_nzb_filename(instance, filename):
if not instance.pk:
instance.save() # Does not work.
name_slug = re.sub('[^a-zA-Z0-9]', '-', instance.name).strip('-').lower()
name_slug = re.sub('[-]+', '-', name_slug)
return u'files/%s_%s.nzb' % (instance.pk, name_slug)
class File(models.Model):
nzb = models.FileField(upload_to=get_nzb_filename)
name = models.CharField(max_length=256)
I know the first time an object is saved the primary key isn't available, so I'm willing to take the extra hit to save the object just to get the primary key, and then continue on.我知道第一次保存 object 时主键不可用,所以我愿意采取额外的措施来保存 object 只是为了获取主键,然后继续。
The above code doesn't work.上面的代码不起作用。 It throws the following error:
它引发以下错误:
maximum recursion depth exceeded while calling a Python object
I'm assuming this is an infinite loop.我假设这是一个无限循环。 Calling the
save
method would call the get_nzb_filename
method, which would again call the save
method, and so on.调用
save
方法将调用get_nzb_filename
方法,该方法将再次调用save
方法,依此类推。
I'm using the latest version of the Django trunk.我正在使用最新版本的 Django 主干。
How can I get the primary key so I can use it to save my uploaded files?如何获取主键以便我可以使用它来保存我上传的文件?
Update @muhuk:更新@muhuk:
I like your solution.我喜欢你的解决方案。 Can you help me implement it?
你能帮我实现它吗? I've updated my code to the following and the error is
'File' object has no attribute 'create'
.我已将代码更新为以下内容,错误为
'File' object has no attribute 'create'
。 Perhaps I'm using what you've written out of context?也许我在使用你写的断章取义的东西?
def create_with_pk(self):
instance = self.create()
instance.save()
return instance
def get_nzb_filename(instance, filename):
if not instance.pk:
create_with_pk(instance)
name_slug = re.sub('[^a-zA-Z0-9]', '-', instance.name).strip('-').lower()
name_slug = re.sub('[-]+', '-', name_slug)
return u'files/%s_%s.nzb' % (instance.pk, name_slug)
class File(models.Model):
nzb = models.FileField(upload_to=get_nzb_filename, blank=True, null=True)
name = models.CharField(max_length=256)
Instead of enforcing the required field in my model I'll do it in my Form class.我不会在我的 model 中强制执行必填字段,而是在我的表格 class 中执行此操作。 No problem.
没问题。
You can do this by setting upload_to
to a temporary location and by creating a custom save method.您可以通过将
upload_to
设置为临时位置并创建自定义保存方法来执行此操作。
The save method should call super first, to generate the primary key (this will save the file to the temporary location). save 方法应该首先调用 super 来生成主键(这会将文件保存到临时位置)。 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.
再调用一次 super 以保存更改,您就可以使用 go。 This worked well for me when I came across this exact issue.
当我遇到这个确切的问题时,这对我很有用。
For example:例如:
class File( models.Model ):
nzb = models.FileField( upload_to='temp' )
def save( self, *args, **kwargs ):
# Call save first, to create a primary key
super( 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 one
if 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 changes
super( File, self ).save( *args, **kwargs )
It seems you'll need to pre-generate your File
models with empty file fields first.看来您需要先使用空文件字段预先生成
File
模型。 Then pick up one and save it with the given file object.然后拿起一个并将其与给定的文件 object 一起保存。
You can have a custom manager method like this;您可以拥有这样的自定义管理器方法;
def create_with_pk(self):
instance = self.create()
instance.save() # probably this line is unneeded
return 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.
由于您最初创建的是 null object,因此您无法在 model 级别上强制执行必填字段。
create_with_pk
is supposed to be a custom manager method , in your code it is just a regular method. create_with_pk
应该是一个自定义管理器方法,在您的代码中它只是一个常规方法。 Hence self
is meaningless.因此,
self
是没有意义的。 It is all properly documented with examples.所有这些都通过示例正确记录。
Had the same issue.有同样的问题。 Solved it attributing an id to the current object by saving the object first.
通过首先保存 object 解决了将 ID 分配给当前 object 的问题。
class Image(models.Model):
def upload_path(self, filename):
if not self.pk:
i = Image.objects.create()
self.id = self.pk = i.id
return "my/path/%s" % str(self.id)
file = models.ImageField(upload_to=upload_path)
You can use the next available primary key ID:您可以使用下一个可用的主键 ID:
class Document(models.Model):
def upload_path(self, filename):
if not self.pk:
document_next_id = Document.objects.order_by('-id').first().id + 1
self.id = self.pk = document_next_id
return "my/path/document-%s" % str(self.pk)
document = models.FileField(upload_to=upload_path)
Details细节
My example is a modification of @vinyll's answer , however, the problem Giles mentioned in his comment (two objects being created) is resolved here.我的示例是对@vinyll 答案的修改,但是, Giles 在他的评论中提到的问题(正在创建两个对象)在这里得到了解决。
I am aware that my answer is not perfect, and there can be issues with the "next available ID", eg, when more users will attempt to submit many forms at once.我知道我的答案并不完美,并且“下一个可用 ID”可能存在问题,例如,当更多用户尝试一次提交多个 forms 时。 Giles 's answer is more robust, mine is simpler (no need to generate temp files, then moving files, and deleting them).
Giles的答案更强大,我的更简单(无需生成临时文件,然后移动文件并删除它们)。 For simpler applications, this will be enough.
对于更简单的应用程序,这就足够了。
Also credits to Tjorriemorrie for the clear example on how to get the next available ID of an object.还感谢 Tjorriemorrie 提供了有关如何获取 object 的下一个可用 ID的清晰示例。
You can create pre_save and post_save signals.您可以创建 pre_save 和 post_save 信号。 Actual file saving will be in post_save, when pk is already created.
当 pk 已创建时,实际文件保存将在 post_save 中。 Do not forget to include signals in app.py so they work.
不要忘记在 app.py 中包含信号,以便它们工作。 Here is an example:
这是一个例子:
_UNSAVED_FILE_FIELD = 'unsaved_file'
@receiver(pre_save, sender=File)
def skip_saving_file_field(sender, instance: File, **kwargs):
if not instance.pk and not hasattr(instance, _UNSAVED_FILE_FIELD):
setattr(instance, _UNSAVED_FILE_FIELD, instance.image)
instance.nzb = None
@receiver(post_save, sender=File)
def save_file_field(sender, instance: Icon, created, **kwargs):
if created and hasattr(instance, _UNSAVED_FILE_FIELD):
instance.nzb = getattr(instance, _UNSAVED_FILE_FIELD)
instance.save()
Here are 2 possible solutions:这里有两种可能的解决方案:
id
before inserting a rowid
For simplicity I use postgresql db, although it is possible to adjust implementation for your db backend.为简单起见,我使用 postgresql db,尽管可以为您的 db 后端调整实现。
By default django creates id
as bigserial
(or serial
depending on DEFAULT_AUTO_FIELD
).默认情况下 django 创建
id
作为bigserial
(或serial
取决于DEFAULT_AUTO_FIELD
)。 For example, this model:例如,这个 model:
class File(models.Model):
nzb = models.FileField(upload_to=get_nzb_filename)
name = models.CharField(max_length=256)
Produces the following DDL:生成以下 DDL:
CREATE TABLE "example_file" ("id" bigserial NOT NULL PRIMARY KEY, "nzb" varchar(100) NOT NULL, "name" varchar(256) NOT NULL);
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)默认情况下
bigserial
以tablename_colname_seq
的形式创建序列名称(在我们的例子中为example_file_id_seq
)
The solution is to retrieve this id using nextval
:解决方案是使用
nextval
检索此 id:
def get_nextval(model, using=None):
seq_name = f"{model._meta.db_table}_id_seq"
if using is None:
using = "default"
with connections[using].cursor() as cursor:
cursor.execute("select nextval(%s)", [seq_name])
return cursor.fetchone()[0]
And set it before saving the model:并在保存 model 之前设置它:
class File(models.Model):
# fields definition
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
if not self.pk:
self.pk = get_nextval(self, using=using)
force_insert = True
super().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:请注意,我们依赖
force_insert
行为,因此请务必阅读文档并使用测试覆盖您的代码:
from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms import ModelForm
from django.test import TestCase
from example import models
class FileForm(ModelForm):
class Meta:
model = models.File
fields = (
"nzb",
"name",
)
class FileTest(TestCase):
def test(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")
nzt
then update with actual nzt
valuenzt
的情况下插入,然后用实际nzt
值更新The idea is self-explanatory - we basically pop nzt
on the object creation and save object again after we know id
:这个想法是不言自明的——我们基本上在 object 创建时弹出
nzt
并在我们知道id
后再次保存 object :
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
nzb = None
if not self.pk:
nzb = self.nzb
self.nzb = None
super().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}""",
)
Well I'm not sure of my answer but -好吧,我不确定我的答案,但是-
use nested models, if you can -如果可以的话,使用嵌套模型 -
class File(models.Model):
name = models.CharField(max_length=256)
class FileName(models.Model):
def get_nzb_filename(instance, filename):
return instance.name
name = models.ForeignKey(File)
nzb = models.FileField(upload_to=get_nzb_filename)
And in create method -在创建方法中 -
File_name = validated_data.pop(file_name_data)
file = File.objects.create(validated_data)
F = FileName.objects.create(name=file, **File_name)
Try this,尝试这个,
def get_nzb_filename(instance, filename):
pk = instance.pk or File.objects.all().latest().pk + 1
name_slug = re.sub('[^a-zA-Z0-9]', '-', instance.name).strip('-').lower()
name_slug = re.sub('[-]+', '-', name_slug)
return u'files/%s_%s.nzb' % (pk, name_slug)
Here we are simply fetching the pk of the instance, in case it's not available yet we query the database to fetch the latest instance and increment it by 1.在这里,我们只是获取实例的 pk,以防它尚不可用,我们查询数据库以获取最新的实例并将其增加 1。
pk = instance.pk or File.objects.all().latest().pk + 1
Ty, is there a reason you rolled your own slugify filter?泰,你有理由推出自己的 slugify 过滤器吗?
Django ships with a built-in slugify
filter, you can use it like so: Django 带有内置的
slugify
过滤器,您可以像这样使用它:
from django.template.defaultfilters import slugify
slug = slugify(some_string)
Not sure if you were aware it was available to use...不确定您是否知道它可以使用...
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.