Django:具有多个子模型类型的父模型

Ale*_*yer 2 python django inheritance model

我为CMS创建了一组Django模型来显示一系列Products.

每个页面都包含一系列行,所以我有一个泛型

class ProductRow(models.Model):
  slug = models.SlugField(max_length=100, null=False, blank=False, unique=True, primary_key=True)
  name = models.CharField(max_length=200,null=False,blank=False,unique=True)
  active = models.BooleanField(default=True, null=False, blank=False)
Run Code Online (Sandbox Code Playgroud)

对于不同类型的行,我有一系列这个模型的孩子:

class ProductBanner(ProductRow):
  wide_image = models.ImageField(upload_to='product_images/banners/', max_length=100, null=False, blank=False)
  top_heading_text = models.CharField(max_length=100, null=False, blank=False)
  main_heading_text = models.CharField(max_length=200, null=False, blank=False)
  ...

class ProductMagazineRow(ProductRow):
  title = models.CharField(max_length=50, null=False, blank=False)
  show_descriptions = models.BooleanField(null=False, blank=False, default=False)
  panel_1_product = models.ForeignKey(Product, related_name='+', null=False, blank=False)
  panel_2_product = models.ForeignKey(Product, related_name='+', null=False, blank=False)
  panel_3_product = models.ForeignKey(Product, related_name='+', null=False, blank=False)
  ...

class ProductTextGridRow(ProductRow):
  title = models.CharField(max_length=50, null=False, blank=False)
  col1_title = models.CharField(max_length=50, null=False, blank=False)
  col1_product_1 = models.ForeignKey(Product, related_name='+', null=False, blank=False)
  col1_product_2 = models.ForeignKey(Product, related_name='+', null=False, blank=False)
  col1_product_3 = models.ForeignKey(Product, related_name='+', null=False, blank=False)
  ...
Run Code Online (Sandbox Code Playgroud)

等等.

然后在我的ProductPage我有一系列的ProductRows:

class ProductPage(models.Model):
  slug = models.SlugField(max_length=100, null=False, blank=False, unique=True, primary_key=True)
  name = models.CharField(max_length=200, null=False, blank=False, unique=True)
  title = models.CharField(max_length=80, null=False, blank=False)
  description = models.CharField(max_length=80, null=False, blank=False)
  row_1 = models.ForeignKey(ProductRow, related_name='+', null=False, blank=False)
  row_2 = models.ForeignKey(ProductRow, related_name='+', null=True, blank=True)
  row_3 = models.ForeignKey(ProductRow, related_name='+', null=True, blank=True)
  row_4 = models.ForeignKey(ProductRow, related_name='+', null=True, blank=True)
  row_5 = models.ForeignKey(ProductRow, related_name='+', null=True, blank=True)
Run Code Online (Sandbox Code Playgroud)

我遇到的问题是,我想允许这5行中的ProductPage任何一种是不同的子类型ProductRow.但是,当我迭代它们时,如

views.py:

product_page_rows = [product_page.row_1,product_page.row_2,product_page.row_3,product_page.row_4,product_page.row_5]
Run Code Online (Sandbox Code Playgroud)

然后在模板中:

{% for row in product_page_rows %}
  <pre>{{ row.XXXX }}</pre>
{% endfor %}
Run Code Online (Sandbox Code Playgroud)

我不能将任何子字段引用为XXXX.

我尝试向type父节点和子节点添加" ()"方法,尝试区分每一行的类:

class ProductRow(models.Model):

  ...

  @classmethod
  def type(cls):
      return "generic"
Run Code Online (Sandbox Code Playgroud)

class ProductTextGridRow(TourRow):

  ...

  @classmethod
  def type(cls):
      return "text-grid"
Run Code Online (Sandbox Code Playgroud)

但是如果我在模板中更改XXXX,.type()那么它会显示"generic"列表中的每个项目(我在数据中定义了各种行类型),所以我猜所有内容都将作为一个ProductRow而不是相应的子类型返回.我找不到任何办法让孩子们可以访问为正确的子类型,而不是父类型,或以确定哪些类型的孩子,他们实际上是(我试过catch荷兰国际集团AttributeError为好,没有帮助).

有人可以建议我如何正确处理包含共同父类的各种模型类型列表,并能够访问相应子模型类型的字段吗?

Ant*_*ard 5

