Django Nested formsets for models - django

I'm writing a program to store analyses for patient and I need to make a form with nested forests with next scheme
Each patient has an analyses results sheet
Each result sheet has date, patient (foreign key) and set of analyses values
Each set of analyses values has strict number and strict types of analyses
Each type of analyse has it's value, name and units
For example I want to create John's analyse result sheet for blood
Patient: John
Date: 10.02.23
Set of values: 'Blood analysis'
Red blood cells: 3,23 10^9
Haemoglobin: 124 g/l
I've made models
patient/models.py
class Patient(models.Model):
hist_number = models.IntegerField(unique=True, verbose_name='Номер истории болезни')
last_name = models.CharField(max_length=32, verbose_name='Фамилия')
analysis/models.py
class PatientAnalysisResultSheet(models.Model):
an_number = models.PositiveBigIntegerField(verbose_name='Номер исследования', unique=True)
date = models.DateField(verbose_name='Дата анализа')
time = models.TimeField(verbose_name='Время')
patient = models.ForeignKey('patient.Patient', verbose_name='Пациент', on_delete=models.CASCADE)
class AnalysisType(models.Model):
name = models.CharField(max_length=256, verbose_name='Исследование', unique=True)
measurement = models.CharField(max_length=10, verbose_name='Единицы измерения')
class PatientAnalysis(models.Model):
sheet = models.ForeignKey(PatientAnalysisResultSheet, on_delete=models.CASCADE)
analysis = models.ForeignKey(AnalysisType, verbose_name='Лабораторный показатель',
on_delete=models.CASCADE)
value = models.FloatField(verbose_name='Значение')
So I googled about formsets and now did next steps:
views.py
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views.generic import FormView
# Create your views here.
from django.views.generic.detail import SingleObjectMixin
from analysis.forms import PatientAnSheetFormset
from patient.models import Patient
class PatientAnSheet(SingleObjectMixin, FormView):
model = Patient
template_name = 'analysis/patient_ansheets.html'
def get(self, request, *args, **kwargs):
self.object = self.get_object(queryset=Patient.objects.all())
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object(queryset=Patient.objects.all())
return super().post(request, *args, **kwargs)
def get_form(self, form_class = None):
return PatientAnSheetFormset(**self.get_form_kwargs(), instance=self.object)
def form_valid(self, form):
form.save()
messages.add_message(
self.request,
messages.SUCCESS,
'Изменения сохранены добавлен!'
)
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return reverse('patient-detail', kwargs={'pk':self.object.pk})
forms.py:
from django.forms import inlineformset_factory
from patient.models import Patient
from analysis.models import PatientAnalysisResultSheet
PatientAnSheetFormset = inlineformset_factory(Patient, PatientAnalysisResultSheet, fields='__all__', extra=1)
But now I'm stuck as I don't know how to create a set of values and how to net it to the form. Appreciate

Related

Creating a form wizard / formset to answer questions from a table and saving them into another (Survey)

