Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to limit field choices in formset?

I'm having problems limiting the selectable choices in a formset. I have the following models: Employees, Department, Project, Projecttype, Membership, and Role. An employee can add/remove the roles that they play for a given departments project in the formset, the form should limit the selectable projects to only those belonging to the department that the employee belongs to.

MODELS:

class Department(models.Model):
    name = models.CharField(max_length=20)
    def __unicode__(self):
    return self.name

class Employee(models.Model):
    fname = models.CharField(max_length=15)
    department = models.ForeignKey(Department)
    def __unicode__(self):
        return self.fname

class Projecttype(models.Model):
    name = models.CharField(max_length=20)
    def __unicode__(self):
        return self.name

class Project(models.Model):
    projecttype = models.ForeignKey(Projecttype)
    department = models.ForeignKey(Department)
    members = models.ManyToManyField(Employee, through='Membership')
    def __unicode__(self):
       return "%s > %s" % (self.department, self.projecttype)

class Role(models.Model):
    name = models.CharField(max_length=20)
    def __unicode__(self):
       return self.name

class Membership(models.Model):
    project = models.ForeignKey(Project, null=True)
    department = models.ForeignKey(Department)
    employee = models.ForeignKey(Employee)
    role = models.ManyToManyField(Role, blank=True, null=True)
    class Meta:
        unique_together = (("project", "employee",),)

VIEW:

def employee_edit(request, employee_id):
    i = get_object_or_404(Employee, pk=employee_id)
    MembershipFormSet = modelformset_factory(Membership, exclude=('department', 'employee'),)
    f = MembershipFormSet(queryset=Membership.objects.filter(employee=i),)
    return render_to_response('gcs/edit.html', {'item': i, 'formset': f, }, context_instance=RequestContext(request))

Right now an EU can select a role to play for any departments project. It's acting like this:

Project Options:

Projects.objects.all()

I want to limit the projects with something like this: LIMIT PROJECT CHOCIES TO:

Projects.objects.filter(department=i.department)
like image 779
thedeepfield Avatar asked Mar 16 '12 19:03

thedeepfield


2 Answers

This Stack Overflow question is fairly similar. I like the approach of Matthew's answer, where you build the form dynamically in a function that has access to the employee via closure. In your case, you want something like:

from django.http import HttpResponseRedirect

def make_membership_form(employee):
    """
    Returns a Membership form for the given employee, 
    restricting the Project choices to those in the 
    employee's department. 
    """
    class MembershipForm(forms.ModelForm):
        project = forms.ModelChoiceField(queryset=Projects.objects.filter(department=employee.department))
        class Meta:
            model = Membership
            excludes = ('department', 'employee',)
    return MembershipForm

def employee_edit(request, employee_id):
    employee = get_object_or_404(Employee, pk=employee_id)
    # generate a membership form for the given employee
    MembershipForm = make_membership_form(employee)
    MembershipFormSet = modelformset_factory(Membership, form=MembershipForm)

    if request.method == "POST":
        formset = MembershipFormSet(request.POST, queryset=Membership.objects.filter(employee=employee))
        if formset.is_valid():
            instances = formset.save(commit=False)
                for member in instances:
                    member.employee = employee
                    member.department = employee.department
                    member.save()
            formset.save_m2m()
            # redirect after successful update
            return HttpResponseRedirect("")
    else:
        formset = MembershipFormSet(queryset=Membership.objects.filter(employee=employee),)
    return render_to_response('testdb/edit.html', {'item': employee, 'formset': formset, }, context_instance=RequestContext(request))
like image 57
Alasdair Avatar answered Nov 10 '22 08:11

Alasdair


EDIT

Darn. All that typing because I missed one part of the code ;). As @Alasdair mentions in the comments, you've excluded department from the form, so you can limit this with Django. I'm going to leave my original answer, though, just in case it might help someone else.

For your circumstances, all you need is:

class MembershipForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(MembershipForm, self).__init__(*args, **kwargs)
        self.fields['project'].queryset = self.fields['project'].queryset.filter(department_id=self.instance.department_id)

And, then:

MembershipFormSet = modelformset_factory(Membership, form=MembershipForm, exclude=('department', 'employee'),)

Original Answer (for posterity)

You can't limit this in Django, because the value for department is changeable, and thus the list of projects can vary depending on which particular department is selected at the moment. In order to validate the form, you'll have to feed all possible projects that could be allowed to Django, so your only option is AJAX.

Create a view that will return a JSON response consisting of projects for a particular department fed into the view. Something along the lines of:

from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_list_or_404
from django.utils import simplejson

def ajax_department_projects(request):
    department_id = request.GET.get('department_id')
    if department_id is None:
        return HttpResponseBadRequest()

    project_qs = Project.objects.select_related('department', 'project_type')
    projects = get_list_or_404(project_qs, department__id=department_id)
    data = []
    for p in projects:
        data.append({
            'id': p.id,
            'name': unicode(p),
        })

    return HttpResponse(simplejson.dumps(data), mimetype='application/json')

Then, create a bit of JavaScript to fetch this view whenever the department select box is changed:

(function($){
    $(document).ready(function(){
        var $department = $('#id_department');
        var $project = $('#id_project');

        function updateProjectChoices(){
            var selected = $department.val();
            if (selected) {
                $.getJSON('/path/to/ajax/view/', {department_id: selected}, function(data, jqXHR){
                    var options = [];
                    for (var i=0; i<data.length; i++) {
                        output = '<option value="'+data[i].id+'"';
                        if ($project.val() == data[i].id) {
                            output += ' selected="selected"';
                        }
                        output += '>'+data[i].name+'</option>';
                        options.push(output);
                    }
                    $project.html(options.join(''));
                });
            }
        }

        updateProjectChoices();
        $project.change(updateProjectChoices);
    });
})(django.jQuery);
like image 45
Chris Pratt Avatar answered Nov 10 '22 10:11

Chris Pratt