这通常(读"总是")一个糟糕的设计,有这样的东西:

class MyModel(models.Model):
    ...
    row_1 = models.ForeignKey(...)
    row_2 = models.ForeignKey(...)
    row_3 = models.ForeignKey(...)
    row_4 = models.ForeignKey(...)
    row_5 = models.ForeignKey(...)
Run Code Online (Sandbox Code Playgroud)

它不可扩展.如果您想要允许6行或4行而不是5天,一天(谁知道?),您将不得不添加/删除新行并更改数据库方案(并处理具有5行的现有对象).并且它不是DRY,您的代码量取决于您处理的行数,并且涉及大量的复制粘贴.

这很清楚,这是一个不好的设计,如果你想知道你会怎么做,如果你必须处理100行,而不是5.

您必须使用一个ManyToManyField()和一些自定义逻辑来确保至少有一行,最多五行.

class ProductPage(models.Model):
    ...
    rows = models.ManyToManyField(ProductRow)
Run Code Online (Sandbox Code Playgroud)

如果要对行进行排序,可以使用如下的显式中间模型:

class ProductPageRow(models.Model):

    class Meta:
        order_with_respect_to = 'page'

    row = models.ForeignKey(ProductRow)
    page = models.ForeignKey(ProductPage)

class ProductPage(models.Model):
    ...
    rows = model.ManyToManyField(ProductRow, through=ProductPageRow)
Run Code Online (Sandbox Code Playgroud)

我想只允许N行(比方说5),你可以实现自己的order_with_respect_to逻辑:

from django.core.validators import MaxValueValidator

class ProductPageRow(models.Model):

    class Meta:
        unique_together = ('row', 'page', 'ordering')

    MAX_ROWS = 5

    row = models.ForeignKey(ProductRow)
    page = models.ForeignKey(ProductPage)
    ordering = models.PositiveSmallIntegerField(
        validators=[
            MaxValueValidator(MAX_ROWS - 1),
        ],
    )
Run Code Online (Sandbox Code Playgroud)

('row', 'page', 'ordering')强制执行元组唯一性,并且排序限制为五个值(从0到4),这对夫妇的出现次数不能超过5次('row', 'page').

但是,除非您有充分的理由100%确定无法N通过任何方式在数据库中添加多行(包括DBMS控制台上的直接SQL查询输入),否则无需"锁定"这是一个水平.

所有"不受信任"的用户很可能只能通过HTML表单输入更新您的数据库.并且您可以在填写表单时使用formset强制最小和最大行数.

注意:这也适用于您的其他型号.任何一组名称 foobar_N,其中N是一个递增的整数,背叛了一个非常糟糕的数据库设计.


然而,这并不能解决您的问题.

从父模型实例中获取子模型实例的最简单方法(阅读"首先想到的")是循环遍历每个可能的子模型,直到获得匹配的实例.

class ProductRow(models.Model):
    ...
    def get_actual_instance(self):
        if type(self) != ProductRow:
            # If it's not a ProductRow, its a child
            return self
        attr_name = '{}_ptr'.format(ProductRow._meta.model_name)
        for possible_class in self.__subclasses__():
            field_name = possible_class._meta.get_field(attr_name).related_query_name()
            try:
                return getattr(self, field_name)
            except possible_class.DoesNotExist:
                pass
         # If no child found, it was a ProductRow
         return self
Run Code Online (Sandbox Code Playgroud)

它涉及到每次尝试点击数据库.它仍然不是很干.获得它的最有效方法是添加一个字段,告诉你孩子的类型:

from django.contrib.contenttypes.models import ContentType

class ProductRow(models.Model):
    ...
    actual_type = models.ForeignKey(ContentType, editable=False)

    def save(self, *args, **kwargs):
        if self._state.adding:
            self.actual_type = ContentType.objects.get_for_model(type(self))
         super().save(*args, **kwargs)

    def get_actual_instance(self):
        my_info = (self._meta.app_label, self._meta.model_name)
        actual_info = (self.actual_type.app_label, self.actual_type.model)
        if type(self) != ProductRow or my_info == actual_info:
            # If this is already the actual instance
            return self
        # Otherwise
        attr_name = '{}_ptr_id'.format(ProductRow._meta.model_name)
        return self.actual_type.get_object_for_this_type(**{
            attr_name: self.pk,
        })
Run Code Online (Sandbox Code Playgroud)