7

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)
Braiam
  • 1
  • 11
  • 47
  • 78
thedeepfield
  • 6,138
  • 25
  • 72
  • 107
  • [This stack overflow question](http://stackoverflow.com/questions/622982/django-passing-custom-form-parameters-to-formset) is quite similar. There are two approaches that work. 1) create a form that takes employee as an argument in its `__init__` method and use the curry function. 2) Build the form class in the view function. If you don't need to reuse the form elsewhere, I find the second approach easier. – Alasdair Mar 16 '12 at 19:52
  • new to python, do you structure the form class in the view just as you would in the model.py? – thedeepfield Mar 16 '12 at 19:59
  • I've expanded my comment as an answer below. – Alasdair Mar 16 '12 at 20:48

2 Answers2

8

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))
Community
  • 1
  • 1
Alasdair
  • 298,606
  • 55
  • 578
  • 516
  • is class MembershipForm suppose to be in the view or in the model? – thedeepfield Mar 16 '12 at 20:40
  • Forms don't really belong in `models.py`. In larger Django apps, you sometimes have a `forms.py` module. In this case, you can put the `make_membership_form` function in the `views.py` if you want. – Alasdair Mar 16 '12 at 20:46
  • I fixed the indentation for the `employee_edit` view. Make sure it's correct in your code. – Alasdair Mar 16 '12 at 20:47
  • Thank you so much for this, I had to modify your code. but all works! i've updated my question to reflect what worked for me. THANK YOU AGAIN!! – thedeepfield Mar 16 '12 at 21:47
  • Glad you got it working! I've updated my answer to include the modifications you made. Note the redirect after a successful update -- it prevents users from resubmitting the request if they hit refresh after a successful update. – Alasdair Mar 16 '12 at 22:04
4

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);
Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • `department` isn't changeable - note `exclude=('department', 'employee'),)`. – Alasdair Mar 16 '12 at 19:50
  • OMG THANK YOU, ive been stuck on this issue for a week. Ive seen a similar answer thrown around but could never fully understand whats going on. can you please tell me what this coding wizard means? references? – thedeepfield Mar 16 '12 at 20:14
  • Also, my fromset has an additional blank form for creating a new membership entry. However the project field in this new form contains no choices... why are my choices missing now? – thedeepfield Mar 16 '12 at 20:18