Set form input as variable in get_queryset - django

AIM
I wish to render results.html with all the locations associated with the search_text entered by the User.
To do so, I am attempting to filter the results in get_quersey(), but am struggling to set the form_input variable. I am currently using form_input = self.request.GET.get('search_text').
CODE
models.py
import re
from django.db import models
from twython import Twython
class Location(models.Model):
""" Model representing a Location (which is attached to Hashtag objects
through a M2M relationship) """
name = models.CharField(max_length=1400)
def __str__(self):
return self.name
class Hashtag(models.Model):
""" Model representing a specific Hashtag serch by user """
search_text = models.CharField(max_length=140, primary_key=True)
locations = models.ManyToManyField(Location, blank=True)
def __str__(self):
""" String for representing the Model object (search_text) """
return self.search_text
def display_locations(self):
""" Creates a list of the locations attached to the Hashtag model """
return list(self.locations.values_list('name', flat=True).all())
forms.py
from django import forms
from django.forms import ModelForm
from django.utils.translation import ugettext_lazy as _
from .models import Location, Hashtag
class SearchHashtagForm(ModelForm):
''' ModelForm for user to search by hashtag '''
def clean_hashtag(self):
data = self.cleaned_data['search_text']
# Check search_query doesn't include '#'. If so, remove it.
if data[0] == '#':
data = data[1:]
return data
class Meta:
model = Hashtag
fields = ['search_text',]
labels = {'search_text':_('Hashtag Search'), }
help_texts = { 'search_text': _('Enter a hashtag to search.'), }
views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic
from django.views.generic.edit import FormView
from .models import Location, Hashtag
from .forms import SearchHashtagForm
class HashtagSearch(FormView):
""" FormView for user to enter hashtag search query """
template_name = 'mapping_twitter/hashtag_search_form.html'
form_class = SearchHashtagForm
def get_success_url(self):
return reverse('mapping_twitter:results')
def form_valid(self, form):
# the method is called when valid form data has been POSTed, and returns an HttpResponse
form.clean_hashtag()
form.save()
return super().form_valid(form)
def form_invalid(self, form):
# Check if the search_text is invalid because it already exists in the database. If so, render results.html with that search_text
search_text = self.request.POST.get('search_text')
if search_text and Hashtag.objects.filter(pk=search_text).exists():
return HttpResponseRedirect(reverse('mapping_twitter:results'))
else:
return super(HashtagSearch, self).form_invalid(form)
class SearchResultsView(generic.ListView):
""" Generic class-based view listing search results of locations """
model = Hashtag
template_name = 'mapping_twitter/results.html'
def get_queryset(self, **kwargs):
# ISSUE: restrict the search_text displayed on results.html to the 'search_text' entered by the user
qs = Hashtag.objects.all()
form_input = self.request.GET.get('search_text')
if form_input:
qs = qs.filter(search_text__iexact=form_input)
return qs
def get_context_data(self, **kwargs):
context = super(SearchResultsView, self).get_context_data(**kwargs)
context['search_text'] = Hashtag.objects.all()
return context

In get_context_data you don't use get_queryset method at all. You just write another query which fetch all Hashtag objects. You should rewrite it to this:
def get_context_data(self, **kwargs):
context = super(SearchResultsView, self).get_context_data(**kwargs)
context['search_text'] = self.get_queryset()
return context
Note self.get_queryset() is using instead of Hashtag.objects.all().
Also note to use self.request.GET.get('search_text') you need to pass search_text as GET argument when performing redirect in HashtagSearch view:
class HashtagSearch(FormView):
""" FormView for user to enter hashtag search query """
template_name = 'mapping_twitter/hashtag_search_form.html'
form_class = SearchHashtagForm
def get_success_url(self):
return '{}?search_text={}'.format(reverse('mapping_twitter:results'), self.request.POST.get('search_text'))
UPD
But isntead of two views I recommend you to use single ListView. Just add GET form to your results.html and simple override get_queryset method:
# template
<form method="GET" action="">
<input type="text" name="search_text" placeholder="Search post" value="{{ request.GET.search_text }}">
<input type="submit" value="Search">
</form>
# view
class SearchResultsView(generic.ListView):
""" Generic class-based view listing search results of locations """
model = Hashtag
template_name = 'mapping_twitter/results.html'
def get_queryset(self, **kwargs):
# ISSUE: restrict the search_text displayed on results.html to the 'search_text' entered by the user
qs = Hashtag.objects.all()
form_input = self.request.GET.get('search_text')
if form_input:
qs = qs.filter(search_text__iexact=form_input)
return qs

