Django Formset to respond to survey/questionnaire - python

In short: I have an app where a user inputs workouts. Before they add exercises to their workout, they must submit a 'readiness' questionnaire with 5-10 questions, rating each low-high. I can't work out how to list ALL ReadinessQuestions in a view, and have the user submit an answer for each of them in a formset.
Models: Model with the question set (ReadinessQuestion), and model that stores readiness_question, rating and the Workout it belongs to (WorkoutReadiness).
Form: I've made a ModelForm into a formset_factory. When I set the 'readiness_question' form field widget to be 'readonly', this makes my form invalid and ignores my 'initial' data
View: I want to create a view where the 5-10 Questions from ReadinessQuestion are in a list, with dropdowns for the Answers.
I have set initial data for my form (each different question)
the user selects the 'rating' they want for each readiness_question
a new Workout is instantiated
the WorkoutReadiness is saved, with ForeignKey to the new Workout
How my page it looks now (nearly how I want it to look): https://imgur.com/a/HD1l3oe
The error on submission of form:
Cannot assign "'Sleep'": "WorkoutReadiness.readiness_question" must be a "ReadinessQuestion" instance.
The Django documentation is really poor here, widget=forms.TextInput(attrs={'readonly':'readonly'}) isn't even on the official Django site. If I don't make the readiness_question 'readonly', the user has dropdowns which they can change (this is undesirable).
#models.py
class Workout(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
date = models.DateField(auto_now_add=True, null=True)
date_created = models.DateTimeField(auto_now_add=True, null=True)
def __str__(self):
return f'{self.date}'
class ReadinessQuestion(models.Model):
name = models.CharField(max_length=40, unique=True)
def __str__(self):
return f'{self.name}'
class WorkoutReadiness(models.Model):
class Rating(models.IntegerChoices):
LOWEST = 1
LOWER = 2
MEDIUM = 3
HIGHER = 4
HIGHEST = 5
workout = models.ForeignKey(Workout, on_delete=models.CASCADE, null=True, blank=True)
readiness_question = models.ForeignKey(ReadinessQuestion, on_delete=models.CASCADE, null=True)
rating = models.IntegerField(choices=Rating.choices)
#forms.py
class WorkoutReadinessForm(forms.ModelForm):
class Meta:
model = WorkoutReadiness
fields = ['readiness_question', 'rating',]
readiness_question = forms.CharField(
widget=forms.TextInput(attrs={'readonly':'readonly'})
)
WRFormSet = forms.formset_factory(WorkoutReadinessForm, extra=0)
#views.py
def create_workoutreadiness(request):
questions = ReadinessQuestion.objects.all()
initial_data = [{'readiness_question':q} for q in questions]
if request.method == 'POST':
formset = WRFormSet(request.POST)
workout = Workout.objects.create(user=request.user)
if formset.is_valid():
for form in formset:
cd = form.cleaned_data
readiness_question = cd.get('readiness_question')
rating = cd.get('rating')
w_readiness = WorkoutReadiness(
workout=workout,
readiness_question=readiness_question,
rating=rating
)
w_readiness.save()
return HttpResponseRedirect(reverse_lazy('routines:workout_exercise_list', kwargs={'pk':workout.id}))
else:
formset = WRFormSet(initial=initial_data)
context = {
'questions':questions,
'formset':formset,
}
return render(request, 'routines/workout_readiness/_form.html', context)
ALTERNATIVELY:
I can drop the 'readonly' attr and change the rendering of my HTML. However, this causes formset.is_valid() to be false, as the initial data field is not populated. screenshot of option 2
#forms.py
class WorkoutReadinessForm(forms.ModelForm):
class Meta:
model = WorkoutReadiness
fields = ['readiness_question', 'rating',]
WRFormSet = forms.formset_factory(WorkoutReadinessForm, extra=0)
#views.py
if request.method == 'POST':
formset = WRFormSet(request.POST, initial=initial_data) ###CHANGED THIS####
workout = Workout.objects.create(user=request.user)
print(formset)
if formset.is_valid():
#form.html
<table>
<form action="" method="post">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<tr>
<td>{{ form.initial.readiness_question }}</td>
<td>{{ form.rating }}</td>
</tr>
{% endfor %}
<button type="submit" class="btn btn-outline-primary">Submit</button>
</form>
</table>
ALTERNATIVE #2:
Theoretically I could have my WorkoutReadiness model as a wider model with the 5-10 questions as columns, and 1 row per Workout. I know this goes against tidy data modelling principles, but I can't see a way around this in Django at the moment. Formsets are limiting my project.

I managed to solve this (using alternative no.1). I'm not sure it's the best way, but it works for now:
#models.py - Added blank=True to readiness_question in the WorkoutReadiness model
#forms.py - Removed the widget with attrs={'readonly':'readonly'} - I don't think Django supports this anymore, it certainly doesn't encourage it
class WorkoutReadinessForm(forms.ModelForm):
class Meta:
model = WorkoutReadiness
fields = ['readiness_question','rating']
WRFormSet = forms.formset_factory(WorkoutReadinessForm, extra=0)
#views.py - As well as having the formset populated beforehand with the initial_data, I made sure to add it back into each form individually after validating. The form.cleaned_data.readiness_question field is None, until I replace it with each field in initial_data
#CBV
class WorkoutReadinessCreateView(CreateView):
model = WorkoutReadiness
fields = ['readiness_question','rating']
template_name = 'routines/workout_readiness/_form.html'
initial_data = [{'readiness_question':q} for q in ReadinessQuestion.objects.all()]
def post(self, request, *args, **kwargs):
formset = WRFormSet(request.POST)
if formset.is_valid():
return self.form_valid(formset)
return super().post(request, *args, **kwargs)
def form_valid(self, formset):
workout = Workout.objects.create(user=self.request.user)
for idx, form in enumerate(formset):
cd = form.cleaned_data
readiness_question = self.initial_data[idx]['readiness_question']
rating = cd.get('rating')
w_readiness = WorkoutReadiness(
workout=workout,
readiness_question=readiness_question,
rating=rating
)
w_readiness.save()
return HttpResponseRedirect(
reverse_lazy(
'routines:workout_exercise_list',
kwargs={'pk':workout.id})
)
def get_context_data(self, **kwargs):
"""Render initial form"""
context = super().get_context_data(**kwargs)
context['formset'] = WRFormSet(
initial =self.initial_data
)
return context
#FBV
def create_workoutreadiness(request):
questions = ReadinessQuestion.objects.all()
initial_data = [{'readiness_question':q} for q in questions]
if request.method == 'POST':
formset = WRFormSet(request.POST, initial=initial_data)
workout = Workout.objects.create(user=request.user)
if formset.is_valid():
for idx, form in enumerate(formset):
cd = form.cleaned_data
readiness_question = initial_data[idx]['readiness_question']
rating = cd.get('rating')
w_readiness = WorkoutReadiness(
workout=workout,
readiness_question=readiness_question,
rating=rating
)
w_readiness.save()
return HttpResponseRedirect(reverse_lazy('routines:workout_exercise_list', kwargs={'pk':workout.id}))
else:
formset = WRFormSet(initial=initial_data)
context = {
'questions':questions,
'formset':formset,
}
return render(request, 'routines/workout_readiness/_form.html', context)
#html/template - I render form.initial.readiness_question, which means the user can't edit it in a dropdown
<div class="form-group my-5">
<form action="" method="post">
<table class="mb-3">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<tr>
<td>{{ form.initial.readiness_question }}</td>
<td>{{ form.rating }}</td>
</tr>
{% endfor %}
</table>
<button type="submit" class="btn btn-outline-primary">Submit</button>
</form>
</div>
How it looks - needs tidying up, but the functionality works

Related

Why does one user can add only one record in form in django?

I have created form, in which users can add some kind of information. But, I noticed that one user can add only one object. I mean, after secod time of filling out a form, new infermation displayed, but old record was deleted.
models.py:
from django.db import models
from django.contrib.auth.models import User
class File(models.Model):
customer = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
graf_title = models.CharField('Название файла', max_length = 200)
chart = models.TextField('Значения осей')
title = models.CharField('Название Графика', max_length = 50)
x_title = models.CharField('Название Оси Х', max_length = 50)
y_title = models.CharField('Название Оси Y', max_length = 50)
forms.py:
from .models import File
from django import forms
class FileForm(ModelForm):
class Meta:
model = File
fields = '__all__'
exclude = ['customer']
views.py:
#It is the form view
def main(request):
error = ''
customer = File.objects.get(customer=request.user)
formset = FileForm(request.POST)
if request.method == 'POST':
formset = FileForm(request.POST, instance=customer)
if formset.is_valid():
formset.save()
return redirect('grafic:list')
else:
error = 'Форма была неверной('
formset = FileForm()
data = {
'form': formset,
'error': error,
}
return render(request, 'graf/first.html', data)
#list is the view, which show all user's records
def list(request):
record= File.objects.filter(customer=request.user)
return render(request, 'graf/list.html', {'record': record})
SO as I said, when one user fills out form more than one time, in the list page there are only last added record. I guess that problem in views.py at customer = File.objects.get(customer=request.user). I know that get() give only one object, so I had changed get() to filter(). But I have a error: 'QuerySet' object has no attribute '_meta'
and it specify on **formset = FileForm(request.POST, instance=customer) ** line. I hope that will help you). Thank you in advance.
list.html:
{% for records in record %}
<article class="card">
<div class="card_wrapper">
<figure class="card_feature">
</figure>
<div class="card_box">
<a href="{% url 'grafic:index' records.id %}">
<header class="card_item card_header">
<h6 class="card__item card__item--small card__label">Твой график</h6>
<a class="card__item card__item--small card__title" href="{% url 'grafic:index' records.id %}">
{{records.graf_title}}
</a>
</header>
<hr class="card__item card__divider">
</a>
</div>
</div>
</article>
{% endfor %}
You are editing a record by constructing your FileForm(…, instance=…). If you want to create a new record, you should not pass an instance.
The view thus should look like:
def main(request):
error = ''
if request.method == 'POST':
# no instance=customer
form = FileForm(request.POST, request.FILES)
if form.is_valid():
form.instance.customer = request.user
form.save()
return redirect('grafic:list')
else:
error = 'Форма была неверной('
form = FileForm()
data = {
'form': form,
'error': error,
}
return render(request, 'graf/first.html', data)

