简体   繁体   English

Django:访问models.filefield(upload_to)位置中的主键

[英]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 级别上强制执行必填字段。

EDIT编辑

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.所有这些都通过示例正确记录

Context语境

Had the same issue.有同样的问题。 Solved it attributing an id to the current object by saving the object first.通过首先保存 object 解决了将 ID 分配给当前 object 的问题。

Method方法

  1. create a custom upload_to function创建自定义 upload_to function
  2. detect if object has pk检测 object 是否有 pk
  3. if not, save instance first, retrieve the pk and assign it to the object如果没有,请先保存实例,检索 pk 并将其分配给 object
  4. generate your path with that用它生成你的路径

Sample working code:示例工作代码:

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:这里有两种可能的解决方案:

Retrieve id before inserting a row在插入行之前检索id

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)默认情况下bigserialtablename_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")

Insert without nzt then update with actual nzt value在没有nzt的情况下插入,然后用实际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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM