Django 将 User.groups ManyToManyField 在表单中表示为选择框
在我的应用程序中,我使用用户组来表示一种用户类型。在我的情况下,一个用户只能属于一个组。在实现上,我有两个选择:
- 把多对多关系(ManyToMany)改成外键(ForeignKey)
- 在我的表单上用多选框(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 个回答
推荐解决方案
首先,我觉得你应该重新考虑用多对多关系来表示一对多关系。因为很可能会出现用户有多个组的情况,这样可能会导致你代码中出现难以追踪的错误。
既然你已经在使用用户资料类(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)
如果这两种方法中的任何一种能解决你的问题,请告诉我。