Related

The view awesomeinventory.supplier.views.supplierCreateView didn't return an HttpResponse object. It returned None instead in Create view

My code is:
views.py
class supplierListView(LoginRequiredMixin, ListView):
template_name = "supplier/Supplier_list.html"
def get_queryset(self):
organisation = self.request.user.userprofile.company
return Supplier.objects.filter(organisation=organisation)
class supplierCreateView(LoginRequiredMixin, CreateView):
template_name = "supplier/supplier_create.html"
form_class = SupplierModelForm
def get_success_url(self):
return reverse("supplier:supplier_list")
def form_valid(self, form):
supplier = form.save(commit=False)
supplier.organisation = self.request.user.userprofile.company
supplier.supplier_created_by = self.request.user
supplier.save()
my urls:
from awesomeinventory.supplier.views import (
supplierListView,
supplierDetailView,
supplierCreateView,
supplierContactListView,
supplierContactCreateView,
supplierContactDetailView,
)
app_name = "supplier"
urlpatterns = [
path("supplier_list/", view=supplierListView.as_view(), name="supplier_list"),
path("supplier_create/", view=supplierCreateView.as_view(), name="supplier_create"),
path("<int:pk>/detail/", view=supplierDetailView.as_view(), name="supplier_detail"),
path("<int:pk>/update/", view=supplierDetailView.as_view(), name="supplier_update"),
path("<int:pk>/delete/", view=supplierDetailView.as_view(), name="supplier_delete"),
path("supplierContact_list/", view=supplierContactListView.as_view(), name="supplierContact_list"),
path("<int:suppk>/supplierContact_create/", view=supplierContactCreateView.as_view(), name="supplierContact_create"), # int is supplier_id
path("<int:pk>/Contact/detail/", view=supplierContactDetailView.as_view(), name="supplierContact_detail"),
]
I m able to go to supplier:supplier_list page and it works well.
But when I want to create a supplier with supplierCreateView, supplier is create but it seems to have an issue with get_success_url as I have error
The view awesomeinventory.supplier.views.supplierCreateView didn't return an HttpResponse object. It returned None instead
The method form_valid is supposed to return the response or redirect the user. In your implementation you only save the object and return nothing essentially returning None which gives you an error. Instead of using form.save(commit=False) you can simply modify the instance wrapped by the form and leave all the processing to the super classes form_valid:
class supplierCreateView(LoginRequiredMixin, CreateView):
template_name = "supplier/supplier_create.html"
form_class = SupplierModelForm
def get_success_url(self):
return reverse("supplier:supplier_list")
def form_valid(self, form):
form.instance.organisation = self.request.user.userprofile.company
form.instance.supplier_created_by = self.request.user
return super().form_valid(form)
Note: A class name should ideally be in PascalCase so SupplierCreateView instead of supplierCreateView
This will work for you.
from django.urls import reverse_lazy
class supplierCreateView(LoginRequiredMixin, CreateView):
template_name = "supplier/supplier_create.html"
form_class = SupplierModelForm
def get_success_url(self):
# pay attention I'm using reverse_lazy instead of reverse
return reverse_lazy("supplier:supplier_list")
You can read more about reverse_lazy here.

Form input to a template

