Django 多对多与管理后台

4 投票
1 回答
1532 浏览
提问于 2025-04-18 12:55

我有一个django应用,模型的设置比较复杂。我最终使用了多层组合来创建一个层次模型。所有的关系都是一对一的,所以我本可以使用继承,但我选择不这样做,这样我可以利用对象组合来构建我的模型。这意味着我可以做一些事情,比如

product.outerframe.top.cost

这样可以让我需要进行的复杂计算变得更加有条理。

不过,这种模型安排让使用django的管理后台变得有点棘手。基本上,我有一个中间表,也就是说,外框表只是指向其他表的一堆外键(每个外键都有唯一约束)。我最终重写了ModelAdmin的add_view()和change_view()方法,这个过程相当困难。

有没有更简单的方法来处理在使用django管理后台时的多对多/中间表?

enter image description here

这些表的排列方式是:

产品 > 外框、内框、玻璃、其他

外框 > 上、下、侧等。

内框 > 上、下、侧等。

玻璃 > 玻璃类型等。

其他 > 配件等。

这是我的模型:

class Product(mixins.ProductVariables):
    name = models.CharField(max_length=255)
    sku = models.CharField(max_length=100, unique=True, db_index=True)
    image = thumbnail.ImageField(upload_to='product_images', blank=True)
    description = models.TextField(blank=True)
    group = models.ForeignKey('ProductGroup', related_name='products', null=True)
    hidden = models.BooleanField(default=False)
    product_specific_mark_up = models.DecimalField(default=1.0, max_digits=5,decimal_places=2)

    # Methods for totals
    def total_material_cost(self, width, height, options):
        return sum([
            self.outerframe.cost(width, height, options),
            self.innerframe.cost(width, height, options),
            self.glass.cost(width, height, options),
            self.other.cost(width, height, options),
        ])

    def total_labour_time(self, width, height, options):
        return sum([
            self.outerframe.labour_time(width, height, options),
            self.innerframe.labour_time(width, height, options),
            self.glass.labour_time(width, height, options),
            self.other.labour_time(width, height, options),
        ])

    def total_co2_output(self, width, height, options):
        return sum([
            self.outerframe.co2_output(width, height, options),
            self.innerframe.co2_output(width, height, options),
            self.glass.co2_output(width, height, options),
            self.other.co2_output(width, height, options),
        ])

    @property
    def max_overall_width(self):
        return 1000

    @property
    def max_overall_height(self):
        return 1000

    def __unicode__(self):
        return self.name


class OuterFrame(models.Model, mixins.GetFieldsMixin, mixins.GetRelatedClassesMixin):
    top = models.OneToOneField(mixins.TopFrame)
    bottom = models.OneToOneField(mixins.BottomFrame)
    side = models.OneToOneField(mixins.SideFrame)
    accessories = models.OneToOneField(mixins.Accessories)
    flashing = models.OneToOneField(mixins.Flashing)
    silicone = models.OneToOneField(mixins.Silicone)

    product = models.OneToOneField(Product)

    def cost(self, width, height, options):
        #accessories_cost = (self.accessories.cost if options['accessories'] else 0)
        #flashing_cost = (self.flashing.cost if options['flashing'] else 0)
        #silicone_cost = (self.silicone.cost if options['silicone'] else 0)
        return sum([
            self.top.cost * (width / 1000),
            self.bottom.cost * (width / 1000),
            self.side.cost * (width*2 / 1000),
            #accessories_cost,
            #flashing_cost,
            #silicone_cost,
        ])

    def labour_time(self, width, height, options):
        return datetime.timedelta(minutes=100)

    def CO2_output(self, width, height, options):
        return 100 # some kg measurement

    @classmethod
    def get_fields(cls):
        options = cls._meta
        fields = {}
        for field in options.fields:
            if field.name == 'product':
                continue
            if isinstance(field, models.OneToOneField):
                related_cls = field.rel.to
                related_fields = fields_for_model(related_cls, fields=related_cls.get_fields())
                fields.update( { related_cls.__name__ + '_' + name:field for name, field in related_fields.iteritems() })
        return fields



