簡體   English   中英

在 Django 管理員中過濾表單字段查詢集時如何不丟失數據

[英]How not to lose data when filtering formfield querysets in Django admin

概括

如果我們在 Django 管理表單上過濾選擇字段的查詢集,似乎我們必須非常小心不要排除該字段的現有值(在與表單關聯的 model 實例上)。

如果我們確實排除它們,現有值將被視為無效,這可能會在以后導致令人討厭的意外。

有沒有通用的方法來防止這種情況發生?

更多細節可以在下面找到。

背景

在 Django 管理表單上限制ForeignKeyManyToManyField的可用選擇(表示)的願望似乎相當普遍:

從上面的討論來看,有幾種不同的方法可以限制這些選擇,但大多數都歸結為在管理表單上過濾ModelChoiceField (或ModelMultipleChoiceField )的查詢集。

一般問題

以下是文檔ModelChoiceField.queryset的評價:

model 對象的 QuerySet,從中派生字段的選擇,並用於驗證用戶的選擇。 在呈現表單時對其進行評估。

如果您考慮這一點,實際上很容易用過濾器射中自己的腳,甚至一開始都沒有意識到。

例如,如果現有 model 實例上的ForeignKey字段(或ManyToManyField )的當前值意外地從查詢集中排除,這將使當前值無效。

根據您想要實現的目標,這可能是理想的行為,但也可能導致令人討厭的意外。

例子

為了說明這一點,請考慮以下過濾查詢集的示例,直接來自ModelAdmin.formfield_for_foreignkey()的 Django 文檔:

class MyModelAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "car":
            kwargs["queryset"] = Car.objects.filter(owner=request.user)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

基於這個例子,讓我們假設這是my_app的一部分,具有以下模型:

class Car(models.Model):
    owner = models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True, blank=True, 
        on_delete=models.CASCADE)


class MyModel(models.Model):
    car = models.ForeignKey(to=Car, null=True, blank=True, on_delete=models.CASCADE)

如果你在管理網站上玩這個,一開始一切似乎都很好。

但是,例如,如果有兩個(員工)用戶可以訪問同一個MyModel實例,會發生什么?

設想

假設我們有擁有car_xuser_a和擁有user_bcar_y

首先, user_a通過管理站點創建一個MyModel實例(即my_model ),並設置my_model.car = car_x

接下來, user_b想要通過管理站點更改相同的my_model

由於我們的查詢集過濾器, my_model.car的當前值,即car_xuser_b無效,因此他們只能從空值 ( --- ) 或car_y中進行選擇。

此外, user_b甚至不會知道my_model.car最初具有不同的值。

因此, my_model.car的值將在user_b保存表單后更改,而沒有人知道。

免責聲明:我知道這個特定的場景可以很容易地修復,例如通過阻止不同的用戶訪問同一個MyModel實例。 但是,這里的具體場景並不重要。 關鍵是,有無數種方法可以意外地從表單字段查詢集中排除現有值,並且在許多情況下,管理員用戶不會知道何時發生這種情況。

如果您的查詢集過濾器排除了通過MyModel管理頁面上的add another按鈕新創建的Car實例,則會出現類似的問題。 但是,在這種情況下,用戶至少會看到驗證錯誤。

問題

在我看來,很多人一定遇到過這類問題,但我一直無法在 SO 上找到任何類似的問題。

這讓我想知道:我是否以錯誤的方式接近這個? 如果我遇到這個問題,這是我的設計缺陷嗎?

如果沒有,是否有通用的方法來防止此類問題?

希望有更好的答案,這是我自己的嘗試:

據我所知,有一種解決方法,但我不確定這是否是最好的方法。

基本上,如果問題是現有值可能被排除在查詢集中,那么我們需要做的是確保顯式包含任何現有值。

以下是兩個(類似)實現,基於原始帖子中的示例(在 Django 2.2 中測試):

實現 A 使用了一個稍微定制的ModelForm ,它提供了一種訪問當前MyModel實例的自然方式,但這會破壞代碼:

class MyModelForm(ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # add the current car to the queryset (or empty queryset)
        self.fields["car"].queryset |= Car.objects.filter(
            id=self.instance.car_id)


class MyModelAdmin(admin.ModelAdmin):
    form = MyModelForm
    
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "car":
            kwargs["queryset"] = Car.objects.filter(owner=request.user)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

實現 B 使用一種稍微笨拙的方式從formfield_for_foreignkey()中訪問當前的MyModel實例,但將所有內容保存在一個地方:

class MyModelAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "car":
            # get the current value for the car field (if any)
            current_car_id = None
            current_my_model_id = request.resolver_match.kwargs.get('object_id')
            if current_my_model_id:
                my_model = MyModel.objects.get(id=current_my_model_id)
                current_car_id = my_model.car.id
            # combine the querysets
            qs_current_car = Car.objects.filter(id=current_car_id)
            qs_user_cars = Car.objects.filter(owner=request.user)
            kwargs['queryset'] = qs_user_cars | qs_current_car
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

也許我們也可以在MyModelForm.__init__()中進行所有查詢集過濾,但是我們需要將request破解到__init__()中,例如這里描述的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM