[英]Session wide filtering of querysets in Django admin for super user
[英]How not to lose data when filtering formfield querysets in Django admin
如果我們在 Django 管理表單上過濾選擇字段的查詢集,似乎我們必須非常小心不要排除該字段的現有值(在與表單關聯的 model 實例上)。
如果我們確實排除它們,現有值將被視為無效,這可能會在以后導致令人討厭的意外。
有沒有通用的方法來防止這種情況發生?
更多細節可以在下面找到。
在 Django 管理表單上限制ForeignKey
或ManyToManyField
的可用選擇(表示)的願望似乎相當普遍:
從上面的討論來看,有幾種不同的方法可以限制這些選擇,但大多數都歸結為在管理表單上過濾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_x
的user_a
和擁有user_b
的car_y
。
首先, user_a
通過管理站點創建一個MyModel
實例(即my_model
),並設置my_model.car = car_x
。
接下來, user_b
想要通過管理站點更改相同的my_model
。
由於我們的查詢集過濾器, my_model.car
的當前值,即car_x
對user_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.