I am new to Django. I apologize if I don't make much sense. I have an EU project to create a survey tool for adult educators who are assisting migrants and refugees. I would like to use a WizardView/SessionView together with formset to answer a set of 24 questions from a matrix based on Max-Neef's Human Needs theory and Self Determination Theory. The idea is that Survey model will inherit 24 questions with ForeignKey one by one in a WizardView with previous/next/save functions as the user populate reflection field save them into the survey table. I have been pondering how might I save code instead of creating 24 tables for wizard view and use a hybrid of formsets and form_tools.
How might one create such a tool?
Any help will be most appreciated. Thank you in advance.
I tried ModelForms and class based Views following several tutorials on the web and paid websites.
models.py
from django.urls import reverse
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from django.forms import ModelChoiceField
class QuestionChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return "Question: {}".format(obj.name)
def formfield_for_foreignkey(self, question, request, **kwargs):
if question.name == 'question':
return QuestionChoiceField(queryset=Question.objects.all())
return super().formfield_for_foreignkey(question, request, **kwargs)
class Question(models.Model):
LEVELS = (
('Self', 'Relationship with myself'),
('Peers', 'Relationship with my peers'),
('Learners', 'Relationship with my learners'),
('Workplace', 'Relationship with my workplace'),
('Society', 'Relationship with my community'),
('Planet', 'Relationship with the global society'),
)
COMPETENCES = (
('Life Internal', 'Life Internal'),
('Life External', 'Life External'),
('Relatedness', 'Relatedness'),
('Autonomy', 'Autonomy'),
)
level = models.CharField(max_length=64, choices=LEVELS)
competence = models.CharField(max_length=64, choices=COMPETENCES)
question = models.TextField(max_length=400)
objects = models.Manager()
class Meta:
ordering = ['competence']
verbose_name = "Question"
verbose_name_plural = "Questions"
def __str__(self):
return f'{self.question}'
class Survey(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE,
related_name="surveys", verbose_name="Survey Question:")
reflection = models.TextField(max_length=400)
started_at = models.DateTimeField(default=timezone.now)
is_complete = models.BooleanField(verbose_name="Completed?")
author = models.ForeignKey(User, related_name="surveys", on_delete=models.CASCADE, verbose_name="Author:")
objects = models.Manager()
class Meta:
ordering = ['author']
verbose_name = "Survey"
verbose_name_plural = "Surveys"
def __str__(self):
return f'{self.author.username}' ': ''\n' \
f'{self.question}' '\n' \
f'{self.reflection}'
def get_indexed_objects(self):
return Survey.objects.filter(live=True)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('Survey-detail', kwargs={'pk': self.pk})
forms.py
from django import forms
from SDAM.models import Survey, Question
from selectable.forms import AutoComboboxSelectWidget
from .lookups import QuestionLookup
class QuestionForm(forms.ModelForm):
def clean(self):
super(QuestionForm, self).clean()
class Meta:
model = Question
fields = ['question', 'level', 'competence']
widgets = {
'level': AutoComboboxSelectWidget(lookup_class=QuestionLookup),
'competence': AutoComboboxSelectWidget(lookup_class=QuestionLookup),
}
class SurveyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class Meta:
model = Survey
fields = ['question', 'reflection', 'author', 'started_at', 'is_complete']
widgets = {
'level': AutoComboboxSelectWidget(lookup_class=QuestionLookup),
'competence': AutoComboboxSelectWidget(lookup_class=QuestionLookup),
'question': AutoComboboxSelectWidget(lookup_class=QuestionLookup),
}
def clean(self):
# Super the clean method to maintain main validation and error messages
super(SurveyForm, self).clean()
question = self.cleaned_data.get('question')
author = self.cleaned_data.get('author')
reflection = self.cleaned_data.get('reflection')
started_at = self.cleaned_data.get('started_at')
is_complete = self.cleaned_data.get('is_complete')
survey = Survey.objects.get(question=question,
reflection=reflection,
author=author, started_at=started_at,
is_complete=is_complete)
return survey.save()
def save(self, commit=True):
instance = super(SurveyForm, self).save(commit=False)
if commit:
# save
instance.save(update_fields=['question', 'reflection', 'author', 'started_at', 'is_complete'])
return instance
views.py
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.models import User
from django.utils import timezone
from django.shortcuts import get_object_or_404, render, reverse
from django.views.generic import (CreateView, DeleteView, DetailView, ListView, UpdateView)
from .filters import SurveyFilter
from .forms import SurveyForm
from .models import Survey
"""
#login_required
def home(request):
context = {
'surveys': Survey.objects.all()
}
return render(request, 'SDAM/survey.html', context)
"""
class SurveyListView(LoginRequiredMixin, ListView):
model = Survey
form_class = SurveyForm
context_object_name = 'survey_list'
ordering = ['-started_at']
template_name = "SDAM/survey_list.html"
paginate_by = 3
def get_queryset(self, *args, **kwargs):
return super().get_queryset()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['is_complete'] = True
context['surveys'] = Survey.objects.all()
return context
class UserSurveyListView(LoginRequiredMixin, ListView):
model = Survey
context_object_name = 'user_surveys'
template_name = 'SDAM/user_surveys.html' # <app>/<model>_<viewtype>.html
paginate_by = 3
def get_queryset(self):
author = get_object_or_404(User, username=self.kwargs.get('author'))
return Survey.objects.filter(author=author, is_complete=True).order_by('-started_at')
def get_context_data(self, **kwargs):
context = super(UserSurveyListView, self).get_context_data(**kwargs)
context['user_surveys'] = Survey.objects.all()
context['is_complete'] = True
context['filter'] = SurveyFilter(self.request.GET, queryset=self.queryset())
return context
class SurveyDetailView(LoginRequiredMixin, DetailView):
model = Survey
template_name = "SDAM/survey_detail.html"
queryset = Survey.objects.all()
def get_object(self):
obj = super().get_object()
# Record the last accessed date
obj.last_accessed = timezone.now()
obj.save()
return obj
class SurveyCreateView(LoginRequiredMixin, CreateView):
model = Survey
fields = ['question', 'reflection', 'started_at', 'is_complete']
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class SurveyUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Survey
fields = ['reflection', 'started_at', 'is_complete']
template_name = "SDAM/survey_update.html"
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
def test_func(self):
survey = self.get_object()
if self.request.user == survey.author:
return True
return False
class SurveyDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = Survey
success_url = '/sdam/'
def test_func(self):
survey = self.get_object()
if self.request.user == survey.author:
return True
return False
lookups.py
from selectable.base import LookupBase
from selectable.registry import registry
from .models import Survey, Question
class QuestionLookup(LookupBase):
model = Question
search_fields = ('level', 'competence', 'question',)
def get_query(self, request, term):
data = ['Level', 'Competence', 'Question']
return [x for x in data if x.startswith(term)]
class SurveyLookup(LookupBase):
model = Survey
search_fields = ('question', 'reflection', 'author', 'started_at', 'is_complete',)
def get_query(self, request, term):
data = ['Question', 'Reflection', 'Author', 'Started_at', 'Is_complete']
return [x for x in data if x.startswith(term)]
registry.register(QuestionLookup)
registry.register(SurveyLookup)
filters.py
import django_filters
from .models import Survey, Question
class SurveyFilter(django_filters.FilterSet):
class Meta:
model = Survey
fields = ("question", "reflection", "author", "started_at", "is_complete",)
class QuestionFilter(django_filters.FilterSet):
class Meta:
model = Question
fields = ("competence", "level", "question",)
admin.py
from ckeditor.widgets import CKEditorWidget
from selectable.forms import AutoComboboxSelectWidget, AutoCompleteSelectField, AutoCompleteSelectMultipleWidget
from django.forms import ModelChoiceField
from django.contrib import admin
from django import forms
from SDAM.models import Survey, Question
from .lookups import QuestionLookup, SurveyLookup
class QuestionChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return "Question: {}".format(obj.name)
def formfield_for_foreignkey(self, question, request, **kwargs):
if question.name == 'question':
return QuestionChoiceField(queryset=Question.objects.all())
return super().formfield_for_foreignkey(question, request, **kwargs)
class QuestionAdminForm(forms.ModelForm):
question = forms.CharField(widget=CKEditorWidget())
class Meta:
model = Question
fields = ['level', 'competence', 'question']
widgets = {
'level': AutoComboboxSelectWidget(lookup_class=QuestionLookup),
'competence': AutoComboboxSelectWidget(lookup_class=QuestionLookup),
}
class SurveyAdminForm(forms.ModelForm):
reflection = forms.CharField(widget=CKEditorWidget())
class Meta:
model = Survey
fields = ['question', 'reflection', 'author', 'started_at', 'is_complete']
widgets = {
'question': AutoCompleteSelectField(lookup_class=SurveyLookup),
'level': AutoComboboxSelectWidget(lookup_class=QuestionLookup),
'competence': AutoComboboxSelectWidget(lookup_class=QuestionLookup),
}
#admin.register(Question)
class QuestionAdmin(admin.ModelAdmin):
list_display = ['question', 'competence', 'level', "id"]
list_filter = ['competence', 'level']
fieldsets = [
("Question details", {"fields": [
"competence", "level", "question"]}),
]
def question(self):
return f'{self.question}'
#admin.register(Survey)
class SurveyAdmin(admin.ModelAdmin):
list_display = ["author", "question", "reflection", "started_at", "is_complete"]
list_filter = ["author", "question", "is_complete"]
fieldsets = [
("Reflection Details", {"fields": ["question", "reflection", "author", "started_at", "is_complete"]}),
]
def survey_author(self):
return f'{self.author.username}'
urls.py
from django.urls import path
from .views import (
SurveyListView,
UserSurveyListView,
SurveyDetailView,
SurveyCreateView,
SurveyUpdateView,
SurveyDeleteView,
)
# from . import views
urlpatterns = [
path('sdam/', SurveyListView.as_view(), name='Survey-list'),
path('user/<str:username>', UserSurveyListView.as_view(), name='User-surveys'),
path('sdam/<int:pk>/', SurveyDetailView.as_view(), name='Survey-detail'),
path('sdam/new/', SurveyCreateView.as_view(), name='Survey-create'),
path('sdam/<int:pk>/update/', SurveyUpdateView.as_view(), name='Survey-update'),
path('sdam/<int:pk>/delete/', SurveyDeleteView.as_view(), name='Survey-delete'),
]

