开发者

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))
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