tsurantino
tsurantino

Reputation: 1027

Django Representing User.groups ManyToManyField as a Select in Form

For my application, I am using User Groups to represent a type of user. In my particular case, a user can only be in one group. In implementation, I have two options:

  1. Override ManyToMany to be a ForeignKey
  2. Represent ManyToMany as a MultipleChoiceField on my form, accept only 1 submission and then go from there.

I went with Option 2 because sometimes, having a user be part of 2 groups was useful for testing (just convenience). I don't think there is a difference between the two in terms of implementation (but you're advice is appreciated).

In my view, I then write code to associate the two (which is a ManyToMany in a UserProfile extension class of User) - I'm not sure if this is working.

The main error I encounter is that the form does not permit validation and says that the ManyToMany needs a "list of values" to proceed.

I have the following set of code:

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 } )

and 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>

Everything seems to load perfectly fine. But the form won't go past is_valid() because the Groups field requires a "list of values". I've seen others ask how to parse information from a TextField/TextArea, but I don't see why I'd need to split my info since it's only 1.

Your advice is highly appreciated.

Upvotes: 0

Views: 3277

Answers (1)

Giles Smith
Giles Smith

Reputation: 1972

Prefered Solution

Firstly, I think you should reconsider using a M:M relationship to represent a 1:M relationship. There is going to be a strong likelihood that there are certain situations where a user gets multiple groups, and that may result in bugs in you code at a later stage, that are tricky to trace.

As you are already using a UserProfile class, I would put a Foreign key on the user profile model, and that will provide an accurate representation of the data structure that should exist (even if it means logging in and out when testing).

Update

You could do this by changing the model as so:

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

If you have lots of existing code that uses the existing user groups relationship, this is a less practical solution. However if you really need to force this restriction (one group per user/userprofile), then this would do so.

Solution to your specific problem

If for what ever reason, you don't feel my comments above are appropriate (I don't know the specific situation in which your code exists)...

I think the problem that you are experiencing is due to the fact that the select widget returns a single item to the form, whereas a SelectMultiple will return a list of values. As the form expects a list, this is where your problem is.

I would suggest subclassing the SelectMultiple widget, so that it actually renders as a select single on the form, but still uses the existing logic to return a list.

This is the current render function in the SelectMultiple Widget:

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))

if you subclassed and overrode the render method as follows:

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))

This will render a select single, but retrieve a list of items.

Then in your form meta, just use your new custom class:

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

Alternative

Alternatively you could override the Select widget to return a list:

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)

Let me know if either of these solve your problem.

Upvotes: 2

Related Questions