Django 将 User.groups ManyToManyField 在表单中表示为选择框

0 投票
1 回答
3084 浏览
提问于 2025-04-17 16:26

在我的应用程序中,我使用用户组来表示一种用户类型。在我的情况下,一个用户只能属于一个组。在实现上,我有两个选择:

  1. 把多对多关系(ManyToMany)改成外键(ForeignKey)
  2. 在我的表单上用多选框(MultipleChoiceField)表示多对多关系,只接受一次提交,然后再进行处理。

我选择了第二个选项,因为有时候让一个用户属于两个组对测试很有帮助(只是为了方便)。我觉得这两种实现方式没有太大区别(但我很感激你的建议)。

在我看来,我接下来写代码来关联这两个(这在用户的用户资料扩展类中是一个多对多关系)——我不确定这是否有效。

我遇到的主要错误是,表单不允许验证,并且提示多对多关系需要一个“值的列表”才能继续。

我有以下一段代码:

forms.py

from django.forms import ModelForm, Textarea
from django.contrib.auth.models import User, Group
from registration.models import UserProfile
from django import forms
from django.db import models

class RegistrationForm(ModelForm):
    class Meta:
        model = User
        fields = ('username', 'password', 'email', 'first_name', 'last_name', 'groups')
        widgets = {
            'groups': forms.Select,
            'password': forms.PasswordInput,
        #    'text': Textarea(attrs = {'rows': 3, 'class': 'span10', 'placeholder': 'Post Content'}),
        }

    def __init__(self, *args, **kwargs):
        super(RegistrationForm, self).__init__(*args, **kwargs)
        self.fields['groups'].label = 'Which category do you fall under?'

views.py

def get_registration(request):
    if request.method == 'POST':
        register_form = RegistrationForm(request.POST)
        company_form = CompanyRegistrationForm(request.POST, request.FILES)

        if register_form.is_valid() and company_form.is_valid(): # check CSRF
            if (request.POST['terms'] == True):
                new_user = register_form.save()
                new_company = company_form.save()

                new_profile = UserProfile(user = user, agreed_terms = True)
                new_profile.companies_assoc.add(new_company)
                new_profile.save()

                return HttpResponseRedirect(reverse('companyengine.views.get_company'))
        return render(request, 'registration/register.html', { 'register_form': register_form, 'company_form': company_form } )

    else:
        first_form = RegistrationForm
        second_form = CompanyRegistrationForm
        return render(request, 'registration/register.html', { 'register_form': register_form, 'company_form': company_form } )

还有templates.html

<h2>Sign Up</h2>
<form action="/register" method="POST" enctype="multipart/form-data">{% csrf_token %}
    <p>{{ register_form.non_field_error }}</p>
    {% for field in register_form %}
    <div class="control-group">
        {{ field.errors }}
        <label class="control-label">{{ field.label }}</label>
        <div class="controls">
            {{ field }}
        </div>
    </div>
    {% endfor %}

    <div id="company_fields">
        <p>{{ register_form.non_field_error }}</p>
        {% for field in company_form %}
        <div class="control-group">
            {{ field.errors }}
            <label class="control-label">{{ field.label }}</label>
            <div class="controls">
                {{ field }}
            </div>
        </div>
        {% endfor %}
    </div>

    <label><input type="checkbox" name="terms" id="terms"> I agree with the <a href="#">Terms and Conditions</a>.</label>
    <input type="submit" value="Sign up" class="btn btn-primary center">
    <div class="clearfix"></div>
</form>

一切似乎都加载得很好。但是表单无法通过is_valid(),因为组字段需要一个“值的列表”。我看到其他人询问如何从文本框/TextArea中解析信息,但我不明白为什么我需要拆分我的信息,因为它只有一个。

非常感谢你的建议。

1 个回答

2

推荐解决方案

首先,我觉得你应该重新考虑用多对多关系来表示一对多关系。因为很可能会出现用户有多个组的情况,这样可能会导致你代码中出现难以追踪的错误。

既然你已经在使用用户资料类(UserProfile),我建议在用户资料模型上加一个外键,这样可以更准确地表示应该存在的数据结构(即使这意味着在测试时需要登录和登出)。

更新

你可以通过这样修改模型来实现:

class UserProfile(models.Model):
    # existing fields here
    single_group = models.ForeignKey(Group)

如果你有很多现有代码在使用当前的用户组关系,这样的解决方案可能不太实际。不过,如果你真的需要强制这个限制(每个用户/用户资料只能有一个组),那么这样做是可以的。

针对你具体问题的解决方案

如果因为某种原因,你觉得我上面的建议不合适(我不知道你代码的具体情况)……

我认为你遇到的问题是因为选择控件(select widget)返回的是单个项目,而选择多个控件(SelectMultiple)会返回一个值的列表。因为表单期望的是一个列表,所以这就是你问题的所在。

我建议你可以对选择多个控件进行子类化,这样它在表单上实际上渲染为单选,但仍然使用现有的逻辑返回一个列表。

这是当前选择多个控件的渲染函数:

class SelectMultiple(Select):
    def render(self, name, value, attrs=None, choices=()):
        if value is None: value = []
        final_attrs = self.build_attrs(attrs, name=name)
        output = [u'<select multiple="multiple"%s>' % flatatt(final_attrs)]
        options = self.render_options(choices, value)
        if options:
            output.append(options)
        output.append('</select>')
        return mark_safe(u'\n'.join(output))

如果你对子类进行了修改,并重写了渲染方法如下:

class CustomSelectSingleAsList(SelectMultiple):
    def render(self, name, value, attrs=None, choices=()):
        if value is None: value = []
        final_attrs = self.build_attrs(attrs, name=name)
        output = [u'<select %s>' % flatatt(final_attrs)] # NOTE removed the multiple attribute
        options = self.render_options(choices, value)
        if options:
            output.append(options)
        output.append('</select>')
        return mark_safe(u'\n'.join(output))

这样会渲染为单选,但会获取一个项目列表。

然后在你的表单元数据中,只需使用你新的自定义类:

widgets = { 'groups': myforms.CustomSelectSingleAsList, 'password': forms.PasswordInput, # 'text': Textarea(attrs = {'rows': 3, 'class': 'span10', 'placeholder': '帖子内容'}), }

替代方案

另外,你也可以重写选择控件,使其返回一个列表:

class SelectSingleAsList(Select):
    def value_from_datadict(self, data, files, name):
        if isinstance(data, (MultiValueDict, MergeDict)):
            return data.getlist(name)  # NOTE this returns a list rather than a single value.
        return data.get(name, None)

如果这两种方法中的任何一种能解决你的问题,请告诉我。

撰写回答