Django passing Foreign Keys with multiple forms on a page

I am fairly new to Django and struggling a bit on how to get the primary keys from form input on a multiform view. I cannot get the keys into the database.
I have 3 models: Human, Human_notes, and Location. My form is made of these separate models as forms on one view. I thought I should collect the primary key from each form once saved and apply that to the next form data as a foreign key...
<form action="human-add" method="POST">
{% csrf_token %}
{{ human_form.as_p }}
{{ location_form.as_p }}
{{ human_notes_form.as_p }}
<button type="submit" class="save btn btn-success">Save</button>
</form>
Human has FKs to Location...:
class Human(models.Model):
intaker = models.ForeignKey(User, default=None, on_delete=models.SET_NULL, null=True)
location = models.OneToOneField('Location', on_delete=models.SET_NULL, null=True, related_name='humans')
Human_notes has FK to Human...(maybe this will become an FK in Human but originally thought many notes for one human) :
class HumanNotes(models.Model):
human = models.ForeignKey(Human, on_delete=models.SET_NULL, null=True, related_name='humans_notes')
My view is:
def human_add(request):
if request.method == 'POST':
human_form = HumanForm(request.POST)
location_form = LocationForm(request.POST)
human_notes_form = HumanNotesForm(request.POST)
if human_form.is_valid() and location_form.is_valid() and human_notes_form.is_valid():
human_form.save(commit=False)
location_form.save()
locationKey = location_form.id
human_notes_form.save(commit=False)
human_notes_form.intaker = request.user
human_notes_form.save()
noteKey = human_notes_form.id
human_form.location = locationKey
human_form.note = noteKey
human_form.intaker = request.user
human_form.save()
return redirect('/intake/success-form')
else:
context = {
'human_form': human_form,
'location_form': location_form,
'human_notes_form': human_notes_form,
}
else:
context = {
'human_form': HumanForm(),
'location_form': LocationForm(),
'human_notes_form': HumanNotesForm(),
}
return render(request, 'intake/human-add.html', context)
The only error I am getting is that 'LocationForm' object has no attribute 'id' - but I even added it explicitly (thought I should not have to and don't want it visible):
class HumanNotesForm(forms.ModelForm):
class Meta:
model = HumanNotes
fields = ['id','intaker','note']
class LocationForm(forms.ModelForm):
class Meta:
model = Location
fields = ['id','st1','st2','cty','county','state','zip','lat','long','img','img_nm','img_id']
Any guidance appreciated.
This did the trick....as did getting excellent guidance from forum.djangoproject.com. Needed to understand the diff between forms and models.
def human_add(request):
if request.method == 'POST':
human_form = HumanForm(request.POST)
location_form = LocationForm(request.POST)
human_notes_form = HumanNotesForm(request.POST)
if human_form.is_valid() and location_form.is_valid() and human_notes_form.is_valid():
loc = location_form.save()
hum = human_form.save(commit=False)
humnote = human_notes_form.save(commit=False)
hum.intaker = request.user
hum.location = loc #.id NOTE YOU NEED THE ENTIRE INSTANCE
human_form.save()
humnote.intaker = request.user
humnote.human = hum #.id NOTE YOU NEED THE ENTIRE INSTANCE
human_notes_form.save()
return redirect('/intake/success-form')
else:
context = {
'human_form': human_form,
'location_form': location_form,
'human_notes_form': human_notes_form,
}
else:
context = {
'human_form': HumanForm(),
'location_form': LocationForm(),
'human_notes_form': HumanNotesForm(),
}
return render(request, 'intake/human-add.html', context)

create a distinct model option in a dropdown using django

I have created this application but the problem I face now is one that has kept me up all night. I want users to be able to see and select only their own categories when they want to create a post. This is part of my codes and additional codes would be provided on request
category model
class Category(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, default=1,related_name='categories_created')
name = models.CharField(max_length = 120)
slug = models.SlugField(unique= True)
timestamp = models.DateTimeField(auto_now=False, auto_now_add=True)
post model
class Post(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, default=1,related_name='posts_created') #blank=True, null=True)
title = models.CharField(max_length = 120)
slug = models.SlugField(unique= True)
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='category_created', null= True)
addition codes would be provided immediately on request. Thanks
View.py in post app
def create(request):
if not request.user.is_authenticated():
messages.error(request, "Kindly confirm Your mail")
#or raise Http404
form = PostForm(request.POST or None, request.FILES or None)
user = request.user
categories = Category.objects.filter(category_created__user=user).distinct()
if form.is_valid():
instance = form.save(commit=False)
instance.user = request.user
instance.save()
create_action(request.user, 'Posts', instance)
messages.success(request, "Post created")
return HttpResponseRedirect(instance.get_absolute_url())
context = {
"form": form,
}
template = 'create.html'
return render(request,template,context)
Form
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = [
"title",
"content",
"category",
]
html
{% if form %}
<form method="POST" action="" enctype="multipart/form-data">{% csrf_token %}
{{ form|crispy|safe }}
<input type="submit" name="submit" value="Publish">
</form>
{% endif %}
What you need to do is well-described here. Basically, you are using ModelForm which generates the form from your model. Your model doesn't know anything about filtering by user, so you will need to explicitly add a QuerySet to your form that only shows the desired categories. Change your "categories = ..." line to something like:
form.category.queryset = Category.objects.filter(user=user)
form.fields['category'].queryset = Category.objects.filter(user=user)</strike>

