所以......我很欣赏@alfonso.kim 的回答,但是仅仅为了“渲染”目的而创建一个全新的 Django 模型的想法对我来说听起来有点矫枉过正。请!不要误会我的意思:这可能是“规范”的做法(我已经多次看到这种方法被推荐)并且可能比我所做的更好,但我想展示我是如何解决我的特定问题的:
我查看了 Django 的源代码,特别ManyToMany
是在 Admin 中如何显示关系。如果您查看我上面的原始问题,我想弄清楚 Django在编辑一个产品时使用哪个类来显示类别(那个“双列选择”,给它一个名字,我非常喜欢)。事实证明它是一个django.forms.models.ModelMultipleChoiceField,“经验丰富”,带有FilteredSelectMultiple小部件的提示。
有了这些信息,我为我的班级创建了一个自定义管理表单Coupon
,手动添加了我想要显示的字段:
class CouponAdminForm(forms.ModelForm):
brands = forms.ModelMultipleChoiceField(
queryset=Brand.objects.all().order_by('name'),
required=False,
widget=FilteredSelectMultiple("Brands", is_stacked=False))
categories = forms.ModelMultipleChoiceField(
queryset=Category.objects.all().order_by('name'),
required=False,
widget=FilteredSelectMultiple("Categories", is_stacked=False))
products = forms.ModelMultipleChoiceField(
queryset=Product.objects.all().order_by('name'),
required=False,
widget=FilteredSelectMultiple("Products", is_stacked=False))
def __init__(self, *args, **kwargs):
# ... we'll get back to this __init__ in a second ...
class Meta:
model = Coupon
exclude = ('filter_by',) # Exclude because we're gonna build this field manually
然后告诉ModelAdmin
班级我的优惠券使用该表格而不是默认表格:
class CouponsAdmin(admin.ModelAdmin):
form = CouponAdminForm
# ... #
admin.site.register(Coupon, CouponsAdmin)
这样做会在公式的根部brand
显示三个表单的手动添加字段(categories
和products
) 。换句话说:这产生了三个与我模型中的其他字段处于同一级别的新字段。但是:它们并不是真正的“一流”字段,因为它们实际上要确定我的模型(字段)中一个特定字段的内容,让我们记住,它或多或少像一个字典:Coupon
Coupon.filter_by
filter_by = {
"brands": [2, 3],
"categories": [7]
}
为了让使用 Admin 网页的人清楚地知道这三个字段并不是 Coupon 模型中“真正的”第一级字段,我决定将它们分组显示。
为此,我需要更改CouponsAdmin
字段的布局。我不希望这种分组影响我Coupon
模型的其他字段的显示方式,即使后来将新字段添加到模型中,所以我让表单的所有其他字段保持不变(换句话说:只应用特殊/分组布局到表单中的 和 字段brands
)categories
。products
令我惊讶的是,我无法在ModelForm
课堂上做到这一点。我不得不去ModelAdmin
(我真的不知道为什么......):
class CouponsAdmin(admin.ModelAdmin):
def get_fieldsets(self, request, obj=None):
fs = super(CouponsAdmin, self).get_fieldsets(request, obj)
# fs now contains only [(None, {'fields': fields})] meaning, ungrouped fields
filter_by_special_fields = (brands', 'categories', 'products')
retval = [
# Let every other field in the model at the root level
(None, {'fields': [f for f in fs[0][1]['fields']
if f not in filter_by_special_fields]
}),
# Now, let's create the "custom" grouping:
('Filter By', {
'fields': ('brands', 'categories', 'products')
})
]
return retval
form = CouponAdminForm
关于这里的更多信息fieldsets
那成功了:
现在,当管理员用户Coupon
通过此表单创建一个新表单时(换句话说:当用户单击页面上的“保存”按钮时)我将获得一个查询集,用于我在自定义表单中声明的额外字段(一个查询集用于brands
, 另一个用于categories
, 另一个用于products
) 但我实际上需要将该信息转换为字典。我能够通过覆盖save
Model's Form的方法来实现这一点:
class CouponAdminForm(forms.ModelForm):
brands = forms.ModelMultipleChoiceField(queryset=Brand.objects.all().order_by('name'),
required=False,
widget=FilteredSelectMultiple("Brands", is_stacked=False))
categories = forms.ModelMultipleChoiceField(queryset=Category.objects.all().order_by('name'),
required=False,
widget=FilteredSelectMultiple("Categories", is_stacked=False))
products = forms.ModelMultipleChoiceField(queryset=Product.objects.all().order_by('name'),
required=False,
widget=FilteredSelectMultiple("Products", is_stacked=False))
def __init__(self, *args, **kwargs):
# ... Yeah, yeah!! Not yet, not yet...
def save(self, commit=True):
filter_by_qsets = {}
for key in ['brands', 'categories', 'products']:
val = self.cleaned_data.pop(key, None) # The key is always gonna be in 'cleaned_data',
# even if as an empty query set, so providing a default is
# kind of... useless but meh... just in case
if val:
filter_by_qsets[key] = val # This 'val' is still a queryset
# Manually populate the coupon's instance filter_by dictionary here
self.instance.filter_by = {key: list(val.values_list('id', flat=True).order_by('id'))
for key, val in filter_by_qsets.items()}
return super(CouponAdminForm, self).save(commit=commit)
class Meta:
model = Coupon
exclude = ('filter_by',)
这正确填充了“保存”filter_by
上的优惠券字典。
剩下一点细节(让管理表单对用户更友好一点):在编辑现有的 Coupon
时,我希望表单的brands
和categories
字段products
预先填充filter_by
优惠券字典中的值。
这就是修改Form__init__
的方法派上用场的地方(请记住,我们正在修改的实例可以在Form 的属性中访问)self.instance
class CouponAdminForm(forms.ModelForm):
brands = forms.ModelMultipleChoiceField(queryset=Brand.objects.all().order_by('name'),
required=False,
widget=FilteredSelectMultiple("Brands", is_stacked=False))
categories = forms.ModelMultipleChoiceField(queryset=Category.objects.all().order_by('name'),
required=False,
widget=FilteredSelectMultiple("Categories", is_stacked=False))
products = forms.ModelMultipleChoiceField(queryset=Product.objects.all().order_by('name'),
required=False,
widget=FilteredSelectMultiple("Products", is_stacked=False))
def __init__(self, *args, **kwargs):
# For some reason, using the `get_changeform_initial_data` method in the
# CouponAdminForm(forms.ModelForm) didn't work, and we have to do it
# like this instead? Maybe becase the fields `brands`, `categories`...
# are not part of the Coupon model? Meh... whatever... It happened to me the
# same it happened to this OP in stackoverflow: https://stackoverflow.com/q/26785509/289011
super(CouponAdminForm, self).__init__(*args, **kwargs)
self.fields["brands"].initial = self.instance.filter_by.get('brands')
self.fields["categories"].initial = self.instance.filter_by.get('categories')
self.fields["products"].initial = self.instance.filter_by.get('products')
def save(self, commit=True):
filter_by_qsets = {}
for key in ['brands', 'categories', 'products']:
# ... explained above ...
就是这样。
截至目前(现在,2017 年 3 月 19 日),这似乎可以很好地满足我的需要。
正如alfonso.kim在他的回答中指出的那样,除非我更改窗口的 Javascrip(或者我使用自定义模型?不知道:没有尝试过),否则我无法ChainedForeignKey
动态过滤不同的字段。这种方法我无法过滤管理网页上的选择框,删除仅属于所选类别的产品,例如,我不能做诸如“如果用户选择一个brand
,过滤器categories
,products
所以他们只显示属于的元素那个牌子”。发生这种情况是因为当用户选择品牌时,浏览器和服务器之间没有 XHR (Ajax) 请求。基本上:流程是您获取表格->您填写表格-->您发布表单,当用户单击表单上的“事物”时,浏览器<-->服务器之间没有通信。如果用户在选择中选择“可口可乐” brands
,该products
选择会被过滤,并plastic bags
从可用产品中删除(例如),但很好......这种方法对我的需求来说“足够好” 。
请注意:这个答案中的代码可能包含一些多余的操作,或者可以写得更好的东西,但到目前为止,它似乎工作正常(谁知道,也许我必须编辑我的答案几天后说“我完全错了!请不要这样做!”但到目前为止似乎还可以)不用说:我欢迎任何人必须说的任何建议评论:-)
我希望这对将来的某人有所帮助。