Multiple Forms and Formsets in CreateView - python

I have 2 models, Father and Son.
I have a page to register Father. On the same page I have a formset to register Son.
On page has a button "more" to add another Father and their respective Son on the same page.
Does anyone have any examples using CreateView?

Class based views are still new, so I'll write this out. The process is simple:
First, create the forms for your objects. One of the forms will be repeated. Nothing special to be done here.
class SonInline(ModelForm):
model = Son
class FatherForm(ModelForm):
model = Father
Then, create your formset:
FatherInlineFormSet = inlineformset_factory(Father,
Son,
form=SonInline,
extra=1,
can_delete=False,
can_order=False
)
Now, to integrate it with your CreateView:
class CreateFatherView(CreateView):
template_name = 'father_create.html'
model = Father
form_class = FatherForm # the parent object's form
# On successful form submission
def get_success_url(self):
return reverse('father-created')
# Validate forms
def form_valid(self, form):
ctx = self.get_context_data()
inlines = ctx['inlines']
if inlines.is_valid() and form.is_valid():
self.object = form.save() # saves Father and Children
return redirect(self.get_success_url())
else:
return self.render_to_response(self.get_context_data(form=form))
def form_invalid(self, form):
return self.render_to_response(self.get_context_data(form=form))
# We populate the context with the forms. Here I'm sending
# the inline forms in `inlines`
def get_context_data(self, **kwargs):
ctx = super(CreateFatherView, self).get_context_data(**kwargs)
if self.request.POST:
ctx['form'] = FatherForm(self.request.POST)
ctx['inlines'] = FatherInlineFormSet(self.request.POST)
else:
ctx['form'] = Father()
ctx['inlines'] = FatherInlineFormSet()
return ctx
Finally, here is the template:
The key part is the jquery django-dynamic-formset plugin that keeps adding new inline forms:
<form id="father-form" method="POST" enctype="multipart/form-data" action=".">
{% csrf_token %}
<div class="row">
{% for f in form %}
<div class="span3">{{ f.label }}<br />{{ f }}
{% if f.errors %}
{% for v in f.errors %}
<br /><span style="color:red;">{{ v }}</span>
{% endfor %}
{% endif %}
</div>
{% endfor %}
</div>
<hr />
<h2>Sons:</h2>
<table class="table-striped">
<table>
{% for f2 in inlines %}
<tr id="{{ f2.prefix }}-row">
{% for i in f2 %}
<td>
{{ i }}{% if i.errors %}<span style="color:red;">{{ i.errors }}</span>{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
{{ inlines.management_form }}
<input type="submit" class="btn btn-primary" value="Go Go Gadget →">
</form>
<script type="text/javascript">
$(function() {
$('#father-form tr').formset({
prefix: '{{ inlines.prefix }}'
});
})
</script>

Related

Django Class based Form only updating the final post value

As the title states, I have a table that I'm attempting to update that only updates the final value in the post, from what I understand if I want to update multiple records I must iterate over my request object and update a form instance with the specified ID in my db.
Before submission all records have a price_checked of 0.
and then after - you can see the final value from the post request updates all the records!
postgres table
The code in question that updates my model form instance.
def post(self,request):
if request.method=='POST':
for key in request.POST.getlist('product_id'):
product_update = ProductTable.objects.get(id=key)
form = ProductUpdateForm(request.POST,instance=product_update)
print(form)
if form.is_valid():
form.save()
messages.success(request,message='price checked...')
return redirect('product')
is anyone able to assist? I've been at this point for over 2 weeks.
models/form/view for reference.
models.py
from django.db import models
class ProductTable(models.Model):
id = models.AutoField(
primary_key=True,
editable=False
)
product_name = models.TextField(max_length=255,null=False)
price = models.DecimalField(max_digits=6,decimal_places=2,null=False)
from_date = models.DateTimeField()
to_date = models.DateTimeField(null=True)
price_checked = models.IntegerField(default=0,null=False)
def __str__(self : str) -> str:
return f"{self.product_name} - {self.id} with a price check of {self.price_checked}"
forms.py
from django.forms import ModelForm
from .models import ProductTable
class ProductUpdateForm(ModelForm):
class Meta:
model = ProductTable
fields = ('price_checked',)
views.py
from typing import Any, Dict
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect
from django.views.generic import ListView
from django.db import transaction
from .forms import ProductUpdateForm
from .models import ProductTable
class TableDataView(LoginRequiredMixin,ListView):
model = ProductTable
context_object_name = 'product'
template_name = 'tables/product.html'
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
context['product'] = ProductTable.objects.filter(pk__in= [4607,4642, 4645])
return context
def post(self,request):
if request.method=='POST':
for key in request.POST.getlist('product_id'):
product_update = ProductTable.objects.get(id=key)
form = ProductUpdateForm(request.POST,instance=product_update)
print(form)
if form.is_valid():
form.save()
messages.success(request,message='price checked...')
return redirect('product')
product.html
{% extends '_base.html' %} {% block content %} {% load crispy_forms_tags %}
<form method="POST" action="">
{% csrf_token %}
{{ form|crispy }}
<div class="container mx-flex pt-5 mb-5">
<table class="table" id="table">
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Date</th>
<th>Input</th>
</tr>
</thead>
<tbody>
{% for data in product %}
<tr>
<th>{{ data.product_name }}</th>
<th>{{ data.price }}</th>
<th>{{ data.from_date }}</th>
<th>
<input type="hidden" name="product_id" value="{{data.id}}" />
<input class="form" name="price_checked" id="priced_checked" type="number" placeholder="{{ data.price_checked }}"/>
<input type="submit" value="OK">
</th>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</form>
{% endblock content %}
request.POST
<QueryDict: {'csrfmiddlewaretoken': ['p0j3e0UbrY1VuFEAnJWopaCICCxOPj8v2OkLRZZiGlUa4YtxGwduD2bAIrm91VKe'], 'product_id': ['4607', '4642', '4645'], 'price_checked': ['1', '2', '3']}>
after read formset documents,I think it should looks like this.
the view python return a formset to template,and in the post function create formset by request.POST
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
ProductFormSet = modelformset_factory(ProductTable)
context['product_formset'] = ProductFormSet(queryset=ProductTable.objects.filter(pk__in= [4607,4642, 4645]))
return context
def post(self,request):
if request.method=='POST':
ProductFormSet = modelformset_factory(ProductTable)
formset = ProductFormSet(request.POST)
if formset.is_valid():
formset.save()
messages.success(request,message='price checked...')
return redirect('product')
in the template I have mark the changes,the important thing is {{ product_formset.management_form }} in the begin form.and {{ form.id }} of each loop,the each cell data should be {{ form.product_name.value() }} or {{ form.product_name.data }},you can try that
{% extends '_base.html' %} {% block content %} {% load crispy_forms_tags %}
<form method="POST" action="">
{% csrf_token %}
{{ form|crispy }}
<!-- changed -->
{{ product_formset.management_form }}
<div class="container mx-flex pt-5 mb-5">
<table class="table" id="table">
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Date</th>
<th>Input</th>
</tr>
</thead>
<tbody>
<!-- changed -->
{% for form in product_formset %}
{{ form.id }}
<tr>
<td>{{ form.product_name.value() }}</td>
<td>{{ form.price.value() }}</td>
<td>{{ form.from_date.value() }}</td>
<td>
{{ form.price_checked }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<input type="submit" value="submit">
</form>
{% endblock content %}
maybe this will not help you,because I never used the django form things.
I guess you are a precise man who want to follow every django's principle.
But as I know,traditional form has much limitation,some ajax or reactjs has more flexible.
A couple changes to support formsets:
Use the same formset for updating and displaying the objects, but disable the fields you don't want to be updated (make them read-only):
class ProductUpdateForm(ModelForm):
disabled_fields = ['product', 'price', 'date_added', 'date_removed']
class Meta:
model = ProductTable
fields = ('product', 'price', 'date_added', 'date_removed', 'price_checked',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.disabled_fields:
self.fields[field].disabled = True
Then update the view to use the form for displaying and for updating:
from django.forms import modelformset_factory
class TableDataView(LoginRequiredMixin,ListView):
model = ProductTable
context_object_name = 'product'
template_name = 'tables/product.html'
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
ProductTableFormset = modelformset_factory(ProductTable, form=ProductUpdateForm,)
context['formset'] = ProductTableFormset(queryset=ProductTable.objects.filter(pk__in=[4607,4642, 4645]))
return context
def post(self,request):
ProductTableFormset = modelformset_factory(ProductTable, form=ProductUpdateForm)
if request.method == 'POST':
formset = ProductTableFormset(request.POST, queryset=ProductTable.objects.filter(pk__in=[4607,4642, 4645]))
if formset.is_valid():
formset.save()
messages.success(request,message='price checked...')
return redirect('product')
And in your template, render the whole formset:
{% extends '_base.html' %}
{% block content %}
{% load crispy_forms_tags %}
<form method="POST" action="">
{% csrf_token %}
{{ form|crispy }}
<div class="container mx-flex pt-5 mb-5">
<table class="table" id="table">
{{ formset }}
</table>
</div>
</form>
{% endblock content %}
EDIT: If you want the simplest change you can do to fix this (so you don't have to work on the styles for the formset), you can do away with the form for the post and update the products as is:
def post(self,request):
if request.method == 'POST':
products_to_update = []
for id, price_checked in zip(request.POST.getlist('product_id'), request.POST.getlist('price_checked')):
product = ProductTable.objects.get(id=id)
product.price_checked = price_checked
products_to_update.append(product)
ProductTable.objects.bulk_update(products_to_update, ['price_checked'])
messages.success(request,message='price checked...')
return redirect('product')

Django Image not uploading

I am unable to have the file in a inlineformset_factory actually upload to the DB or static folder. The form completes then executes succeful_url. Im just not understanding why this isn't uploading the files after submit. Any help is greatly appreciated.
Edit: I am not getting any errors at all.
[12/Mar/2020 15:04:02] "POST /agent/listings/new/ HTTP/1.1" 302 0
[12/Mar/2020 15:04:02] "GET /agent/dashboard/ HTTP/1.1" 200 6915
Model.py
class Listing(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4,editable=False)
agent = models.ForeignKey(CustomUser, on_delete=models.DO_NOTHING)
class Images(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
image = models.ImageField(upload_to="listing/images/")
listing_id = models.ForeignKey(Listing, on_delete=models.DO_NOTHING)
Forms.py
class ListingImage(forms.ModelForm):
class Meta:
model = Images
exclude = ()
ListingImageFormSet = inlineformset_factory(Listing, Images, fields=['image'], form=ListingImage, extra=2)
Views.py
class AgentNewListing(CreateView):
model = Listing
fields = ['agent']
template_name = 'agent/new_listing.html'
success_url = '/agent/dashboard/'
def get_context_data(self, **kwargs):
data = super(AgentNewListing, self).get_context_data(**kwargs)
if self.request.POST:
data['listing_form'] = ListingImageFormSet(self.request.POST)
else:
data['listing_form'] = ListingImageFormSet()
return data
def form_valid(self, form):
context = self.get_context_data()
listingform = context['listing_form']
with transaction.atomic():
form.instance.agent = self.request.user
self.object = form.save(commit=False)
if listingform.is_valid():
listingform.instance = self.object
listingform.save()
return super(AgentNewListing, self).form_valid(form)
def dispatch(self, request, *args, **kwargs):
if not is_valid_agent(self.request.user):
return redirect('/agent/signin/')
return super(AgentNewListing, self).dispatch(request, *args, **kwargs)
Template.htlm
{% extends 'agent/agent_base.html' %}
{% block agent_body %}
{% load crispy_forms_tags %}
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">New Listing</h1>
</div>
<div class="col-sm-6 col-lg-6 ml-2">
<form action="" method="post" enctype="multipart/form-data">{% csrf_token %}
{{ form|crispy }}
<table class="table">
{{ listing_form.management_form|crispy }}
{% for form in listing_form.forms %}
{% if forloop.first %}
<thead>
<tr>
{% for field in form.visible_fields %}
<th>{{ field.label|capfirst }}</th>
{% endfor %}
</tr>
</thead>
{% endif %}
<tr class="">
{% for field in form.visible_fields %}
<td>
{# Include the hidden fields in the form #}
{% if forloop.first %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% endif %}
{{ field.errors.as_ul }}
{{ field }}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
<input class="btn btn-primary" type="submit" value="Submit"/> back to the list
</form>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.formset/1.2.2/jquery.formset.js"></script>
<script type="text/javascript">
$('.formset_row').formset({
addText: 'add family member',
deleteText: 'remove',
prefix: 'familymember_set'
});
</script>
{% endblock %}
Settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
Urls.py
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
The problem was I wasn't calling the self.request.FILES in the inlineformeset. The solution is in the view and looks like this.
def get_context_data(self, **kwargs):
data = super(AgentNewListing, self).get_context_data(**kwargs)
if self.request.POST:
data['listing_form'] = ListingImageFormSet(self.request.POST, self.request.FILES)
else:
data['listing_form'] = ListingImageFormSet()
return data

Django update model entry using form fails

I want to update a model entry using a form. the problem is that instead of updating the entry it creates a new entry.
def edit(request, c_id):
instance = get_object_or_404(C, id=int(c_id))
if request.POST:
form = CForm(request.POST, instance=instance)
if form.is_valid():
form.save()
return redirect('/a/b', c_id)
else:
form = CForm(instance=instance)
args = {}
args.update(csrf(request))
args['form'] = form
args['c_id'] = c_id
return render_to_response('a/b.html', args)
HTML code:
<form action="/a/edit/{{ c_id }}/" method="post">
{% csrf_token %}
{% for field in form %}
<div class="fieldWrapper">
{{ field.errors }}
{{ field.label_tag }} {{ field }}
{% if field.help_text %}
<p class="help">{{ field.help_text|safe }}</p>
{% endif %}
</div>
{% endfor %}
<input type="submit" value="Submit"/>
</form>
CForm class code
class CForm(forms.ModelForm):
class Meta:
model = C
fields = ['name', 'code']
You're checking the request for a POST method incorrectly. request.POST isn't a boolean, it contains a dictionary of post variables and is always going to have the CSRF token in it so it will always be "truthy". What you need is request.method.
Instead of:
if request.POST:
Replace it with:
if request.method == "POST":

Can't get past Django MultiValueDictKeyError on specific form id?

So, everytime I submit my form, I always get this error
MultiValueDictKeyError at /template/ 'form-21-id'
Here is my views.py:
class SomeTemplate(TemplateView):
template_name = 'template/index.html'
def get_context_data(self, **kwargs):
context = super(AttendanceTemplate, self).get_context_data(**kwargs)
instruction = Instruction(self.request.user.username)
sections_list = self.request.GET.getlist('sections_list')
term = self.request.GET.get('term', instruction.term)
enrollments = Enrollment.objects.using('api').prefetch_related('profile').filter(section_id__in=['111111'], term=term)
attendanceQuery = Enrollment.objects.using('api').prefetch_related('student').filter(section_id__in=['111111'], term=term)
#logger.error(dir(attendanceQuery))
#logger.error(attendanceQuery.values())
for enrollment in attendanceQuery:
#logger.error(enrollment.student.first_name)
attendance, created = Attendance.objects.update_or_create(
section_id=enrollment.section_id,
term=enrollment.term,
first_name=enrollment.student.first_name,
last_name=enrollment.student.last_name,
email_address=enrollment.student.email_address,
meeting_date=timezone.now(),
section_info=3,
)
something = Attendance.objects.filter(section_id__in=['111111'], term=term)
formset = AttendanceFormSet(queryset=something)
combined = zip(enrollments, formset)
context['combined'] = combined
context['formset'] = formset
return context
def post(self, request):
formset = AttendanceFormSet(request.POST)
logger.error(formset.errors)
if formset.is_valid():
formset = formset.save();
return render_to_response("template/index.html", {'formset': formset},context_instance=RequestContext(request))
else:
return HttpResponse(request.POST)
Here is my index.html file:
<form method="POST" action="">
{% csrf_token %}
{{ formset.management_form }}
{% for enrollment, form in combined %}
{{ form.id }}
{% for hidden in combined %}
{{ hidden.hidden_fields }}
{% endfor %}
<div class="wrapper-formset">
<div>
{{ form.first_name.value }}
{{ form.last_name.value }}
{{ form.email_address.value }}
</div>
<div class="clear-all"></div>
</div>
{% endfor %}
<button type="submit" class="save btn btn-default">Save</button>
</form>
I've included the following key form properties in my template as well:
form.id
management form
hidden fields
What am I missing? Doing it wrong? DB issue?

UpdateView with Multiple Formset not working

I tried creating a generic UpdateView for a model with generic relations with 2 other models, based on the CreateView provided in this tutorial.
The form and formsets are populated as expected, but when I save I get redirected to the model detail page and changes are not saved.
My question is similar to this one but the suggestions provided are not helpful because they are specific to the former question.
Here is my code :
# models.py
class Ingredient(models.Model):
...
content_type = models.ForeignKey(ContentType, editable=False)
object_id = models.PositiveIntegerField(editable=False)
content_object = GenericForeignKey()
class Step(models.Model):
Same as Ingredient
class Recipe(models.Model):
...
ingredients = generic.GenericRelation(Ingredient)
steps = generic.GenericRelation(Step)
# forms.py
class RecipeForm(forms.ModelForm):
class Meta:
model = Recipe
IngredientFormSet = generic_inlineformset_factory(Ingredient, extra=1)
StepFormSet = generic_inlineformset_factory(Step, extra=1)
# views.py
class RecipeUpdate(UpdateView):
model = Recipe
form_class = RecipeForm
template_name = 'app_recipes/recipe_add.html'
def get(self, request, *args, **kwargs):
self.object = self.get_object()
form_class = self.get_form_class()
form = self.get_form(form_class)
ingredient_form = IngredientFormSet(instance = self.object)
step_form = StepFormSet(instance = self.object)
return self.render_to_response(self.get_context_data(
form=form,
ingredient_form=ingredient_form,
step_form=step_form))
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form_class = self.get_form_class()
form = self.get_form(form_class)
ingredient_form = IngredientFormSet(self.request.POST, instance=self.object)
step_form = StepFormSet(self.request.POST, instance=self.object)
if (form.is_valid() and ingredient_form.is_valid() and step_form.is_valid()):
return self.form_valid(form, ingredient_form, step_form)
else:
return self.form_invalid(form, ingredient_form, step_form)
def form_valid(self, form, ingredient_form, step_form):
self.object = form.save()
ingredient_form.instance = self.object
ingredient_form.save()
step_form.instance = self.object
step_form.save()
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, form, ingredient_form, step_form):
return self.render_to_response(self.get_context_data(
form=form,
ingredient_form=ingredient_form,
step_form=step_form))
def get_success_url(self):
return reverse('recipe_detail', kwargs={'pk': self.object.pk})
I am using the same template used by CreateView :
...
<script src="{{ STATIC_URL }}js/jquery.formset.js"></script> # django-dynamic-formset
<script type="text/javascript">
$(function() {
$(".inline.{{ ingredient_form.prefix }}").formset({
prefix: "{{ ingredient_form.prefix }}",
})
$(".inline.{{ step_form.prefix }}").formset({
prefix: "{{ step_form.prefix }}",
})
})
</script>
...
<form action="." method="post">
{% csrf_token %}
<div>
{{ form.as_p }}
</div>
<fieldset>
<legend>Recipe Ingredients</legend>
{{ ingredient_form.management_form }}
{{ ingredient_form.non_form_errors }}
{% for form in ingredient_form %}
{{ form.id }}
<div class="inline {{ ingredient_form.prefix }}">
{% for field in form.visible_fields %}
<div>
{{ field.errors }}
{{ field.label_tag }}
{{ field }}
</div>
{% endfor %}
</div>
{% endfor %}
</fieldset>
<fieldset>
<legend>Recipe Steps</legend>
{{ step_form.management_form }}
{{ step_form.non_form_errors }}
{% for form in step_form %}
{{ form.id }}
<div class="inline {{ step_form.prefix }}">
{% for field in form.visible_fields %}
<div>
{{ field.errors }}
{{ field.label_tag }}
{{ field }}
</div>
{% endfor %}
</div>
{% endfor %}
</fieldset>
<input type="submit" value="Add recipe" class="submit" />
</form>
...
NB : I use django-dynamic-formset in the template to dynamically add and remove forms in formsets.
Thank you

Categories