class InnerFrame(models.Model, mixins.GetFieldsMixin, mixins.GetRelatedClassesMixin):
    top = models.OneToOneField(mixins.TopFrame)
    bottom = models.OneToOneField(mixins.BottomFrame)
    side = models.OneToOneField(mixins.SideFrame)
    accessories = models.OneToOneField(mixins.Accessories)

    product = models.OneToOneField(Product)

    def cost(self, width, height, options):
        #accessories_cost = (self.accessories.cost if options['accessories'] else 0)
        print self.top.cost
        return sum([
            self.top.cost * (width / 1000),
            self.bottom.cost * (width / 1000),
            self.side.cost * (width*2 / 1000),
        #    accessories_cost,
        ])

    def labour_time(self, width, height, options):
        return datetime.timedelta(minutes=100)

    def CO2_output(self, width, height, options):
        return 100 # some kg measurement

class Glass(models.Model, mixins.GetRelatedClassesMixin):
    glass_type_a = models.OneToOneField(mixins.GlassTypeA)
    glass_type_b = models.OneToOneField(mixins.GlassTypeB)
    enhanced = models.OneToOneField(mixins.Enhanced)
    laminate = models.OneToOneField(mixins.Laminate)
    low_iron = models.OneToOneField(mixins.LowIron)
    privacy = models.OneToOneField(mixins.Privacy)
    anti_slip = models.OneToOneField(mixins.AntiSlip)
    heat_film_mirror = models.OneToOneField(mixins.HeatMirrorField)
    posished_edges = models.OneToOneField(mixins.PolishedEdges)

    product = models.OneToOneField(Product)

    def cost(self, width, height, options):
        return sum([
        ])

    def labour_time(self, width, height, options):
        return datetime.timedelta(minutes=100)

    def CO2_output(self, width, height, options):
        return 100 # some kg measurement

class Other(models.Model, mixins.GetRelatedClassesMixin):
    num_packages = models.OneToOneField(mixins.NumberPackages)

    product = models.OneToOneField(Product)

    def cost(self, width, height, options):
        return 100

    def labour_time(self, width, height, options):
        return datetime.timedelta(minutes=100)

    def CO2_output(self, width, height, options):
        return 100 # some kg measurement

混合类:

class TimeCostMixin(models.Model, GetFieldsMixin):
    cost = models.DecimalField(default=0.0, max_digits=10, decimal_places=2)
    time = models.TimeField(default=datetime.timedelta(0))
    class Meta:
        abstract = True

##### Frame #####
class FrameComponentMixin(TimeCostMixin):
    external_width = models.IntegerField(default=0)
    material_weight = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
    u_value = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
    class Meta:
        abstract = True


class TopFrame(FrameComponentMixin):
    pass


class BottomFrame(FrameComponentMixin):
    pass


class SideFrame(FrameComponentMixin):
    pass


class Accessories(TimeCostMixin):
    material_weight = models.DecimalField(default=0.0,max_digits=10,decimal_places=2)


class Flashing(TimeCostMixin):
    pass


class Silicone(TimeCostMixin):
    labour_time = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
#################

##### Glass #####
class GlassTypeA(TimeCostMixin):
    material_weight = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
    u_value = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)

class GlassTypeB(TimeCostMixin):
    material_weight = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)
    u_value = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)

class Enhanced(TimeCostMixin):
    material_weight = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)

class Laminate(TimeCostMixin):
    material_weight = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)

class LowIron(TimeCostMixin):
    pass

class Privacy(TimeCostMixin):
    pass

class AntiSlip(TimeCostMixin):
    pass

class HeatMirrorField(TimeCostMixin):
    u_value = models.DecimalField(default=0.0, max_digits=10,decimal_places=2)

class PolishedEdges(models.Model):
    cost = models.DecimalField(default=0.0, max_digits=10, decimal_places=2)
##################

##### other  #####
class NumberPackages(models.Model):
    number_of_packages = models.IntegerField(default=0)
##################

还有一个让人抓狂的管理后台!

