I'm trying to set up crispy form on a form and a formset (Visit and VisitService).
I'm having trouble attaching the helper to the formset, no matter how I do it.
I added helper as an attribute to the Formset and added the helper to view and context but it keeps giving me the following error:
VariableDoesNotExist - Failed lookup for key [helper]
(also tried [helper_attribute])
here is what I'm using:
forms.py:
class VisitForm(forms.ModelForm):
class Meta:
model = models.Visit
fields = [
[...all visit fields go here...]
]
def __init__(self, *args, **kwargs):
super(VisitForm, self).__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.layout = Layout(
[...fields go here...]
),
ButtonHolder(
Submit('submit', 'Submit', css_class='button white btn-wide')
)
)
class VisitServiceForm(forms.ModelForm):
class Meta:
model = models.VisitService
fields = [
'service',
'unit',
]
def __init__(self, *args, **kwargs):
super(VisitServiceForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Row(
Div(Field('service'),css_class='col-sm-4'),
Div(Field('unit'),css_class='col-sm-4'),
)
VisitServiceFormSet = forms.modelformset_factory(
models.VisitService,
form=VisitServiceForm,
extra=2,
)
VisitServiceInlineFormSet = forms.inlineformset_factory(
models.Visit,
models.VisitService,
extra=5,
fields=('service', 'unit'),
formset=VisitServiceFormSet,
min_num=1,
)
views.py:
def create_visit(request, patient_pk):
patient = get_object_or_404(models.Patient, pk=patient_pk)
form_class = forms.VisitForm
form = form_class()
visitservice_forms = forms.VisitServiceInlineFormSet(
queryset=models.VisitService.objects.none()
)
helper = forms.VisitServiceForm()
if request.method == 'POST':
form = form_class(request.POST)
visitservice_forms = forms.VisitServiceInlineFormSet(
request.POST,
queryset=models.VisitService.objects.none()
)
if form.is_valid() and visitservice_forms.is_valid():
visit = form.save(commit=False)
visit.patient = patient
visit.save()
visitservices = visitservice_forms.save(commit=False)
for visitservice in visitservices:
visitservice.visit = visit
visitservice.save()
messages.success(request, "Added visit")
return HttpResponseRedirect(visit.get_absolute_url())
return render(request, 'fpform/visit_form.html', {
'patient': patient,
'form': form,
'formset': visitservice_forms,
'helper': helper,
})
template:
<div class="container">
<form method="POST" action="">
{% crispy form %}
{% crispy formset formset.form.helper_attribute %}
</div>
</form>
in my template I've also used each one of these separately with no luck:
{% crispy formset formset.form.helper %}
{% crispy formset helper_attribute %}
{% crispy formset helper %}
{% crispy formset form.helper_attribute %}
{% crispy formset form.helper %}
I looked at the crispy documentation but couldn't find the answer there.
A whole day spent on this but it hasn't gone anywhere. Also feel free to let me know if there is a better way to achieve this.
To define the layout of formsets you need to create an independent FormHelper class and pass it to the form.
http://django-crispy-forms.readthedocs.io/en/latest/crispy_tag_formsets.html
# forms.py
class VisitServiceFormHelper(FormHelper):
def __init__(self, *args, **kwargs):
super(VisitServiceFormHelper, self).__init__(*args, **kwargs)
self.layout = Layout(
Div(Field('service'),css_class='col-sm-4'),
Div(Field('unit'),css_class='col-sm-4'),
)
#views.py
formset = VisitServiceInlineFormSet(queryset=VisitService.objects.none())
helper = VisitServiceFormHelper()
return render(request, 'template.html', {'formset': formset, 'helper': helper})
#template
{% crispy formset helper %}
Related
I am trying to create a page where various data corresponding to mutliple models can be input by the user, and to have an option to dynamically add additional forms. I have been attempting to use htmx for this and am able to dynamically add forms, however when I save it is only the last entered form that is saved. I haven't used formsets as this wont integrate well with htmx https://justdjango.com/blog/dynamic-forms-in-django-htmx#django-formsets-vs-htmx.
Code below any suggestion as to how to get all the dynamically created forms to be save would be most appreciated!
models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=50)
class Book(models.Model):
author = models.ForeignKey(Author, on_delete=models.CASCADE)
title = models.CharField(max_length=100)
forms.py
from django import forms
from .models import Book, Author
from crispy_forms.helper import FormHelper
class AuthorForm(forms.ModelForm):
class Meta:
model = Author
fields = ['name']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'id-CaseForm'
self.helper.form_class = 'blueForms'
self.helper.form_method = 'post'
self.helper.form_tag = False
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ('title',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_tag = False
self.helper.form_show_labels = False
views.py
class create_book(TemplateView):
template_name = 'create_book.html'
def get(self, *args, **kwargs):
author_form = AuthorForm
book_form = BookForm
return self.render_to_response(
{'book_form': BookForm,
"author_form": AuthorForm,}
)
def post(self, *args, **kwargs):
author_form = AuthorForm(data=self.request.POST)
book_form = BookForm(data=self.request.POST)
if author_form.is_valid():
author = author_form.save()
if book_form.is_valid():
book = book_form.save(commit=False)
book.author = author
book.save()
def create_book_form(request):
form = BookForm()
context = {
"form": form
}
return render(request, "partials/book_form.html", context)
urls.py
urlpatterns = [
path('create_book/', create_book.as_view(), name='create-book'),
path('htmx/create-book-form/', create_book_form, name='create-book-form'),]
create_book.html
{% extends "base.html" %}
{% block content %}
{% load crispy_forms_tags %}
<form class="blueForms" id="id-CaseForm" method="post" >
{% crispy author_form %}
{% crispy book_form %}
<button type="button" hx-get="{% url 'create-book-form' %}" hx-target="#bookforms" hx-swap="beforeend">
Add book form
</button>
<div id="bookforms"></div>
</form>
partials/book_form.html
{% load crispy_forms_tags %}
<div hx-target="this" hx-swap="outerHTML">
{% csrf_token %}
{% crispy form %}
{{ form.management_form }}
</div>
A quick Google finds me this module:
https://github.com/adamchainz/django-htmx
It seems someone may have already done this for you my friend :)
And thanks in advance for any suggestions. I have been playing around with how to properly implement formsets with a CreateView for a couple of days and I'm stuck. Here is my code.
My Models:
class Team(models.Model):
team_name = models.CharField(max_length=264,null=False,blank=False)
class Player(models.Model):
player_name = models.CharField(max_length=264,null=False,blank=False)
team = models.ForeignKey(Team,null=True,on_delete=models.CASCADE)
My View:
class CreateTeamView(LoginRequiredMixin,CreateView):
model = Team
form_class = CreateTeamForm
template_name = 'create_team.html'
def get_context_data(self, **kwargs):
context = super(CreateTeamView, self).get_context_data(**kwargs)
if self.request.POST:
context['new_player'] = NewPlayerFormSet(self.request.POST)
else:
context['nwe_player'] = NewPlayerFormSet()
return context
def get_form_kwargs(self, *args, **kwargs):
kwargs = super(CreateTeamView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
context = self.get_context_data()
new_player_form = context['new_player']
if new_player_form.is_valid():
self.object = form.save()
new_player_form.instance = self.object
new_player_form.save()
instance = form.save()
else:
return self.render_to_response(self.get_context_data(form=form))
My Forms:
class CreateTeamForm(forms.ModelForm):
class Meta:
model = Team
exclude = []
NewPlayerFormSet = inlineformset_factory(Team, Player, extra=1, fields=['player_name',])
My HTML:
<div="players>
<div="add_players">
{{ new_player.management_form }}
{% for form in new_player %}
{{ form.id }}
{{ form.player_name }}
</div>
</div>
My form is saving one player, but when I try to update the code to save more than one player with the initial CreateView, it only recognizes the first contact. I have overridden the BaseInlineFormset to do validation as shown below....
class NewPlayerFormSet(NewPlayerFormSet,BaseInlineFormSet):
player_name = forms.CharField(required=True,widget=forms.TextInput)
def add_fields(self, form, index):
super(NewPlayerFormSet,self).add_fields(form,index)
form.fields['player_name'].required = False
def clean(self):
super(NewPlayerFormSet, self).clean()
for form in self.forms:
if form.cleaned_data.get('player_name'):
pass
else:
form.add_error('player_name','Player Name is required.')
pass
I'm trying to get the code to save a second contact. I have used tried various JQuery attempts....but am unclear if my problem is with JQuery or mayby my HTML templates? That's where I'm stuck.
I tried to do something like...
$(document).ready(function() {
// Watch for the 'add player' click
$('#add_player').click(function(e) {
e.preventDefault();
$('div.add_player:last').clone().each(function(i) {
$(this).find('input,select').each(function(i) {
// Remove any existing values
$(this).val('');
}).appendTo('div#players');
});
});
And while this works to duplicate the form, the players beyond number 1 are not being saved. Not sure what I'm doing incorrectly.
It would appear there is a JQuery plugin for this, but I'm trying to avoid using it for a number of reasons. Thanks again for any help to point me in the right direction.
This was not easy. I spent about a week trying to piece this all together. Here are all of the parts that I used to finally make it work. I ultimately did wind up using jquery.formset.js from GitHub in my solution. Hope I save someone a week.
class Team(models.Model):
team_name = models.CharField(max_length=264,null=False,blank=False)
class Player(models.Model):
player_name = models.CharField(max_length=264,null=False,blank=False)
team = models.ForeignKey(Team,null=True,on_delete=models.CASCADE)
My Views.py
class CreateTeamView(LoginRequiredMixin,CreateView):
model = Team
form_class = TeamForm
template_name = 'create_team.html'
def get(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
player_form = CreatePlayerFormSet()
return self.render_to_response(
self.get_context_data(form=form,
player_form=player_form,
))
def form_valid(self, form, player_form):
self.object = form.save()
player_form.instance = self.object
player_form.save()
instance = form.save()
def form_invalid(self, form, player_form):
return self.render_to_response(
self.get_context_data(form=form,
player_form=player_form,
))
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
player_form = CreatePlayerFormSet(self.request.POST)
if (form.is_valid() and player_form.is_valid()):
return self.form_valid(form, player_form)
else:
return self.form_invalid(form, player_form)
My Forms.py
class CreateTeamForm(forms.ModelForm):
class Meta:
model = Team
exclude = [ ]
CreatePlayerFormSet = inlineformset_factory(Team, Player, extra=1, fields=(['player_name'])
My HTML Template: ( Using jquery/jquery.formset.js )
<script src="{% static 'jquery/jquery.formset.js' %}"></script>
<script type="text/javascript">
$(function() {
$(".inline.{{ player_form.prefix }}").formset({
prefix: "{{ player_form.prefix }}",
})
})
</script>
<form method="POST" enctype="multipart/form-data" id="forms">
{% csrf_token %}
{{ player_form.management_form }}
{{ player_form.non_form_errors }}
{% for form in player_form %}
{{ form.id }}
<div class="inline {{ player_form.prefix }}">
<div class="leftwidth22">
<div class="width52">
<h2 class="floatright23">Player Name - </h2>
</div>
</div>
<div class="rightwidth53">
<h2 class="width70">
{{ form.player_name }}
</h2>
</div>
{% endfor %}
I'm receiving the following error when I click the save button on my inline formset using Crispy Forms:
[{'id': ['This field is required.']}, {'id': ['This field is required.']}, {'id': ['This field is required.']}, {}, {}]
The formset is bound but not valid because of the missing id, but I'm not sure how to set the id.
#views.py
class View(LoginRequiredMixin, TemplateView):
template_name = "example.html"
MyFormSet = modelformset_factory(
model=MyModel,
form=MyModelForm,
formset=MyModelFormset,
can_delete=True,
extra=1,
fields=('field_1','field_2', ))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
my_formset = self.MyFormSet()
context['my_formset'] = MyModel.objects.all().order_by('field_1')
return context
def post(self, request, *args, **kwargs):
my_formset = self.MyFormSet(request.POST, request.FILES)
if my_formset.is_valid():
try:
my_formset.save()
except:
messages.add_message(request, messages.ERROR, 'Cannot delete: this parent has a child 1 !')
else:
context = self.get_context_data()
context['my_formset'] = my_formset
return render(request, self.template_name, context)
return HttpResponseRedirect(reverse_lazy("example"))
#forms.py
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ['field_1', 'field_2']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
Row(
Column('field_1'),
Column('field_2'),
Column('DELETE'),
)
)
#template
<form action="" enctype="multipart/form-data" method="post">{% csrf_token %}
{{ my_formset.management_form|crispy }}
{% for form in my_formset.forms %}
{% crispy form form.helper %}
{% endfor %}
<button class="btn btn-success" type="submit">Save</button>
</form>
When rendering forms with layouts as part of formset, you must set render_hidden_fields = true. See more notes in the documentation.
https://django-crispy-forms.readthedocs.io/en/latest/form_helper.html
Good Evening,
Im having trouble with a crispy forms inlineformset. I have followed guides as per:
https://github.com/timhughes/django-cbv-inline-formset/blob/master/music/views.py
https://django-crispy-forms.readthedocs.io/en/latest/crispy_tag_formsets.html#formsets
EDIT
I think the issue is something to do with the dual submit buttons. the devicemodel form has a button that when pressed produces this error. but there is also a save button as part of the resource helper, when that's submitted I get an empty model form error.
I've added screenshots of what happens when you action each button
and I must be missing something as am getting the error:
['ManagementForm data is missing or has been tampered with']
here is my update view:
class EditDeviceModel(PermissionRequiredMixin, SuccessMessageMixin, UpdateView):
model = DeviceModel
form_class = DeviceModelForm
template_name = "app_settings/base_formset.html"
permission_required = 'config.change_devicemodel'
success_message = 'Device Type "%(model)s" saved successfully'
def get_success_url(self, **kwargs):
return '{}#device_models'.format(reverse("config:config_settings"))
def get_success_message(self, cleaned_data):
return self.success_message % dict(
cleaned_data,
model=self.object.model,
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title']='Edit Device Model'
if self.request.POST:
context['formset'] = DeviceFormSet(self.request.POST, instance=self.object)
else:
context['formset'] = DeviceFormSet(instance=self.object)
context['helper'] = DeviceFormSetHelper()
return context
def form_valid(self, form):
context = self.get_context_data()
formset = context['formset']
if formset.is_valid():
self.object = form.save()
formset.instance = self.object
formset.save()
return redirect(self.success_url)
else:
return self.render_to_response(self.get_context_data(form=form))
Here are my forms:
class MonitoredResourceForm(forms.ModelForm):
class Meta:
model = MonitoredResource
fields = ['resource','model']
def __init__(self, *args, **kwargs):
self.is_add = kwargs.pop("is_add", False)
super(MonitoredResourceForm, self).__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.form_id = 'snmp_resource_form'
self.helper.form_method = 'POST'
self.helper.layout = Layout(
Div(
Div(
Field('model'),
Field('resource', placeholder="Resource"),
css_class='col-lg-3'
),
css_class='row'
),
Div(
Div(
HTML("""<input type="submit" name="submit" value="""),
HTML('"Add' if self.is_add else '"Update' ),
HTML(""" monitored resource" class="btn btn-primary"/>"""),
HTML("""Cancel"""),
HTML("""{% if object %}
<a href="{% url 'config:delete_monitoredresource' object.id %}"
class="btn btn-danger">
Delete <i class="fa fa-trash-o" aria-hidden="true"></i></a>
{% endif %}"""),
css_class='col-lg-12'
),
css_class='row'
),
)
class DeviceModelForm(forms.ModelForm):
class Meta:
model = DeviceModel
fields = ['model','vendor','device_type','ports','uplink_speed']
def __init__(self, *args, **kwargs):
self.is_add = kwargs.pop("is_add", False)
super(DeviceModelForm, self).__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.form_id = 'device_type_form'
self.helper.form_method = 'POST'
self.helper.layout = Layout(
Div(
Div(
Field('model', placeholder="Model"),
Field('vendor',),
Field('device_type',),
Field('ports', placeholder="Ports"),
Field('uplink_speed', placeholder="Uplink Speed"),
css_class='col-lg-6'
),
css_class='row'
),
Div(
Div(
HTML("""<input type="submit" name="submit" value="""),
HTML('"Add' if self.is_add else '"Update' ),
HTML(""" Device Model" class="btn btn-primary"/>"""),
HTML("""Cancel"""),
HTML("""{% if object %}
<a href="{% url 'config:delete_device_model' object.id %}"
class="btn btn-danger">
Delete <i class="fa fa-trash-o" aria-hidden="true"></i></a>
{% endif %}"""),
css_class='col-lg-12'
),
css_class='row'
),
)
DeviceFormSet = inlineformset_factory(DeviceModel, MonitoredResource, form=MonitoredResourceForm, extra=1)
class DeviceFormSetHelper(FormHelper):
def __init__(self, *args, **kwargs):
super(DeviceFormSetHelper, self).__init__(*args, **kwargs)
self.form_method = 'post'
self.render_required_fields = True
self.form_id = 'snmp_resource_form'
self.form_method = 'POST'
self.add_input(Submit("submit", "Save"))
self.layout = Layout(
Div(
Div(
Field('model'),
Field('resource', placeholder="Resource"),
css_class='col-lg-6'
),
css_class='row'
),
)
and in the templates I render:
{% block content %}
{% include "home/form_errors.html" %}
<div class="col-lg-6">
{% crispy form %}
</div>
<div class="col-lg-6">
{% crispy formset helper %}
</div>
<!-- /.row -->
{% endblock %}
is anyone able to see what im missing?
I think you have to render management form in your template, explained here why you need that
Management Form is used by the formset to manage the collection of forms contained in the formset. If you don’t provide this management data, an exception will be raised
add this in view html
{{ DeviceFormSet.management_form }}
You are missing a tag and also {{format.management_form|crispy}}
I guess
Your problem is that each form in a formset has its own management_form. I haven't dealt with this specifically in crispy, but in the general formsets, that was the problem that I had. You have to manually spell out each piece of the formset, either by iteration or hardcoding, and make sure that each has its management_form.
I had the same problem and found the answer in the documentation:
{{ formset.management_form|crispy }}
{% for form in formset %}
{% crispy form %}
{% endfor %}
In the django docs, there's an example of using inlineformset_factory to edit already created objects
https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#using-an-inline-formset-in-a-view
I changed the example to be this way:
def manage_books(request):
author = Author()
BookInlineFormSet = inlineformset_factory(Author, Book, fields=('title',))
if request.method == "POST":
formset = BookInlineFormSet(request.POST, request.FILES, instance=author)
if formset.is_valid():
formset.save()
return HttpResponseRedirect(author.get_absolute_url())
else:
formset = BookInlineFormSet(instance=author)
return render_to_response("manage_books.html", {
"formset": formset,
})
With the above, it renders only the inline model without the parent model.
To create a new object, say Author, with multiple Books associated to, using inlineformset_factory, what's the approach?
An example using the above Author Book model from django docs will be helpful. The django docs only provided example of how to edit already created object using inlineformset_factory but not to create new one
I've done that using Django Class-Based Views.
Here's my approach:
models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
author = models.ForeignKey(Author)
title = models.CharField(max_length=100)
forms.py
from django.forms import ModelForm
from django.forms.models import inlineformset_factory
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset
from .models import Author, Book
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', )
#property
def helper(self):
helper = FormHelper()
helper.form_tag = False # This is crucial.
helper.layout = Layout(
Fieldset('Create new author', 'name'),
)
return helper
class BookFormHelper(FormHelper):
def __init__(self, *args, **kwargs):
super(BookFormHelper, self).__init__(*args, **kwargs)
self.form_tag = False
self.layout = Layout(
Fieldset("Add author's book", 'title'),
)
BookFormset = inlineformset_factory(
Author,
Book,
fields=('title', ),
extra=2,
can_delete=False,
)
views.py
from django.views.generic import CreateView
from django.http import HttpResponseRedirect
from .forms import AuthorForm, BookFormset, BookFormHelper
from .models import Book, Author
class AuthorCreateView(CreateView):
form_class = AuthorForm
template_name = 'library/manage_books.html'
model = Author
success_url = '/'
def get(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
book_form = BookFormset()
book_formhelper = BookFormHelper()
return self.render_to_response(
self.get_context_data(form=form, book_form=book_form)
)
def post(self, request, *args, **kwargs):
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
book_form = BookFormset(self.request.POST)
if (form.is_valid() and book_form.is_valid()):
return self.form_valid(form, book_form)
return self.form_invalid(form, book_form)
def form_valid(self, form, book_form):
"""
Called if all forms are valid. Creates a Author instance along
with associated books and then redirects to a success page.
"""
self.object = form.save()
book_form.instance = self.object
book_form.save()
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, form, book_form):
"""
Called if whether a form is invalid. Re-renders the context
data with the data-filled forms and errors.
"""
return self.render_to_response(
self.get_context_data(form=form, book_form=book_form)
)
def get_context_data(self, **kwargs):
""" Add formset and formhelper to the context_data. """
ctx = super(AuthorCreateView, self).get_context_data(**kwargs)
book_formhelper = BookFormHelper()
if self.request.POST:
ctx['form'] = AuthorForm(self.request.POST)
ctx['book_form'] = BookFormset(self.request.POST)
ctx['book_formhelper'] = book_formhelper
else:
ctx['form'] = AuthorForm()
ctx['book_form'] = BookFormset()
ctx['book_formhelper'] = book_formhelper
return ctx
urls.py
from django.conf.urls import patterns, url
from django.views.generic import TemplateView
from library.views import AuthorCreateView
urlpatterns = patterns('',
url(r'^author/manage$', AuthorCreateView.as_view(), name='handle-books'),
url(r'^$', TemplateView.as_view(template_name='home.html'), name='home'),
)
manage_books.html
{% load crispy_forms_tags %}
<head>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
</head>
<div class='container'>
<form method='post'>
{% crispy form %}
{{ book_form.management_form }}
{{ book_form.non_form_errors }}
{% crispy book_form book_formhelper %}
<input class='btn btn-primary' type='submit' value='Save'>
</form>
<div>
Notice:
This is a simple runable example that use the inlineformset_factory
feature and Django generic Class-Based Views
I'm assumming django-crispy-forms is installed, and it's properly
configured.
Code repository is hosted at: https://bitbucket.org/slackmart/library_example
I know it's more code that the showed solutions, but start to using Django Class-Based Views is great.
I didn't read your question properly at first. You need to also render the the form for the parent model. I haven't tested this, I'm going off what I've done before and the previously linked answer, but it should work.
UPDATE
If you're using the view to both and edit, you should check for an Author ID first. If there's no ID, it'll render both forms as a new instance, whereas with an ID it'll, fill them with the existing data. Then you can check if there was a POST request.
def manage_books(request, id):
if id:
author = Author.objects.get(pk=author_id) # if this is an edit form, replace the author instance with the existing one
else:
author = Author()
author_form = AuthorModelForm(instance=author) # setup a form for the parent
BookInlineFormSet = inlineformset_factory(Author, Book, fields=('title',))
formset = BookInlineFormSet(instance=author)
if request.method == "POST":
author_form = AuthorModelForm(request.POST)
if id:
author_form = AuthorModelForm(request.POST, instance=author)
formset = BookInlineFormSet(request.POST, request.FILES)
if author_form.is_valid():
created_author = author_form.save(commit=False)
formset = BookInlineFormSet(request.POST, request.FILES, instance=created_author)
if formset.is_valid():
created_author.save()
formset.save()
return HttpResponseRedirect(created_author.get_absolute_url())
return render_to_response("manage_books.html", {
"author_form": author_form,
"formset": formset,
})
I am posting my final solutions, as per extensive assistant given by Onyeka.
Below I post the Add and Edit solutions of using inlineformset_factory of Django using the Author and Book example found in the Django Docs.
First, the Adding of Author object, with 3 extras of Book object to be appended.
Obviously, this goes into your views.py
def add_author(request):
'''This function creates a brand new Author object with related Book objects using inlineformset_factory'''
author = Author()
author_form = AuthorModelForm(instance=author) # setup a form for the parent
BookInlineFormSet = inlineformset_factory(Author, Book, fields=('title',))
if request.method == "POST":
author_form = AuthorModelForm(request.POST)
formset = BookInlineFormSet(request.POST, request.FILES)
if author_form.is_valid():
created_author = author_form.save(commit=False)
formset = BookInlineFormSet(request.POST, request.FILES, instance=created_author)
if formset.is_valid():
created_author.save()
formset.save()
return HttpResponseRedirect(created_author.get_absolute_url())
else:
author_form = AuthorModelForm(instance=author)
formset = BookInlineFormSet()
return render(request, "add_author.html", {
"author_form": author_form,
"formset": formset,
})
def edit_author(request, author_id):
'''This function edits an Author object and its related Book objects using inlineformset_factory'''
if id:
author = Author.objects.get(pk=author_id) # if this is an edit form, replace the author instance with the existing one
else:
author = Author()
author_form = AuthorModelForm(instance=author) # setup a form for the parent
BookInlineFormSet = inlineformset_factory(Author, Book, fields=('title',))
formset = BookInlineFormSet(instance=author)
if request.method == "POST":
author_form = AuthorModelForm(request.POST)
if id:
author_form = AuthorModelForm(request.POST, instance=author)
formset = BookInlineFormSet(request.POST, request.FILES)
if author_form.is_valid():
created_author = author_form.save(commit=False)
formset = BookInlineFormSet(request.POST, request.FILES, instance=created_author)
if formset.is_valid():
created_author.save()
formset.save()
return HttpResponseRedirect(created_author.get_absolute_url())
return render(request, "edit_author.html", {
"author_id": author_id, # This author_id is referenced
# in template for constructing the posting url via {% url %} tag
"author_form": author_form,
"formset": formset,
})
This part goes into your urls.py, assuming views have been imported, and urlpatterns constructed already.
...
url(r'^add/book/$', views.add_author, name='add_author'),
url(r'^edit/(?P<author_id>[\d]+)$', views.edit_author, name='edit_author'),
...
Now to the templates part. The edit Author object template (edit_author.html) looks like this (no styling applied)
<form action="{% url 'edit_book' author_id %}" method="POST" >
<!-- See above: We're using the author_id that was passed to template via views render of the edit_author(...) function -->
{% csrf_token %} <!-- You're dealing with forms. csrf_token must come -->
{{ author_form.as_p }}
{{ formset.as_p }}
<input type="submit" value="submit">
</form>
To add a brand new Author object via template (add_author.html):
<form action="." method="POST" >{% csrf_token %}
{{ author_form.as_p }}
{{ formset.as_p }}
<input type="submit" value="submit">
</form>
NOTE:
Using the action='.' might appear to be a cheap way of constructing the url, whereby the form posts the form data to the same page. With this example, using the action='.' for the edit_author.html template always got the form posted to /edit/ instead of /edit/1 or /edit/2
Constructing the url using the {% url 'edit_author' author_id %} ensures the form always posts to the right url. Failing to do use the {% url %} cost me lots of hours and trouble.
Big thanks to Onyeka.
i did exactly what you are trying :
https://github.com/yakoub/django_training/tree/master/article
you need to create a separate form using the prefix attribute .
then when you save you need to iterate over all books and associate them with the author you just created .
This is my first django inline_formset view for create a invoice with list of invoice_item_set.
In models.py there are three models
Customer
it has customer data like name, mobile_no, his_address etc..
Invoice
it has invoice data like customer_primary_key(required), delivery_address, billed_date etc..
total of the invoice item can be achieved by getting all "invoiceitem_set.all()" as items and from that sum of all add(items.item_subtotal)
InvoiceItem
it has invoiceitem data like invoice_primary_key(required), item_name, quantity, price etc..
the total is calculated before the model is saves
models.py
class Customer(models.Model):
pass
class Invoice(models.Model):
customer_id = models.ForeignKey(Customer, on_delete=models.PROTECT) # many - to - on relationship
invoice_id = models.CharField(....)
bill_note = models.TextField(....)
cash_pay = models.DecimalField(....)
upi_pay = models.DecimalField(....)
#property
def total_amount(self):
bill_total = 0
items = self.invoiceitem_set.all()
for item in items:
bill_total += item.item_subtotal
return bill_total
class InvoiceItem(models.Model):
invoice = models.ForeignKey(Invoice) # many - to - one relationship
item_name = models.CharField(....)
item_quantity = models.DecimalField(....)
item_price = models.DecimalField(....)
item_subtotal = models.DecimalField(....)
def save(self, *args, **kwargs):
self.item_subtotal = self.item_quantity * self.item_price
super(InvoiceItem, self).save(*args, **kwargs)
views.py (CreateView)
from django.db import transaction
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseRedirect
class InvoiceCreateView(LoginRequiredMixin, CreateView):
model = Invoice
form_class = InvoiceForm
template_name = 'billingsite/create_invoice.html'
def get_context_data(self, **kwargs):
context = super(InvoiceCreateView, self).get_context_data(**kwargs)
context['custom_title'] = "New Invoice"
temp = dict()
temp['customer_id'] = 0
if self.request.POST:
customer_id = int(self.request.POST.get('customer_id')) or False # custom clean method.
if customer_id:
customer_object = Customer.objects.get(pk=customer_id)
invoice_object = Invoice.objects.filter(customer_id=customer_object).order_by('-created_time').first()
temp = {
"customer_id": customer_id, "mobile_no": customer_object.mobile_no,
"searched_mobile_no": customer_object.raw_mobile_no,
"customer_name": customer_object.name, "gst_no": customer_object.goods_tax_id,
"pre_bal": customer_object.pending_balance, "purchased_date": "No Bills",
"created_date": customer_object.created_date.strftime(CUSTOM_DATE_FORMAT)
}
context['formset'] = InvoiceFormSet(self.request.POST)
else:
context['formset'] = InvoiceFormSet()
context['temp'] = temp
return context
def post(self, request, *args, **kwargs):
self.object = None
context = self.get_context_data()
customer_id = int(self.request.POST.get('customer_id')) # custom clean method.
if customer_id and customer_id != 0:
customer_object = Customer.objects.get(pk=customer_id)
form_class = self.get_form_class()
form = self.get_form(form_class)
formsets = context['formset']
with transaction.atomic():
form.instance.customer_id = customer_object
form.save(commit=False)
if form.is_valid() and formsets.is_valid():
self.object = form.save()
messages.success(self.request, f'Invoice is Submitted.')
return self.form_valid(form, formsets)
else:
return self.form_invalid(form, formsets)
return reverse_lazy('InvoiceList')
return self.render_to_response(context)
def form_valid(self, form, formsets):
formsets = formsets.save(commit=False)
for formset in formsets:
formset.invoice = self.object
formset.save()
return HttpResponseRedirect(self.get_success_url(self.object.pk))
def form_invalid(self, form, formsets):
return self.render_to_response(
self.get_context_data(form=form, formset=formsets))
def get_success_url(self, pk):
return reverse_lazy('ViewInvoice', kwargs={'pk': pk})
urls.py
urlpatterns = [
path('invoice/create/', views.InvoiceCreateView.as_view(), name='AddInvoice'),
]
forms.py
class InvoiceItemForm(forms.ModelForm):
item_name = forms.CharField(label=_('Product Name'))
item_subtotal = forms.IntegerField(required=False, label=_('Sub Total'))
class Meta:
model = InvoiceItem
fields = ['item_name', 'item_quantity', 'item_price', 'item_subtotal']
exclude = ()
widgets = {
"item_quantity": widgets.NumberInput(attrs={'step': '0.25'}),
"item_price": widgets.NumberInput(attrs={'step': '0.25'})
}
def __init__(self, *args, **kwargs):
super(InvoiceItemForm, self).__init__(*args, **kwargs)
self.fields['item_name'].widget.attrs['placeholder'] = 'Enter the food name'
self.fields['item_quantity'].widget.attrs['placeholder'] = 'Pieces'
self.fields['item_price'].widget.attrs['placeholder'] = 'in ₹'
self.fields['item_subtotal'].widget.attrs['readonly'] = True
self.fields['item_subtotal'].widget.attrs['tabindex'] = -1
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control'
class InlineFormSet(forms.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(InlineFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
form.empty_permitted = False
InvoiceFormSet = forms.inlineformset_factory(
Invoice, InvoiceItem, fields=('__all__'),
form=InvoiceItemForm, formset = InlineFormSet,
extra=0, min_num=1, can_delete=True
)
create_invoice.html
<fieldset>
<div class="text-dark py-4 table-responsive">
<div class="inline-formset inline-group" id="{{ formset.prefix }}-group" data-inline-type="tabular"
data-inline-formset="{
"name": "#{{ formset.prefix }}",
"options": {
"prefix": "{{ formset.prefix }}",
"addText": "Add+",
"deleteText": "<i class='bi bi-x'></i>",
"formCssClass": "dynamic-{{ formset.prefix }}",
}
}">
{% csrf_token %}
<div class="tabular inline-related">
{{ formset.management_form }}
<table id="invoice-table" class="as-table table table-xl table-hover caption">
<div class="d-block invalid-feedback">{{ formset.non_form_errors }}</div>
<caption>Add list of items.</caption>
{% for form in formset.forms %}
{% if forloop.first %}
<thead class="text-light">
<tr class="text-center">
<th scope="col">#</th>
{% for field in form.visible_fields %}
<th scope="col">{{ field.label|capfirst }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% endif %}
<tr scope="row" class="form-row" id="{{ formset.prefix }}-{{ forloop.counter0 }}">
<th class="original">
<div class="index">{{ forloop.counter1 }}</div>
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
</th>
{% for field in form.visible_fields %}
<td class="field-{{ field.name }}">
{% if field.name != "DELETE" %}
{% if field.errors %}
{{ field|addCls:"is-invalid" }}
{% else %}
{{ field }}
{% endif %}
{% if field.errors %}
<div class="invalid-feedback">{{ field.errors }}</div>
{% endif %}
{% else %}
{{ field }}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
<tr scope="row" class="form-row empty-row" id="{{ formset.prefix }}-empty">
<th class="original">
<div class="index">__prefix__</div>
{% for field in formset.empty_form.hidden_fields %}
{{ field }}
{% endfor %}
</th>
{% for field in formset.empty_form.visible_fields %}
<td class="field-{{ field.name }}">
{% if field.name != "DELETE" %}
{{ field }}
{% endif %}
</td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
</div>
</div>
</fieldset>