EXPECTED OUTPUT
Assuming that the user enters "anaconda" in the Form:
Hashtag Search Results
You searched for: anaconda
ACTUAL OUTPUT
Hashtag Search Results
1) You searched for: <QuerySet [<Hashtag: >, <Hashtag: anaconda>]>
CODE
models.py
from django.db import models
class Location(models.Model):
""" Model representing a Location, attached to Hashtag objects through a
M2M relationship """
name = models.CharField(max_length=1400)
def __str__(self):
return self.name
class Hashtag(models.Model):
""" Model representing a specific Hashtag serch by user """
search_text = models.CharField(max_length=140, primary_key=True)
locations = models.ManyToManyField(Location, blank=True)
def __str__(self):
""" String for representing the Model object (search_text) """
return self.search_text
def display_locations(self):
""" Creates a list of the locations """
# Return a list of location names attached to the Hashtag model
return self.locations.values_list('name', flat=True).all()
forms.py
from django import forms
from django.forms import ModelForm
from django.utils.translation import ugettext_lazy as _
from .models import Location, Hashtag
class SearchHashtagForm(ModelForm):
""" ModelForm for user to search by hashtag """
def clean_hashtag(self):
data = self.cleaned_data['search_text']
# Check search_query doesn't include '#'. If so, remove it.
if data[0] == '#':
data = data[1:]
# return the cleaned data
return data
class Meta:
model = Hashtag
fields = ['search_text',]
labels = {'search_text':_('Hashtag Search'), }
help_texts = { 'search_text': _('Enter a hashtag to search.'), }
views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic
from .models import Location, Hashtag
from .forms import SearchHashtagForm
def hashtag_search_index(request):
""" View function for user to enter search query """
# If POST, process Form data
if request.method == 'POST':
# Create a form instance and populate it with data from request (binding):
form = SearchHashtagForm(request.POST)
# Check if form is valid
if form.is_valid():
search_text = form.cleaned_data['search_text']
form.save()
# redirect to a new URL
return HttpResponseRedirect(reverse('mapping_twitter:results'))
# If GET (or any other method), create the default form
else:
form = SearchHashtagForm()
context = {'form':form, 'search_text':Hashtag.search_text}
return render(request, 'mapping_twitter/hashtag_search_index.html', context)
class SearchResultsView(generic.ListView):
""" Generic class-based view listing search results of locations """
model = Hashtag
template_name = 'mapping_twitter/results.html'
def get_queryset(self, **kwargs):
qs = super(SearchResultsView, self).get_queryset()
return qs
def get_context_data(self, **kwargs):
context = super(SearchResultsView, self).get_context_data(**kwargs)
context['search_text'] = Hashtag.objects.all()
return context
results.html
<h1>Hashtag Search Results</h1>
<p>1) You searched for: {{ search_text }}</p>
{% for obj in queryset %}
<p>2) You searched for: {{ obj.search_text }}</p>
{% endfor %}
I think you should pass the queryset of the Model HashTag as you need to render out hashtag.search_text which is an element of that model.
So you can pass the queryset in the view and for loop through it and print all the search_text or pass the object alone and render out its search_text.
context = {
'queryset': qs
}
{% for obj in queryset %}
<p>{{ obj.search_text }}</p>
{% endfor %}
This is my guess, correct me if I am wrong.
For a template to display something, you have to send context or data to that template to print, how can it print something when you don't send anything. So,
context.update({
'hashtag': //value you want to send.
})

Value Not getting saved in my models

I have a model named INPUT in my models.py. It has many-to-many field
My problem is that my values are not getting saved in the database or table names INPUT
I don't know where i am wrong.
My models.py is:
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Person(models.Model):
title=models.CharField(max_length=1000,unique=True)
cost=models.CharField(max_length=100)
desc=models.TextField(blank=True)
color=models.CharField(max_length=1000)
image_urls=models.CharField(max_length=1000)
source=models.CharField(max_length=1000,unique=True)
category=models.CharField(max_length=1000)
size=models.CharField(max_length=1000)
fit=models.CharField(max_length=1000)
fabric=models.CharField(max_length=1000)
type_dress=models.CharField(max_length=1000)
currency=models.CharField(max_length=1000)
advertiser=models.CharField(max_length=1000)
def __unicode__(self):
return self.title
class foroccasion(models.Model):
occasions=models.CharField(max_length=2000)
def __unicode__(self):
return self.occasions
class forstyle(models.Model):
style=models.CharField(max_length=2000)
def __unicode__(self):
return self.style
class forbodytype(models.Model):
bodytype=models.CharField(max_length=2000)
def __unicode__(self):
return self.bodytype
class Input(models.Model):
tabbody=[
("apple","apple"),
("pear/triangle","pear/triangle"),
("hourglass","hourglass"),
("rectangle","rectangle"),
("inverted triangle","invered triangle")
]
tabstyle=[
("vintage","vintage"),
("sophisticated","sophisticated"),
("chic","chic"),
("bone","bone"),
("edgy","edgy")
]
# apparelid=models.(max_length=104)
apparelid=models.ForeignKey('Person')
# apparelid=models.CharField(max_length=100,blank=True)
userid=models.CharField(max_length=100,blank=True,editable=False)
occassion=models.ManyToManyField(foroccasion)
bodytype=models.ManyToManyField(forbodytype)
style=models.ManyToManyField(forstyle)
def __unicode__(self):
return self.userid
My forms.py is as follows:
import re
from django import forms
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from apple3.models import models
from django.forms import ModelForm
from apple3.models import Input,Person
class UserForm(ModelForm):
class Meta:
model=Input
My views.py is as follows:
def title(request,title_id):
cur=0
cur=Person.objects.get(pk=title_id)
# imge=Person.objects.all()[:109]
# for i in range(1,len(imge)-2):
# if i==int(title_id):
# cur=imge[i]
sz=str(cur.size)
sz=sz.split(',')
fsz=[]
for i in sz:
s=""
for j in range(0,len(i)):
if i[j]!='U' and i[j]!='K' and i[j]!=',':
s+=i[j]
fsz.append(s)
lsz=len(fsz)
#form=UserForm(request.POST)
if request.method == 'POST':# If the form has been submitted...
print "dsflkjdfslkjdsl"
form=UserForm(request.POST)# A form bound to the POST data
if form.is_valid(): # All validation rules pass
# Process the data in form.cleaned_data
# ...
#form.cleaned_data()
new=form.save(commit=False)
new.userid=request.user
new.occassion=self.cleaned_data["occassion"]
new.bodytype=self.cleaned_data["bodytype"]
new.style=self.cleaned_data["style"]
new.apparelid=self.cleaned_data["apparelid"]
new.save()
form.save_m2m()
else:
form = UserForm() # An unbound form"""
template=loader.get_template('apple3/title.html')
context=RequestContext(request,{
'cur':cur,
'sz':fsz,
'lsz':lsz,
'form':form,
}
)
return HttpResponse(template.render(context))
my template is as follow:
<form action="/dress/female/" method="post">{% csrf_token %}
{{form.as_p}}
<input type="submit" value="Submit" />
</form>
Also i have tried displaying individual elements of the form but it is not getting saved in the database
Please help me
i am new to django.
Add something like this on your form:
YOURFORM(ModelForm):
class Meta:
model=Yourmodel
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(yourform, self).__init__(*args, **kwargs)
def save(self, commit=True):
instance = super(yourform, self).save(commit=False)
if self.user:
instance.user = self.user
return instance.save()
In my example I set the USER field as a value I call from views like this:
form = myForm(request.POST,user=request.user)