Set form input as variable in get_queryset

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

defining multiple variabless within a form issue

Hi guys I have something that I do not know how to make it work.
I am working on a questionnaire like, where on a page will show up the questionn and a radio like form list of answers that the user can select.
I gave it a try but I do not know how to link a question to my form, could you please help ?
models.py:
from django.db import models
# Create your models here.
class Questionnaire(models.Model):
question = models.CharField(max_length=600)
multiple_answer = models.BooleanField(default=False)
def __str__(self):
return self.question
class Answers(models.Model):
question_id = models.ForeignKey('Questionnaire', on_delete=models.CASCADE)
answer = models.CharField(max_length = 600)
def __str__(self):
return self.answer
class Test(models.Model):
user = models.ForeignKey('registration.MyUser')
created_at = models.TimeField(auto_now_add=True)
closed = models.BooleanField(default=False)
class UserAnswer(models.Model):
test_id = models.ForeignKey('Test')
question = models.ForeignKey('Questionnaire')
answer = models.ForeignKey('Answers')
views.py:
def AnswerChoiceSelect(request):
#import pdb; pdb.set_trace()
if request.method == "POST":
answer_form = QuestionChoices(request.POST)
if answer_form.is_valid():
print("working")
else:
print('Non Valid form')
else:
answer_form = QuestionChoices(request.user)
return render(request,'question_template.html',
{'answer_form':answer_form })
forms.py:
from django import forms
from django.contrib.auth.models import User
from registration.models import MyUser
from .models import Questionnaire, Answers, Test, UserAnswer
from django.contrib.auth import get_user_model
User = get_user_model()
class QuestionChoices(forms.Form):
question_choice = forms.ModelChoiceField(widget=forms.RadioSelect, queryset=None)
def __init__(self,questionnaire, *args, **kwargs):
super(QuestionChoices, self).__init__(*args, **kwargs)
current_question = Questionnaire.objects.get(id = question_id)
self.fields['question_choice'].queryset = Answers.objects.filter(question_id = Current_question.id)