class ProductAdmin(AdminImageMixin, admin.ModelAdmin):
    inlines = [ProductDownloadInline, ProductConfigurationInline]

    add_form_template = 'admin/products/add_form.html'
    change_form_template = 'admin/products/add_form.html'


    @csrf_protect_m
    @transaction.atomic
    def add_view(self, request, form_url='', extra_context=None):
        extra_context = extra_context or {}

        "The 'add' admin view for this model."
        model = self.model
        opts = model._meta

        if not self.has_add_permission(request):
            raise PermissionDenied

        ModelForm = self.get_form(request)
        formsets = []
        inline_instances = self.get_inline_instances(request, None)
        if request.method == 'POST':
            form = ModelForm(request.POST, request.FILES)
            if form.is_valid():
                new_object = self.save_form(request, form, change=False)
                form_validated = True
            else:
                form_validated = False
                new_object = self.model()
            prefixes = {}
            for FormSet, inline in zip(self.get_formsets(request), inline_instances):
                prefix = FormSet.get_default_prefix()
                prefixes[prefix] = prefixes.get(prefix, 0) + 1
                if prefixes[prefix] != 1 or not prefix:
                    prefix = "%s-%s" % (prefix, prefixes[prefix])
                formset = FormSet(data=request.POST, files=request.FILES,
                                  instance=new_object,
                                  save_as_new="_saveasnew" in request.POST,
                                  prefix=prefix, queryset=inline.get_queryset(request))
                formsets.append(formset)

            #####
            outer_frame_forms = [
                modelform_factory(cls)(request.POST, prefix='OuterFrame_'+cls.__name__)
                for cls in models.OuterFrame.get_related_classes(exclude=['product'])
            ]
            inner_frame_forms = [
                modelform_factory(cls)(request.POST, prefix='InnerFrame'+cls.__name__)
                for cls in models.InnerFrame.get_related_classes(exclude=['product'])
            ]
            glass_forms = [
                modelform_factory(cls)(request.POST, prefix='InnerFrame'+cls.__name__)
                for cls in models.Glass.get_related_classes(exclude=['product'])
            ]
            other_forms = [
                modelform_factory(cls)(request.POST, prefix='InnerFrame'+cls.__name__)
                for cls in models.Other.get_related_classes(exclude=['product'])
            ]
            #####

            if all_valid(formsets
                        +outer_frame_forms
                        +inner_frame_forms
                        +glass_forms
                        +other_forms
                        ) and form_validated:
                self.save_model(request, new_object, form, False)
                self.save_related(request, form, formsets, False)
                self.log_addition(request, new_object)

                ##### save object hierichy #####
                # inner frame
                inner_frame = models.InnerFrame()
                inner_frame.product = new_object
                mapping = {f.rel.to:f.name for f in models.InnerFrame._meta.fields if f.name not in ['id','product']}
                for f in inner_frame_forms:
                    obj = f.save()
                    setattr(inner_frame, mapping[obj.__class__], obj)
                inner_frame.save()
                # outer frame
                outer_frame = models.OuterFrame()
                outer_frame.product = new_object
                mapping = {f.rel.to:f.name for f in models.OuterFrame._meta.fields if f.name not in ['id','product']}
                for f in outer_frame_forms:
                    obj = f.save()
                    setattr(outer_frame, mapping[obj.__class__], obj)
                outer_frame.save()
                # glass
                glass = models.Glass()
                glass.product = new_object
                mapping = {f.rel.to:f.name for f in models.Glass._meta.fields if f.name not in ['id','product']}
                for f in glass_forms:
                    obj = f.save()
                    setattr(glass, mapping[obj.__class__], obj)
                glass.save()
                # other
                other = models.Other()
                other.product = new_object
                mapping = {f.rel.to:f.name for f in models.Other._meta.fields if f.name not in ['id','product']}
                for f in other_forms:
                    obj = f.save()
                    setattr(other, mapping[obj.__class__], obj)
                other.save()
                #################################

                return self.response_add(request, new_object)
        else:
            forms = SortedDict({})
            forms['Outer Frame Variables'] = {
                cls.__name__: modelform_factory(cls)(prefix='OuterFrame_'+cls.__name__)
                for cls in models.OuterFrame.get_related_classes(exclude=['product'])
            }
            forms['Inner Frame Variables'] = {
                cls.__name__: modelform_factory(cls)(prefix='InnerFrame'+cls.__name__)
                for cls in models.InnerFrame.get_related_classes(exclude=['product'])
            }
            forms['Glass Variables'] = {
                cls.__name__: modelform_factory(cls)(prefix='InnerFrame'+cls.__name__)
                for cls in models.Glass.get_related_classes(exclude=['product'])
            }
            forms['Other Variables'] = {
                cls.__name__: modelform_factory(cls)(prefix='InnerFrame'+cls.__name__)
                for cls in models.Other.get_related_classes(exclude=['product'])
            }
            extra_context['forms'] = forms

            # Prepare the dict of initial data from the request.
            # We have to special-case M2Ms as a list of comma-separated PKs.
            initial = dict(request.GET.items())
            for k in initial:
                try:
                    f = opts.get_field(k)
                except models.FieldDoesNotExist:
                    continue
                if isinstance(f, models.ManyToManyField):
                    initial[k] = initial[k].split(",")
            form = ModelForm(initial=initial)
            prefixes = {}
            for FormSet, inline in zip(self.get_formsets(request), inline_instances):
                prefix = FormSet.get_default_prefix()
                prefixes[prefix] = prefixes.get(prefix, 0) + 1
                if prefixes[prefix] != 1 or not prefix:
                    prefix = "%s-%s" % (prefix, prefixes[prefix])
                formset = FormSet(instance=self.model(), prefix=prefix,
                                  queryset=inline.get_queryset(request))
                formsets.append(formset)

        adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
            self.get_prepopulated_fields(request),
            self.get_readonly_fields(request),
            model_admin=self)
        media = self.media + adminForm.media

        inline_admin_formsets = []
        for inline, formset in zip(inline_instances, formsets):
            fieldsets = list(inline.get_fieldsets(request))
            readonly = list(inline.get_readonly_fields(request))
            prepopulated = dict(inline.get_prepopulated_fields(request))
            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
                fieldsets, prepopulated, readonly, model_admin=self)
            inline_admin_formsets.append(inline_admin_formset)
            media = media + inline_admin_formset.media

        context = {
            'title': _('Add %s') % force_text(opts.verbose_name),
            'adminform': adminForm,
            'is_popup': IS_POPUP_VAR in request.REQUEST,
            'media': media,
            'inline_admin_formsets': inline_admin_formsets,
            'errors': helpers.AdminErrorList(form, formsets),
            'app_label': opts.app_label,
            'preserved_filters': self.get_preserved_filters(request),
        }
        context.update(extra_context or {})
        return self.render_change_form(request, context, form_url=form_url, add=True)

1 个回答

1

我还没完全理解你那长长的 add_view 方法,不过你问的总体问题的答案就是“没有”。管理界面并没有提供好的方式来处理多层次的不同类型的层级结构。对于两层的层级结构,使用内联(inlines)处理得很好,这样你可以方便地从编辑某一层的对象,管理下一层的相关对象;但超过两层就不行了。

有一个关于在管理界面添加嵌套内联支持的 问题 已经开了很多年,这样可以帮助处理这种情况。但因为有很多复杂的边缘情况,而且很难让用户界面变得易于理解,所以这个功能一直没有被实现。

有时候,你的数据模型复杂到通用的管理界面都无法很好地使用,这时候你最好自己写一个定制的管理界面。其实,管理界面主要是基于模型表单(ModelForms)和内联模型表单集(InlineModelFormsets)构建的,所以自己做一个符合你需求的界面并没有你想象中那么难;通常这样做会更简单(而且效果更好),比起去大幅度定制现有的管理界面。

我还要提到的是,可以通过内联来处理多对多关系的中间表(即使这个中间表是隐式的,不是自己的模型类),因为如何访问这个隐式创建的中间模型并不是很明显:

class MyM2MInline(admin.TabularInline): model = SomeModel.m2m_field.through

撰写回答