Django ModelForm 的条件表单显示逻辑

2 投票
2 回答
1399 浏览
提问于 2025-04-18 03:37

我觉得我可以用一种非常复杂的方法来实现这个功能,可能需要很多数据库查询和一堆拼凑在一起的jQuery代码,但我希望能找到一个更简单的方法。

我正在为一个Django网站构建一个小的分类应用。网站上有各种媒体内容,它们被分类为某种类型,比如流派、子流派、子子流派和子子子流派。例如,一段媒体可能被分类为音乐(流派)、布鲁斯(子流派)、孟菲斯布鲁斯(子子流派)、福音布鲁斯(第二个子子流派),还有32街班卓琴布鲁斯(子子子流派,这个是我编的)。

流派、子流派、子子流派和子子子流派都是模型:

class Genre(models.Model):

    description = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(unique=True)

    def __unicode__(self):
        return self.description


class SubGenre(models.Model):

    parent = models.ForeignKey(Genre, related_name='sub_genre')
    description = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(unique=True)

    def __unicode__(self):
        return self.description


class SubSubGenre(models.Model):

    parent = models.ForeignKey(SubGenre, related_name='sub_sub_genre')    
    description = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(unique=True)

    def __unicode__(self):
        return self.description


class SubSubSubGenre(models.Model):

    parent = models.ForeignKey(SubSubGenre, related_name='sub_sub_sub_genre')
    description = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(unique=True)

    def __unicode__(self):
        return self.description

每个添加到网站的媒体都有一个MTM字段,将它与一个或多个这些分类关联起来,如下所示:

class Media(models.Model):
    genres = models.ManyToManyField(Genre)
    sub_genres = models.ManyToManyField(SubGenre)
    sub_sub_genres = models.ManyToManyField(SubSubGenre)
    sub_sub_sub_genres = models.ManyToManyField(SubSubSubGenre)
    #other fields

我们网站上的每个用户都有一个用户资料(这是基础用户模型的扩展,使用一对一关系)。在这个用户资料中,他们可以告诉我们他们喜欢消费哪种类型的媒体。

class UserProfile(models.Model):
    user = models.OneToOneField(User)
    preferred_genres = models.ManyToManyField(Genre, related_name='genre_pref')
    preferred_sub_genres = models.ManyToManyField(SubGenre, related_name = 'subgenre_pref')
    preferred_sub_sub_genres = models.ManyToManyField(SubSubGenre, related_name = 'subsubgenre_pref')
    preferred_sub_sub_sub_genres = models.ManyToManyField(SubSubSubGenre, related_name = 'subsubsubgenre_pref')

我想创建两个表单:一个是创建新用户资料的,另一个是创建新媒体的。用户应该能够定义他们喜欢的流派、子流派等。媒体上传者也应该能够以相同的方式对他们的媒体进行分类。

我们只有几个流派,但每个流派都有十几个子流派。每个子流派又有20多个子子流派。大多数子子流派还有20多个子子子流派。我不能把所有这些选项一次性放到页面上——那样会让人感到非常困惑。

我希望能这样实现。以“新用户资料表单”为例。用户不需要一次性设置他们喜欢的流派、子流派、子子流派和子子子流派,而是希望有一个多选表单,让用户可以选择他们喜欢的流派。然后,当用户选择了某个流派后(但在提交表单之前),与该流派相关的子流派选项就会出现。当选择了某个子流派后,属于该子流派的子子流派选项也会出现,依此类推。

例如,假设我们在系统中定义了三种流派:音乐、书籍和播客。我们的新用户在设置他们喜欢的流派时,勾选了音乐和书籍的复选框,但播客留空。我希望这个用户能够为音乐和书籍设置他们喜欢的子流派,并且复选框只显示与音乐或书籍相关的子流派(Music.subgenre_set.all()或Book.subgenre_set.all()会返回合适的选项)。

再举个例子,假设有一个用户上传了一个播客。在上传表单中,他们会遇到一个问题,询问这个媒体属于哪个流派。用户勾选了“播客”的复选框。现在我们想知道这个媒体属于哪些子流派。我们该如何让复选框只显示与“播客”相关的子流派呢?

在这两个例子中,如果选择了“播客”作为流派,那么下一个部分的表单中,适当的子流派选项应该由Podcast.subgenre_set.all()表示。如果这个播客的子流派之一是“非虚构”,那么选择这个子流派后,也应该显示相应的子子流派选项:Nonfiction.subsubgenre_set.all()。不过,我不能把流派、子流派、子子流派和子子子流派的名称硬编码,因为将来肯定会添加新的选项,那样会造成扩展上的麻烦。

我还觉得,后台“加载”所有可能的whatever_set,然后用JavaScript的技巧把它们显示出来,会给数据库带来很大的压力。

如果我把表单分成多个页面(和多个数据库查询),那就简单了。但我该如何在一个表单中做到这一点,而不让我的数据库崩溃呢?

谢谢你读完这些内容!

2 个回答

2

首先,我觉得你有太多模型了。所有模型的字段完全一样,所以你最好只用一个模型(可以叫它分类),里面包含名称、描述、父级和级别。比如,类型(Genre)可以设为级别0,没有父级;子类型(SubGenre)可以设为级别1,父级是级别0,依此类推。(你也可以用类似MPTT的方式来处理,但可能有点过于复杂,因为排序并不重要。)

至于你的问题,我认为使用Javascript是个好主意,但不要一次性加载所有内容。你应该用Ajax来实现:当你选择一个类型时,发送一个Ajax请求到服务器,服务器会返回相关的子类型。因为这个请求很轻量,所以用户几乎感觉不到延迟,这样你就可以为用户构建复选框或其他选择项。

4

首先,我建议你重新考虑一下你是怎么设计你的类型的。我觉得一个更好的方法是只用一个模型 Genre,而不是为每个 SubGenre 创建新的模型。这样的话,你可以随意扩展,而不需要改变你的模型结构。

class Genre(models.Model):
    parent = models.ForeignKey(Genre, null=True, related_name='sub_genre')
    description = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(unique=True)

    def __unicode__(self):
        return self.description

    @property
    def is_top_level(self):
        return bool(self.parent)

接下来,我会使用 Form 而不是 ModelForm,可以像这样做:

class GenreForm(forms.Form):
    genre = forms.ModelChoiceField(
        queryset=Genre.objects.filter(parent__is_null=True))
    sub_genre = forms.ModelChoiceField(
        queryset=Genre.objects.filter(
            parent__is_null=False, parent__parent__is_null=True))

在前端,我觉得最简单的办法是给每个子类型的选项加上 class='hidden',然后用一段简单的javascript代码在选择父类型后显示这些选项。

如果你的类型数量非常多,你可能会发现使用ajax来加载后续字段是最有效的,这样可以提高页面加载速度。

希望这些建议对你有帮助!

撰写回答