Using formset with ContentType

I'm having trouble using the UpdateView for a view consisting of a form and formset.
I have the following models: Item and Picture.
Picture is defined as:
class Picture(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255, blank=False)
content_type = models.ForeignKey(ContentType, verbose_name="content type",
related_name="content_type_set_for_%(class)s")
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey("content_type", "object_id")
I have several models that contain pictures. For example, in the Item model:
class Item(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255, blank=False)
pictures = generic.GenericRelation(Picture)
I have the following ItemCreateForm:
class ItemCreateForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ItemCreateForm, self).__init__(*args, **kwargs)
class Meta:
model = Item
The PictureForm:
class PictureForm(forms.ModelForm):
id = forms.IntegerField(widget=forms.HiddenInput)
def __init__(self, *args, **kwargs):
super(PictureForm, self).__init__(*args, **kwargs)
def save(self):
data = self.cleaned_data
obj = Picture(**data);
# do something to obj
# obj.save()
class Meta:
model = Picture
fields = ['id', 'name']
And the view:
class ItemUpdateView(UpdateView):
form_class = ItemCreateForm
template_name = 'item/new.html'
model = Item
success_url = '/items/'
def get_context_data(self, **kwargs):
context = super(ItemUpdateView, self).get_context_data(**kwargs)
item = context['object']
# Dont' create any extra forms when showing an update view
PictureFormSet = formset_factory(PictureForm, extra=0)
return {'form': kwargs['form'],
'picture_formset': UploadFormSet(initial = [ model_to_dict(a) for pic in item.pictures.all()])}
def post(self, request, *args, **kwargs):
self.object = self.get_object()
item_form = ItemCreateForm(request.POST, instance=self.object)
if item_form.is_valid():
item = item_form.save(commit=False)
item.save()
# How do update the pictures?
This is my urls.py:
url(r'^items/(?P<pk>\d+)/update/$', ItemUpdateView.as_view(), name='item_update')
The template:
<form action="" method="post" enctype="multipart/form-data">
{% for field in form %}
# do something
{% endfor %}
{{ picture_formset.management_form }}
{% for form in picture_formset.forms %}
# do something
{% endfor %}
<input name="commit" type="submit" value="Submit" />
</form>
I'm new to Django.
The user can dynamically(via jQuery) add/remove pictures through the Picture form in the single template that is used to display the item and multiple pictures.
1 I had to include the id as a hidden field for the picture, otherwise the pictures will be inserted instead of an Update. QN: Is there a better way to do this?
2 How do I update the picture model? Currently request.POST doesn't have all the fields in the model, thus the model is complaining of NULL fields? I'm totally at lost how to deal with formset in an UpdateView and is not the main form, like a simple example of UpdateView with the pk in the url.
PictureFormSet = formset_factory(PictureForm)
picture_formset = PictureFormSet(request.POST, request.FILES)
for picture_form in picture_formset.forms:
picture_form.save()

