4

我有一个模型,其中一个字段是postgres.fields.JSONField

将要存储在那里的 Json 有一个可变的 ID 字典,它引用数据库中的其他项目(可能的关系/属性)。

请允许我更具体一点:

基本上,我正在尝试创建一个折扣系统,其中一些折扣适用于某些产品。JSON 字段包含用于了解哪些产品可以获得折扣的约束。

例如:

  • 如果我想对“饮料”类别下的所有产品应用 50% 的折扣,并且“饮料”类别5在数据库中具有 id,则折扣记录如下所示:

    discount_type='percent'
    discount='0.5'
    filter_by={
        'category': [5]
    }
    
  • 如果我想为“饮料”类别中的所有产品以及由可口可乐制造的所有产品减免 20 美元那么filter_by字典将如下所示:

    discount_type='fixed amount'
    discount='20'
    filter_by={
        'category': [5],
        'manufacturer': [2]   # Assuming coca-cola is the Manufacturer 
                              # with id==2 in the 'Manufacturers'
                              # table of the database (NOTE: this is 
                              # needed since CocaCola manufactures
                              # products besides "Beverages")
    }
    
  • 如果我想对特定产品(假设id是 is的产品3)应用 25% 的折扣,那么字典将如下所示:

    discount_type='percent'
    discount='0.25'
    filter_by={
        'id': [3]
    }
    

这个想法似乎足够灵活,可以满足我的需求,并且(到目前为止)我很高兴。


现在,问题在于如何在模型的 Django 管理区域中输入这些值Discount

正如预期的那样,filter_by字典呈现为最初如下所示的文本字段:

在此处输入图像描述

如果我想向其中添加字段,我需要编写我想要的确切 JSON ......这意味着如果我想对“饮料”类别应用折扣,我需要找出该类别的哪个 ID has 在数据库中,然后手动键入{"category": [5]},同时在键入时非常小心':确保我不会错过 a][...

Thaaaat ...好吧,这不是很有帮助...

由于我只会通过几个字段(,,...)进行过滤,这些字段category实际上manufacturerproduct数据库其他元素的 ID 列表,我想为每个我可以过滤的东西显示一个大的 MultiSelect 框所以我可以看到我可以过滤的所有元素的用户友好列表,选择一些,然后,当我点击“创建折扣”时,我会得到filter_by字典(我仍然远离担心如何生成字典,因为我什至不知道如何正确呈现管理员表单)。

类似于 Django Admin 自动为我的产品类别所做的事情:

在此处输入图像描述

这真的非常非常好:一种产品可以属于多个类别。为此,Django 并排呈现两个<select multiple框,其中包含可用的类别以及产品已经属于的类别……我可以通过鼠标点击添加/删除类别……真的,真的很好。但是 Django 可以这样做,因为它知道 是模型categories中的ManyToMany关系Product

class Product(models.Model):
    parent = models.ForeignKey('self', null=True, blank=True)
    manufacturer = models.ForeignKey('Manufacturer')
    categories = models.ManyToManyField('Category',
                                         related_name='products', blank=True)

Discount模型的问题是没有,或的ManyToMany字段。可怜的 Django 不知道 a与所有这些事情有关:它只知道有一个字段。categorymanufacturerproductDiscountJson

我真的希望能够<select>在 Django 区域中显示一堆,列出可以存储在字典中的所有可能的过滤器(、、...)(一个带有双精度的Category条目Manufacturer,用于显示数据库,一个条目,显示所有可用的制造商......等等)。但我真的,真的不知道该怎么做。IDfilter_by<select>CategoryManufacturer

我可能会让你厌烦我所做的一堆尝试,使用Widgets, 试图通过 a form, through来表示 JSON 字段forms.ModelMultipleChoiceField(顺便说一句,这似乎是最接近我想要的东西,虽然仍然很远)。 ..但我认为那是毫无意义的,因为没有什么能接近我想要的。

像往常一样,感谢您阅读这封巨大的电子邮件,并提前感谢您。任何提示都会非常感激,即使只是你应该看看“这个”

4

2 回答 2

7

所以......我很欣赏@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显示三个表单的手动添加字段(categoriesproducts) 。换句话说:这产生了三个与我模型中的其他字段处于同一级别的新字段。但是:它们并不是真正的“一流”字段,因为它们实际上要确定我的模型(字段)中一个特定字段的内容,让我们记住,它或多或少像一个字典:CouponCoupon.filter_by

filter_by = {
    "brands": [2, 3],
    "categories": [7]
}

为了让使用 Admin 网页的人清楚地知道这三个字段并不是 Coupon 模型中“真正的”第一级字段,我决定将它们分组显示。

为此,我需要更改CouponsAdmin字段的布局。我不希望这种分组影响我Coupon模型的其他字段的显示方式,即使后来将新字段添加到模型中,所以我让表单的所有其他字段保持不变(换句话说:只应用特殊/分组布局到表单中的 和 字段brandscategoriesproducts令我惊讶的是,我无法在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

那成功了:

管理页面中的Filter_by

现在,当管理员用户Coupon通过此表单创建一个新表单时(换句话说:当用户单击页面上的“保存”按钮时)我将获得一个查询集,用于我在自定义表单中声明的​​额外字段(一个查询集用于brands, 另一个用于categories, 另一个用于products) 但我实际上需要将该信息转换为字典。我能够通过覆盖saveModel'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时,我希望表单的brandscategories字段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,过滤器categoriesproducts所以他们只显示属于的元素那个牌子”。发生这种情况是因为当用户选择品牌时,浏览器和服务器之间没有 XHR (Ajax) 请求。基本上:流程是您获取表格->您填写表格-->您发布表单,当用户单击表单上的“事物”时,浏览器<-->服务器之间没有通信。如果用户在选择中选择“可口可乐” brands,该products选择会被过滤,并plastic bags从可用产品中删除(例如),但很好......这种方法对我的需求来说“足够好” 。

请注意:这个答案中的代码可能包含一些多余的操作,或者可以写得更好的东西,但到目前为止,它似乎工作正常(谁知道,也许我必须编辑我的答案几天后说“我完全错了!请不要这样做!”到目前为止似乎还可以)不用说:我欢迎任何人必须说的任何建议评论:-)

我希望这对将来的某人有所帮助。

于 2017-03-19T17:27:52.063 回答
1

您将需要一些 javascript 来将 json 字典放入一个漂亮的 HTML 小部件中,然后在 Django 处理程序中处理它。

如果你想使用 Django admin 的“魔法”,你必须给它提供渲染漂亮 UI 并为你的折扣系统创建模型所需的输入:

class Discount(models.Model):
  discount_type = models.TextField()
  discount_percentage = models.FloatField()

class DiscountElement(models.Model):
  discount = models.ForeignKey(Discount)
  manufacturer = models.ForeignKey(Manufacturer, null=True)
  category = models.ForeignKey(Category, null=True)
于 2017-03-12T03:55:23.523 回答