Django combine DetailView and FormView

I have a view where I need to display information about a certain model instance hence I use a DetailView. I also need that same view to handle a regular form (not a model form), both displaying the form on GET and validating it on POST. To do that, I am trying to use a FormView however the combination of both view clases does not work:
class FooView(FormView, DetailView):
# configs here
In GET (for simplicity of the question I will only show the issue with GET since POST has a different issue), it does not work because the form never gets added to the context. The reason has to do with method resolution order for that class:
>>> inspect.getmro(FooView)
(FooView,
django.views.generic.edit.FormView,
django.views.generic.detail.DetailView,
django.views.generic.detail.SingleObjectTemplateResponseMixin,
django.views.generic.base.TemplateResponseMixin,
django.views.generic.edit.BaseFormView,
django.views.generic.edit.FormMixin,
django.views.generic.detail.BaseDetailView,
django.views.generic.detail.SingleObjectMixin,
django.views.generic.base.ContextMixin,
django.views.generic.edit.ProcessFormView,
django.views.generic.base.View,
object)
Within the request, Django has to get the form and add it to the context. That happens in ProcessFormView.get:
def get(self, request, *args, **kwargs):
"""
Handles GET requests and instantiates a blank version of the form.
"""
form_class = self.get_form_class()
form = self.get_form(form_class)
return self.render_to_response(self.get_context_data(form=form))
However the first class with the MRO which has get defined is BaseDetailView:
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
As you can see the BaseDetailView.get never calls super hence the ProcessFormView.get will never be called hence the the form will not be added to the context. This can be fixed by creating a mixin view where all these nuances for GET and POST can be taken care of however I do not feel it is a clean solution.
My question: is there any way of accomplishing what I want with Django's default CBV implementation without creating any mixins?
One solution would be to use mixins, as per limelights' comment above.
Another approach is to have two separate views, one a DetailView and the other a FormView. Then, in the template for the former, display the same form you're using in the latter, except that you won't leave the action attribute empty -- instead, set it to the url for the FormView. Something along the lines of this (please beware of any errors as I'm writing this without any testing):
In views.py:
class MyDetailView(DetailView):
model = MyModel
template_name = 'my_detail_view.html'
def get_context_data(self, **kwargs):
context = super(MyDetailView, self).get_context_data(**kwargs)
context['form'] = MyFormClass
return context
class MyFormView(FormView):
form_class = MyFormClass
success_url = 'go/here/if/all/works'
In my_detail_view.html:
<!-- some representation of the MyModel object -->
<form method="post" action="{% url "my_form_view_url" %}">
{{ form }}
</form>
In urls.py:
# ...
url('^my_model/(?P<pk>\d+)/$', MyDetailView.as_view(), name='my_detail_view_url'),
url('^my_form/$', require_POST(MyFormView.as_view()), name='my_form_view_url'),
# ...
Note that the require_POST decorator is optional, in the case that you don't want the MyFormView to be accessible by GET and want it only to be processed when the form is submitted.
Django also has a rather lengthy documentation about this problem.
https://docs.djangoproject.com/en/1.8/topics/class-based-views/mixins/#using-formmixin-with-detailview
They advise to make 2 different views, and have the detail view refer to the form view on post.
I'm currently seeing if this hack might work:
class MyDetailFormView(FormView, DetailView):
model = MyModel
form_class = MyFormClass
template_name = 'my_template.html'
def get_context_data(self, **kwargs):
context = super(MyDetailFormView, self).get_context_data(**kwargs)
context['form'] = self.get_form()
return context
def post(self, request, *args, **kwargs):
return FormView.post(self, request, *args, **kwargs)
By using FormMixin
views.py
from django.contrib.auth import get_user_model
from django.core.urlresolvers import (
reverse_lazy
)
from django.http import Http404
from django.shortcuts import (
render,
redirect
)
from django.views.generic import (
DetailView,
FormView,
)
from django.views.generic.edit import FormMixin
from .forms import SendRequestForm
User = get_user_model()
class ViewProfile(FormMixin, DetailView):
model = User
context_object_name = 'profile'
template_name = 'htmls/view_profile.html'
form_class = SendRequestForm
def get_success_url(self):
return reverse_lazy('view-profile', kwargs={'pk': self.object.pk})
def get_object(self):
try:
my_object = User.objects.get(id=self.kwargs.get('pk'))
return my_object
except self.model.DoesNotExist:
raise Http404("No MyModel matches the given query.")
def get_context_data(self, *args, **kwargs):
context = super(ViewProfile, self).get_context_data(*args, **kwargs)
profile = self.get_object()
# form
context['form'] = self.get_form()
context['profile'] = profile
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
#put logic here
return super(ViewProfile, self).form_valid(form)
def form_invalid(self, form):
#put logic here
return super(ViewProfile, self).form_invalid(form)
forms.py
from django import forms
class SendRequestForm(forms.Form):
request_type = forms.CharField()
def clean_request_type(self):
request_type = self.cleaned_data.get('request_type')
if 'something' not in request_type:
raise forms.ValidationError('Something must be in request_type field.')
return request_type
urls.py
urlpatterns = [
url(r'^view-profile/(?P<pk>\d+)', ViewProfile.as_view(), name='view-profile'),
]
template
username -{{object.username}}
id -{{object.id}}
<form action="{% url 'view-profile' object.id %}" method="POST">
{% csrf_token %}
{{form}}
<input type="submit" value="Send request">
</form>
In Django By Example from lightbird, they're using a library, MCBV, to mix generic views:
My guide’s tutorials will use a library of class-based views based on modified Django generic views; the library is called MCBV (M stands for modular) and the main difference compared to generic CBVs is that it’s possible to mix and match multiple generic views easily (e.g. ListView and CreateView, DetailView and UpdateView, etc.)
You can follow the explanation here: helper-functions
And use it to mix FormView and DetailView, or whatever
Code: MCBV
I performed my solution using ModelForms and something like this:
On method get_context_data of my DetailView I made:
form = CommentForm(
instance=Comment(
school=self.object, user=self.request.user.profile
)
)
context['form'] = form
And my FormView was like:
class SchoolComment(FormView):
form_class = CommentForm
def get_success_url(self):
return resolve_url('schools:school-profile', self.kwargs.get('pk'))
def form_valid(self, form):
form.save()
return super(SchoolComment, self).form_valid(form)
That's a old post but good for reference.
One elegant and reusable away is to use a custom inclusion tag for the form.
templatetags/my_forms_tag.py
from django import template
from ..forms import MyFormClass
register = template.Library()
#register.inclusion_tag('<app>\my_form.html')
def form_tag(*args, **kwargs):
my_form = MyFormClass()
return {'my_form ':my_form}
my_form.html
<form method="post" action="{% url "my_form_view_url" %}">
{{ form }}
</form>
The post will be taken by FormView werever view you put inclusion tag. And it can receive any context you pass in the inclusion. Dont forget to load my_form_tag and create the view for MyForm and include the entry in urls.py

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. Example 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))