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)
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))
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With