ModelForm with a reverse ManytoMany field
I'm having trouble getting ModelMultipleChoiceField to display the initial values of a model instance. I haven't been able to find any documentation about the field, and the examples I've been reading are too confusing. Django: ModelMultipleChoiceField doesn't select initial choices seems to be similar, but the solution that was given there is not dynamic to the model instance.
Here is my case (each database user is connected to one or more projects):
models.py
from django.contrib.auth.models import User
class Project(Model):
users = ManyToManyField(User, related_name='projects', blank=True)
forms.py
from django.contrib.admin.widgets import FilteredSelectMultiple
class AssignProjectForm(ModelForm):
class Meta:
model = User
fields = ('projects',)
projects = ModelMultipleChoiceField(
queryset=Project.objects.all(),
required=False,
widget=Filtered开发者_如何学编程SelectMultiple('projects', False),
)
views.py
def assign(request):
if request.method == 'POST':
form = AssignProjectForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
return HttpResponseRedirect('/index/')
else:
form = AssignProjectForm(instance=request.user)
return render_to_response('assign.html', {'form': form})
The form that it returns is not selecting the instance's linked projects (it looks like: Django multi-select widget?). In addition, it doesn't update the user with any selections made when the form is saved.
Edit: Managed to solve this using the approach here: http://code-blasphemies.blogspot.com/2009/04/dynamically-created-modelmultiplechoice.html
Here's a solution that is better than the older ones, which really don't work.
You have to both load the existing related values from the database when creating the form, and save them back when saving the form. I use the set()
method on the related name (manager) which does all the work for you: taking away existing relations that are not selected anymore, and adding new ones which have become selected. So you don't have to do any looping or checking.
class AssignProjectForm(ModelForm):
def __init__(self, *args, **kwargs):
super(AssignProjectForm, self).__init__(*args, **kwargs)
# Here we fetch your currently related projects into the field,
# so that they will display in the form.
self.fields['projects'].initial = self.instance.projects.all(
).values_list('id', flat=True)
def save(self, *args, **kwargs):
instance = super(AssignProjectForm, self).save(*args, **kwargs)
# Here we save the modified project selection back into the database
instance.projects.set(self.cleaned_data['projects'])
return instance
Aside from simplicity, using the set()
method has another advantage that comes into play if you use Django signals (eg. post_save
etc) on your m2m relation: If you add and remove entries one at a time in a loop, you'll get signals for each object. But if you do it in one operation using set()
, you'll get just one signal with a list of objects. If the code in your signal handler does significant work, this is a big deal.
ModelForm
's don't automatically work for reverse relationships.
Nothing is happening on save()
because a ModelForm
only knows what to do with its own fields - projects
is not a field on the User
model, it's just a field on your form.
You'll have to tell your form how to save itself with this new field of yours.
def save(self, *args, **kwargs):
for project in self.cleaned_data.get('projects'):
project.users.add(self.instance)
return super(AssignProjectForm, self).save(*args, **kwargs)
精彩评论