Django inline formset, once all inlines full, the form creates an extra entry and saves it to the db

Here is my code,
views.py:
from django.views.generic import TemplateView, View, UpdateView
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect
from django.core.urlresolvers import reverse
PurchaserChoiceFormset = inlineformset_factory(Worksheet, PurchaserChoice, form=PurchaserChoiceForm, can_delete=False,extra=5, max_num=5)
from models import Worksheet
from forms import PurchaserChoiceFormset
class WorksheetStep1View(TemplateView):
template_name = 'worksheets/step1.html'
def get_context_data(self, **kwargs): # Exec 1st
context = super(WorksheetStep1View, self).get_context_data(**kwargs)
context['worksheet'] = Worksheet.objects.get(pk=self.kwargs.get('worksheet_id'))
context['choice_formset'] = PurchaserChoiceFormset(self.request.POST or None, instance=context['worksheet'])
return context
def post(self, request, *args, **kwargs):
context = self.get_context_data()
if context['choice_formset'].is_valid():
context['choice_formset'].instance = context['worksheet']
context['choice_formset'].save()
return super(WorksheetStep1View, self).render_to_response(context)
models.py:
from django.db import models
class Worksheet(models.Model):
completed_date = models.DateTimeField(null=True, blank=True)
class PurchaserChoice(models.Model): # Choice made by the purchaser for model/floor
model = models.CharField(blank=True, null=True, max_length=255)
floor = models.CharField(blank=True, null=True, max_length=255) # coming from API
worksheet = models.ForeignKey('worksheets.Worksheet')
def __unicode__(self):
return "PurchaseChoiceID {0}, WorksheetID {1} - Model: {2} - Floor: {3}".format(self.id, self.worksheet.id,
self.model, self.floor, )
class Meta:
#ordering = ('-model', 'floor')
ordering = ('-id', )
I want only up to 5 instances of the purchase choice to exist, but after the form is full and saved again, a new entry is created. If i get rid of context['choice_formset'].instance = context['worksheet'] , it is even worse, it creates a new entry for every instance in the formset.
Can anyone help me figure out why this is happening? I also looked at UpdateView but seems like its got more things to it than I need and I don't think its the cause to my problem. I am not sure why its creating new entries instead of updating once the form has 5 entries in it and saved again.
One way I avoid this problem is just to delete all the choices on save each time, like so:
def post(self, request, *args, **kwargs):
context = self.get_context_data()
if context['choice_formset'].is_valid():
PurchaserChoice.objects.filter(worksheet=context['worksheet']).delete()
#context['choice_formset'].instance = context['worksheet']
context['choice_formset'].save()
return super(WorksheetStep1View, self).render_to_response(context)
So what I found wrong was that I was returning the wrong render_to_response (of the parent class with the context) which caused me to create new objects every time because the instance was lost. This is what fixed it:
return self.render_to_response(context)
instead of:
return super(WorksheetStep1View, self).render_to_response(context)
And this also did not work:
return TemplateResponse(request, self.template_name, context)
I'll see if I can get a better idea of why TemplateResponse did not work.

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