Need Formset for relationship model with forms for all instances of one ForeignKey
I have a ManyToMany field with a relationship model. I want a formset, filtered on one of the keys, which shows a form for each of the other keys.
My guess is that a custom manager on the relationship model is the key to solving this problem. The manager would return "phantom" instances initialized with the appropriate ForeignKey when no real instance was in the database. I'm just don't know how to make a manager add "phantom" instances when it seems designed to filter out existing ones.
I'm hoping an example is worth 1K words.
Say I want my users to be able to rate albums. I would like to display a formset with a form for all albums by the selected band. Exa开发者_JAVA百科mple models & view
from django.contrib.auth.models import User
from django.db import models
class Band(models.Model):
name = models.CharField(max_length=30)
def __unicode__(self):
return self.name
class Album(models.Model):
name = models.CharField(max_length=30)
band = models.ForeignKey(Band)
ratings = models.ManyToManyField(User, through="Rating")
def __unicode__(self):
return self.name
class Rating(models.Model):
user = models.ForeignKey(User)
album = models.ForeignKey(Album)
rating = models.IntegerField()
def __unicode__(self):
return "%s: %s" % (self.user, self.album)
# views.py
from django.forms.models import modelformset_factory
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template.context import RequestContext
from models import Band, Rating
RatingFormSet = modelformset_factory(Rating, exclude=('user',), extra=1)
def update(request):
user = request.user
band = Band.objects.all()[0]
formset = RatingFormSet(request.POST or None,
queryset=Rating.objects.filter(album__band=band,
user=user))
if formset.is_valid():
objects = formset.save(commit=False)
print "saving %d objects" % len(objects)
for obj in objects:
obj.user = user
obj.save()
return HttpResponseRedirect("/update/")
return render_to_response("rating/update.html",
{'formset': formset, 'band':band},
context_instance=RequestContext(request))
The problem is it only shows forms for existing relationship instances. How can I get an entry for all albums.
Thanks.
I came back to this problem after again searching the web. My intuition that a custom manager was required was wrong. What I needed was a custom inline formset that takes two querysets: one to search and the other one with an ordered list of items to be displayed.
The problem with this technique is that model_formsets really like to have existing instances followed by extra instances. The solution is two make two lists of instances to display: existing records and extra records. Then, after model_formsets creates the forms, sort them back into display order.
To sort the formset forms, you need to apply my django patch [14655] to make formsets iterable & then create a sorting iterator.
The resulting view is shown below:
from django.contrib.auth.models import User
from django.forms.models import inlineformset_factory, BaseInlineFormSet, \
BaseModelFormSet, _get_foreign_key
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template.context import RequestContext
from models import Band, Rating
class list_qs(list):
"""a list pretending to be a queryset"""
def __init__(self, queryset):
self.qs = queryset
def __getattr__(self, attr):
return getattr(self.qs, attr)
class BaseSparseInlineFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
self.display_set = kwargs.pop('display_set')
self.instance_class = kwargs.pop('instance_class', self.model)
# extra is limited by max_num in baseformset
self.max_num = self.extra = len(self.display_set)
super(BaseSparseInlineFormSet, self).__init__(*args, **kwargs)
def __iter__(self):
if not hasattr(self, '_display_order'):
order = [(i, obj._display_order)
for i, obj in enumerate(self._instances)]
order.sort(cmp=lambda x,y: x[1]-y[1])
self._display_order = [i[0] for i in order]
for i in self._display_order:
yield self.forms[i]
def get_queryset(self):
if not hasattr(self, '_queryset'):
# generate a list of instances to display & note order
existing = list_qs(self.queryset)
extra = []
dk = _get_foreign_key(self.display_set.model, self.model)
for i, item in enumerate(self.display_set):
params = {dk.name: item, self.fk.name: self.instance}
try:
obj = self.queryset.get(**params)
existing.append(obj)
except self.model.DoesNotExist:
obj = self.instance_class(**params)
extra.append(obj)
obj._display_order = i
self._instances = existing + extra
self._queryset = existing
return self._queryset
def _construct_form(self, i, **kwargs):
# make sure "extra" forms have an instance
if not hasattr(self, '_instances'):
self.get_queryset()
kwargs['instance'] = self._instances[i]
return super(BaseSparseInlineFormSet, self)._construct_form(i, **kwargs)
RatingFormSet = inlineformset_factory(User, Rating, formset=BaseSparseInlineFormSet)
def update(request):
band = Band.objects.all()[0]
formset = RatingFormSet(request.POST or None,
display_set=band.album_set.all(),
instance=request.user)
if formset.is_valid():
objects = formset.save(commit=False)
print "saving %d objects" % len(objects)
for obj in objects:
obj.save()
return HttpResponseRedirect("/update/")
return render_to_response("rating/update.html",
{'formset': formset, 'band':band},
context_instance=RequestContext(request))
精彩评论