Django==3.1.7
django-crispy-forms==1.11.2
I 've 2 models: Order and OrderList
Order is a header and OrderList is a tabular section of the related Order
class Order(models.Model):
print_number = models.PositiveIntegerField(
verbose_name=_("Number"),
default=get_todays_free_print_number,
)
# ... some other fields
class OrderList(models.Model):
order = models.ForeignKey(
Order,
blank=False,
null=False,
on_delete=models.CASCADE
)
item = models.ForeignKey(
Item,
verbose_name=_("item"),
blank=True,
null=True,
on_delete=models.CASCADE
)
# ... some other OrderList fields
The question is how to create a form containing both models and provide the ability to add an OrderList positions within an Order into the form
and save them both.
What I did:
forms.py - I used inline formset factory for the OrderList
from django.forms import ModelForm
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from .models import Order, OrderList
class OrderForm(ModelForm):
class Meta:
model = Order
fields = [
'__all__',
]
class OrderListForm(ModelForm):
class Meta:
model = OrderList
fields = [
'__all__',
]
class OrderListFormSetHelper(FormHelper):
"""Use class to display the formset as a table"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.template = 'bootstrap4/table_inline_formset.html'
# I am not sure we should add a button here
####################################################
self.add_input(Submit('submit', 'Submit',
css_class='btn btn-primary offset4'))
views.py
#login_required
def orders(request):
template = f'{APP_NAME}/index.html'
list_helper = OrderListFormSetHelper()
list_formset = inlineformset_factory(Order,
OrderList,
OrderListForm,)
if request.method == 'POST':
form = OrderForm(request.POST, prefix="header")
if form.is_valid() and list_formset.is_valid():
order = form.save()
order_list = list_formset.save(commit=False)
order_list.order = order
order_list.save()
return HttpResponseRedirect(reverse('order_created'))
else: # all other methods means we should create a blank form
form = OrderForm()
return render(request, template, {'form': form,
'list_form': list_formset,
'list_helper': list_helper})
index.html
<form method="post">
{% csrf_token %}
{% crispy form %}
{% crispy list_form list_helper %}
<!-- the button below doesn't make sense because it does nothing.
the self.add_input in forms.py already adds a submit button.
-->
<button type="submit" class="btn btn-primary">
{% translate "Send an order" %}
</button>
</form>
The resulting html renders the page like that:
But when I press the submit button
it clean up Order related fields and mark them as blank
You use the crispy template tag to render your forms. It uses the FormHelper class to help render your forms, which by default has the attribute form_tag set to True which makes it render a form tag for you. Meaning you are nesting form tags which does not work and is not possible with the HTML5 standard. You need to set this attribute to False to prevent this:
class OrderForm(ModelForm):
class Meta:
model = Order
fields = [
'__all__',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper(self) # Explicitly set helper to prevent automatic creation
self.helper.form_tag = False # Don't render form tag
self.helper.disable_csrf = True # Don't render CSRF token
Next in the helper you make in the view you also have to set these attributes. Furthermore what you call as list_formset is not an instance of a formset but a class, hence you actually need to instantiate the formset class and use it:
#login_required
def orders(request):
template = f'{APP_NAME}/index.html'
list_helper = OrderListFormSetHelper()
list_helper.form_tag = False # Don't render form tag
list_helper.disable_csrf = True # Don't render CSRF token
OrderListFormSet = inlineformset_factory(Order,
OrderList,
OrderListForm,)
if request.method == 'POST':
form = OrderForm(request.POST, prefix="header")
list_formset = OrderListFormSet(request.POST, instance=form.instance) # Instantiate formset
if form.is_valid() and list_formset.is_valid():
order = form.save()
order_list = list_formset.save()
# Remove below two line, have already instantiated formset with `form.instance` and called save without `commit=False`
# order_list.order = order
# order_list.save()
return HttpResponseRedirect(reverse('order_created'))
else: # all other methods means we should create a blank form
form = OrderForm()
list_formset = OrderListFormSet(instance=form.instance) # Instantiate formset
return render(request, template, {'form': form,
'list_form': list_formset,
'list_helper': list_helper})
Related
I have made a new Comment Model in my Django Project but the form is not showing in the browser although I added the form in the template.
Here is the models.py
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
body = models.TextField(max_length=300)
def __str__(self):
return str(self.pk)
Here is the views:
def comment_create(request, self):
post = get_object_or_404(Post, slug=self.kwargs['slug'])
user = User.objects.get(user=request.user)
c_form = CommentModelForm()
context = {
'post': post,
'user': user,
'c_form': c_form,
}
return context
Here is the forms.py
class CommentModelForm(forms.ModelForm):
body = forms.CharField(label='',
widget=forms.TextInput(attrs={'placeholder': 'Add a comment...'}))
class Meta:
model = Comment
fields = ('body',)
Here is the urls.py
path('blogs/comment', comment_create, name='comment-post'),
Here is the template:
<form action="" method="POST"class='ui fluid form'>
{% csrf_token %}
<input type="hidden" name="post_id" value={{post.id}}>
{{ c_form }}
<button type="submit" name="submit_c_form" class="">Send</button>
</form>
First, you have to get the type of request, I added a if/else for GET and POST requests. Added form.is_valid check.
In your function you are trying to get a kwarg form the url but you don't have a kwarg in your path.
path('blogs/<slug:slug>/comment', comment_create, name='comment-post'),
views.py
def comment_create(request, self):
post = get_object_or_404(Post, slug=self.kwargs['slug'])
if request.method == 'POST': # If user submitted form
c_form = CommentModelForm(request.POST) # Get form response
if c_form.is_valid(): # Chekc if form is valid
c_form.user = User.objects.get(user=request.user) # Get user and add it to form
c_form.post = post # Add post to form
c_form.save() # Save form
else:
c_form = CommentModelForm() # Pass form
context = {
'post': post,
'c_form': c_form,
}
return render(request, 'app/comment.html', context) # change template
forms.py
class Meta:
model = Comment
fields = ['body'] # When I set tuples it normally gives me an error
I have a rather complex Django form that affects 3 models and part of which includes an inline formset. I found a nice solution to building the form at https://dev.to/zxenia/django-inline-formsets-with-class-based-views-and-crispy-forms-14o6. I extended that solution and added a third model in a similar way that the formset was added (using a custom Django Crispy Form and inserting it using the Crispy Forms Layout features).
My problem is that any validation errors raised on either of the two inserted forms (the formset and the small subform) are simply ignored - the main form posts correctly and raised ValidationErrors are displayed in the form as errors allowing the user to correct any mistakes and its data is correctly saved to the database. If the subform and formset are valid, their data gets saved correctly as well. However, if the data in the subform and formset is not valid, the form never shows the errors to give the user a chance to correct their mistake, and the data is simply ignored and never saved to the database - the main model's data saves fine though.
My question is, how do I get the form to refresh with errors displayed in the added subform and formset allowing the user to correct their mistakes?
Most of the code below is from the quite good post referenced above with a third model added
Models:
from django.db import models
from django.contrib.auth.models import User
class Collection(models.Model):
subject = models.CharField(max_length=300, blank=True)
owner = models.CharField(max_length=300, blank=True)
note = models.TextField(blank=True)
created_by = models.ForeignKey(User,
related_name="collections", blank=True, null=True,
on_delete=models.SET_NULL)
def __str__(self):
return str(self.id)
class CollectionTitle(models.Model):
"""
A Class for Collection titles.
"""
collection = models.ForeignKey(Collection,
related_name="has_titles", on_delete=models.CASCADE)
name = models.CharField(max_length=500, verbose_name="Title")
language = models.CharField(max_length=3)
Class CollectionTxn(models.Model):
"""
A Class for Collection transactions.
"""
collection = models.ForeignKey(Collection,
related_name="has_txn", on_delete=models.CASCADE)
number_received= models.IntegerField()
date_received= models.DateField()
class Meta:
'''
If 2 rows are entered with the same information, a validation error is raised, but it just
doesn't save the data at all instead of refreshing the form showing the error.
'''
unique_together = ('number_received', 'date_received')
forms.py:
from django import forms
from .models import Collection, CollectionTitle
from django.forms.models import inlineformset_factory
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Fieldset, Div, Row, HTML, ButtonHolder, Submit
from .custom_layout_object import Formset, Subform
import re
class CollectionTitleForm(forms.ModelForm):
class Meta:
model = CollectionTitle
exclude = ()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
formtag_prefix = re.sub('-[0-9]+$', '', kwargs.get('prefix', ''))
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
Row(
Field('name'),
Field('language'),
Field('DELETE'),
css_class='formset_row-{}'.format(formtag_prefix)
)
)
CollectionTitleFormSet = inlineformset_factory(
Collection, CollectionTitle, form=CollectionTitleForm,
fields=['name', 'language'], extra=1, can_delete=True
)
class CollectionForm(forms.ModelForm):
class Meta:
model = Collection
exclude = ['created_by', ]
def __init__(self, *args, **kwargs):
super(CollectionForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = True
self.helper.form_class = 'form-horizontal'
self.helper.label_class = 'col-md-3 create-label'
self.helper.field_class = 'col-md-9'
self.helper.layout = Layout(
Div(
Field('subject'),
Field('owner'),
Fieldset('Add titles',
Formset('titles')),
Field('note'),
Subform('transactions'),
HTML("<br>"),
ButtonHolder(Submit('submit', 'Save')),
)
)
class CollectionTxnForm(forms.ModelForm):
class Meta:
model = CollectionTxn
exclude = ()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['collection'].widget = HiddenInput()
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
Row(
Field('number_received'),
Field('date_received'),
)
)
views.py:
from .models import *
from .forms import *
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
from django.db import transaction
class CollectionCreate(CreateView):
model = Collection
template_name = 'mycollections/collection_create.html'
form_class = CollectionForm
success_url = None
def get_context_data(self, **kwargs):
data = super(CollectionCreate, self).get_context_data(**kwargs)
if self.request.POST:
data['titles'] = CollectionTitleFormSet(self.request.POST)
data['transactions'] = CollectionTrxForm(self.request.POST)
else:
data['titles'] = CollectionTitleFormSet()
data['transactions'] = CollectionTrxForm()
return data
def form_valid(self, form):
context = self.get_context_data()
titles = context['titles']
transactions = context['transactions']
with transaction.atomic():
form.instance.created_by = self.request.user
self.object = form.save()
if titles.is_valid():
titles.instance = self.object
titles.save()
if transactions.is_valid():
transactions.save()
return super(CollectionCreate, self).form_valid(form)
def get_success_url(self):
return reverse_lazy('mycollections:collection_detail', kwargs={'pk': self.object.pk})
custom_layout_object.py
from crispy_forms.layout import LayoutObject, TEMPLATE_PACK
from django.shortcuts import render
from django.template.loader import render_to_string
class Formset(LayoutObject):
template = "mycollections/formset.html"
def __init__(self, formset_name_in_context, template=None):
self.formset_name_in_context = formset_name_in_context
self.fields = []
if template:
self.template = template
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK):
formset = context[self.formset_name_in_context]
return render_to_string(self.template, {'formset': formset})
class SubForm(LayoutObject):
template = "mycollections/subform.html"
def __init__(self, subform_name_in_context, template=None):
self.subform_name_in_context = subform_name_in_context
self.fields = []
if template:
self.template = template
def render(self, subform, form_style, context, template_pack=TEMPLATE_PACK):
subform = context[self.subform_name_in_context]
return render_to_string(self.template, {'subform': subform})
formset.html
{% load crispy_forms_tags %}
{% load staticfiles %}
<style type="text/css">
.delete-row {
align-self: center;
}
</style>
{{ formset.management_form|crispy }}
{% for form in formset.forms %}
{% for hidden in form.hidden_fields %}
{{ hidden|as_crispy_field }}
{% endfor %}
{% crispy form %}
{% endfor %}
<br>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="{% static 'mycollections/libraries/django-dynamic-formset/jquery.formset.js' %}"></script>
<script type="text/javascript">
$('.formset_row-{{ formset.prefix }}').formset({
addText: 'add another',
deleteText: 'remove',
prefix: '{{ formset.prefix }}',
});
</script>
subform.html
{% load crispy_forms_tags %}
{% crispy subform %}
collection_create.html
{% extends "mycollections/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container">
<div class="card">
<div class="card-header">
Create collection
</div>
<div class="card-body">
{% crispy form %}
</div>
</div>
</div>
{% endblock content %}
Basically, for the fields associated with the formset and subform added to the layout, validation errors are still raised, but they do not bubble up to the form level to show the errors, they are just ignored and the data is never saved. The "main" model works fine and validationerrors are displayed for its fields. If there is no invalid data, the main form, the subform, and the formset's data are all saved correctly. If there is invalid data in the formset or subform, the user never gets a chance to correct that data.
Any help as to where I would add the code needed so that if any invalid data entered in the formset or subform would cause the form to refresh displaying errors instead of just ignoring and not saving invalid data would be appreciated.
After much debugging and analyzing code, both my own and Crispy, I have solved my own problem. It was simply a matter of checking for the subform and formset validations, and if not valid, re-rendering the form.
Here is the new form_valid() method from the view that does this:
def form_valid(self, form):
context = self.get_context_data()
titles = context['titles']
transactions = context['transactions']
with transaction.atomic():
form.instance.created_by = self.request.user
if titles.is_valid() and transactions_is_valid():
self.object = form.save() #only save form if other subforms validate
titles.instance = self.object
# Any other field processing goes here
titles.save()
transactions.save()
else:
# If any subform or subformset is invalid, re-render the form showing errors
context.update({'titles': titles})
context.update({'transactions': transactions})
return self.render_to_response(context)
return super(CollectionCreate, self).form_valid(form)
With this, any clean methods in the formset and subform or if there are any other errors (for example if no two rows of the formset can be the same because unique_together() declared in model),the form will refresh showing all errors on all three combined form/formsets allowing the user to correct those errors.
Now - to do it with Ajax so the page does not refresh :)
I'm writing what should be a very simple todo app. The problem is that the edit view is giving me fits! I'm trying to populate a form with data from the database, and it's just not doing the right thing. I've tried the info from this page, but the translation into class-based views must have broken something, or I'm just not using the right kind of form.
Here's the code for the model:
class Todo(models.Model):
id = models.AutoField(primary_key=True)
todo = models.CharField(max_length=255, unique=True)
todo_detail = models.TextField(default='')
date_created = models.DateField(default=timezone.now())
estimated_completion = models.DateTimeField(default=timezone.now())
maybe_completed = models.BooleanField("Completed?", default=False)
def __unicode__(self):
return self.todo
The view code, the commented out bit is from the link:
class TodoEditView(FormView):
model = Todo
form_class = TodoEditForm
template_name = 'todo_edit.html'
#def get(self, request, *args, **kwargs):
# form = self.form_class()
# form.fields['todo'].queryset = Todo.objects.get(id=self.kwargs['pk'])
# form.fields['todo_detail'].queryset = Todo.objects.get(
# id=self.kwargs['pk'])
# form.fields['date_created'].queryset = Todo.objects.get(
# id=self.kwargs['pk'])
# form.fields['estimated_completion'].queryset = Todo.objects.get(
# id=self.kwargs['pk'])
# form.fields['maybe_completed'].queryset = Todo.objects.get(
# id=self.kwargs['pk'])
# template_vars = RequestContext(request, {
# 'form': form
# })
# return render_to_response(self.template_name, template_vars)
def get_context_data(self, **kwargs):
context = super(TodoEditView, self).get_context_data(**kwargs)
context['todo'] = Todo.objects.get(id=self.kwargs['pk'])
return context
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
todo = request.POST['todo']
todo_detail = request.POST['todo_detail']
estimated_completion = request.POST['estimated_completion']
date_created = request.POST['date_created']
t = Todo(todo=todo, todo_detail=todo_detail,
estimated_completion=estimated_completion,
date_created=date_created)
t.save()
return redirect('home')
The form code:
class TodoEditForm(forms.ModelForm):
class Meta:
model = Todo
exclude = ('id', )
And the template code:
{% extends 'todos.html'%}
{% block content %}
<form method="post" action="{% url 'add' %}">
<ul>
{{ form.as_ul }}
{% csrf_token %}
</ul>
{{todo.todo}}
</form>
{% endblock %}
What the heck am I doing wrong?
You should use an UpdateView, not a FormView. That will take care of prepopulating your form.
Also note you don't need any of the logic in the post method - that is all taken care of by the generic view class.
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()
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)