Django管理页面:通过多个模型选择自定义ID的字典(JSONField)而不是原始文本

时间:2017-03-12 02:03:05

标签: python django django-admin customization django-jsonfield

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

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

请允许我更具体一点:

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

例如:

  • 如果我想对属于“饮料”类别的所有产品应用50%的折扣,并且“饮料”类别在数据库中具有标识5,则折扣记录看起来像:

    discount_type='percent'
    discount='0.5'
    filter_by={
        'category': [5]
    }
    
  • 如果我想对“饮料”类别 AND 中的所有产品申请减免20美元,比如CocaCola制造,那么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")
    }
    
  • 如果我想对特定产品申请25%的折扣(让我们说id3的产品),字典会如下:

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

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

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

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

enter image description here

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

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

由于我只是要过滤几个字段(categorymanufacturerproduct ...),这些字段实际上是数据库其他元素的ID列表,我想显示一个可以过滤的 thingy 的大型MultiSelect框,所以我可以看到我可以过滤的所有元素的用户友好列表,选择一些,然后,当我点击在“创建折扣”上,我会得到filter_by字典(我还远没有担心如何生成字典,因为我甚至不知道如何正确呈现字典管理员表格。

像Django Admin自动为我的产品类别做的事情:

enter image description here

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

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模型的问题在于ManyToManycategorymanufacturer没有product字段。可怜的Django不知道Discount与所有这些事情有关:它只知道有一个Json字段。

我真的希望能够在Django区域展示一堆<select>列出所有可能的过滤器(CategoryManufacturerID .. 。)可以存储在filter_by字典中(一个条目带有<select>的{​​{1}} Category,显示数据库中的所有可用类别,Manufacturer的一个条目,显示所有可用的制造商......等等。但我真的,真的不知道该怎么做。

我可以使用Widgets尝试通过form通过forms.ModelMultipleChoiceField来表示JSON字段(顺便提一下,似乎是最接近我想要的东西,虽然仍然很远)...但我觉得这有点无意义,因为没有什么能接近我想要的东西。

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

2 个答案:

答案 0 :(得分:1)

你需要一些javascript来使json字典适合一个漂亮的HTML小部件,然后在Django处理程序中处理它。

如果你想使用&#34;魔法&#34; Django管理员,您必须为其提供所需的输入,以便为您的折扣系统呈现漂亮的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)

答案 1 :(得分:1)

所以...我很欣赏@ alfonso.kim的answer,但是想要为&#34;渲染&#34;而创建一个全新的Django模型的想法。 目的对我来说听起来有点过分。请!不要误会我的意思:它可能是&#34;规范&#34;这样做的方式(我已经多次推荐这种方法)并且可能比更好,但我想表明如何解决我的问题特别的问题:

我看了一下Django的源代码,特别是如何在管理员中显示ManyToMany关系。如果你看看我上面的原始问题,我想弄清楚Django在编辑一个产品时使用哪个类来显示类别&#34;双列选择&#34; ,给它一个名字,我非常喜欢)。事实证明这是一个django.forms.models.ModelMultipleChoiceField&#34;经验丰富的&#34; ,带有一个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)

这样做会在处方集的 root 中显示三个表格的手动添加字段(brandcategoriesproducts)。换句话说:这产生了三个与我Coupon模型中其他字段相同级别的新字段。然而:他们不是真正的第一堂课&#34;字段,因为它们实际上将确定我的模型中的一个特定字段的内容(Coupon.filter_by字段),让我们记住,这是一个看起来或多或少的字典:

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

为了使用管理员网页向人们明确表示这三个字段并非真实地&#34;优惠券模型中的第一级字段,我决定将它们分组显示。

为此,我需要更改字段的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 here

的更多信息

这就是诀窍:

Filter_by in the admin page

现在,当管理员用户通过此表单创建新的Coupon时(换句话说:当用户点击页面上的&#34; Save&#34;按钮时),我会得到一个查询集我在自定义表单中声明的​​额外字段(brands的一个查询集,categories的另一个查询集和products的另一个查询集)但实际上我需要将该信息转换为字典。我通过覆盖模型的表单save方法来实现这一点:

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',)

&#34; Save&#34; 上正确填充了优惠券的filter_by词典。

还有一些细节(为了使管理员形式更加用户友好):编辑现有 Coupon时,我想要brands,{表单的{1}}和categories字段预先填充了优惠券的products字典中的值。

此处修改表单filter_by方法非常方便(请记住,我们正在修改的实例可在__init__中访问表格的属性)

self.instance

就是这样。

截至目前(正确现在,2017年3月19日),这似乎可以很好地满足我的需求。

正如alfonso.kim在他的回答中指出的那样,除非我更改窗口的Javascrip(或者我可能使用{{3} }自定义模型?不知道:没有尝试过)我的意思是,通过这种方法,我无法过滤管理网页上的选择框,删除仅属于所选类别的产品,例如,我不能做&#34;如果用户选择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 ... ,过滤brandcategories,那么他们只会显示属于该品牌的元素&#34 ; 。发生这种情况是因为当用户选择品牌时,浏览器和服务器之间没有XHR(Ajax)请求。基本上:流程你获取表格 - &gt; 您填写表格 - &gt; 您发布表单,浏览器之间没有通信&lt; - &gt;当用户点击&#34;事物&#34;在表格上。如果用户选择&#34; Coca cola&#34;那将是很好的。在products选择中,brands选择被过滤,并从可用产品中删除products(例如)但是......这种方法&#34; good足够的&#34; 满足我的需求。

请注意:这个答案中的代码可能包含一些冗余的操作,或者可能写得更好的东西,但到目前为止,似乎工作正常(谁知道,也许我必须几天后编辑我的回答说&#34;我完全错了!!请不要这样做!&#34; 到目前为止它似乎没问题)毋庸置疑:我会欢迎任何人提出任何意见: - )

的意见

我希望将来可以帮助某人。