How to display objects in a drop down list (django)?

python=2.7, django=1.11.13
In my html I am not able to display my condition choices from my models.py
When filling the form, the user is not able to choose a condition because they are not displayed.
models.py
class Books(models.Model):
book_name = models.CharField(max_length=100)
book_condition = models.ForeignKey('Condition')
def __unicode__(self):
return self.book_name
class Condition(models.Model):
NEW = 'new'
USED = 'used'
COLLECTIBLE = 'collectible'
CONDITION_CHOICES = (
(NEW, 'New'),
(USED, 'Used'),
(COLLECTIBLE, 'collectible'),
)
book = models.ForeignKey(Books)
condition = models.CharField(max_length=10, choices=CONDITION_CHOICES)
def __unicode__(self):
return self.condition
views.py
def add_book(request):
if request.method == 'GET':
context = {
'form': BookForm()
}
if request.method == 'POST':
form = BookForm(request.POST)
if form.is_valid():
form.save()
context = {
'form': form,
}
return render(request, 'add_book_form.html', context=context)
add_book_form.html
{% extends 'base.html' %}
{% block body %}
<h3>Add Book </h3>
<form action="" method="post">
{% csrf_token %}
{{ form}}
<br/>
<input class="button" type="submit" value="Submit"/>
</form>
{% endblock %}
And this is my form, I'm not sure what I am missing.
form
from django.forms import ModelForm
from .models import Books, Condition
class BookForm(ModelForm):
class Meta:
model = Books
fields = '__all__'
class ConditionForm(ModelForm):
class Meta:
model = Condition
fields = '__all__'
The form you're passing to the view is a BookForm, the BookForm contains a ForeignKey field to the Condition model, so the options in the select will be instances of the Condition model.
You would need to preemptively create the Condition model instances, via the admin interface or the shell, and then you could see the conditions on the select, but that won't help, because your Condition instance needs to be associated to a Book, and that makes me think your software is badly designed.
Let me propose a solution:
class Book(models.Model):
"""
This model stores the book name and the condition in the same
table, no need to create a new table for this data.
"""
NEW = 0
USED = 1
COLLECTIBLE = 2
CONDITION_CHOICES = (
(NEW, 'New'),
(USED, 'Used'),
(COLLECTIBLE, 'Collectible'),
)
name = models.CharField(max_length=100)
condition = models.SmallIntegerField(choices=CONDITION_CHOICES)
def __unicode__(self):
return "{0} ({1})".format(self.book_name, self.condition)
class BookForm(ModelForm):
class Meta:
model = Book
fields = '__all__'
Now the conditions are saved as an integer (like it would be if you used foreign keys) and your software is easier to understand and develop.
Try to use Django widgets. For example:
class BookForm(forms.Form):
categories = (('Adventure', 'Action'),
('Terror', 'Thriller'),
('Business', 'War'),)
description = forms.CharField(max_length=9)
category = forms.ChoiceField(required=False,
widget=forms.Select,
choices=categories)

Categories

Resources