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 :)
Related
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})
I am trying to filter M2M queryset with autocomplete-light support.
I can get the filter working with the built-in ModelForm. Here is simplified version of code that works flawlessly without autocomplete:
models.py:
class FEmodel(models.Model):
name = models.Charfield(max_length=200)
class Workspace(models.Model):
name = models.Charfield(max_length=200)
fem = models.ForeignKey(FEmodel)
class Element(models.Model):
EID = models.PositiveIntegerField()
fem = models.ForeignKey(FEmodel)
class Panel(models.Model):
workspace = models.ForeignKey(Workspace)
elements = models.ManyToManyField(Element)
forms.py:
class PanelForm(forms.ModelForm):
def __init__(self,*args,**kwargs):
super(PanelForm,self).__init__(*args,**kwargs)
ws = self.instance.workspace
self.fields['elements'].queryset = Element.objects.filter(fem=ws.fem)
class Meta:
model = Panel
fields = ('__all__')
views.py:
#login_required(login_url='/login/')
def panel_edit(request, pk, id=None):
workspace = get_object_or_404(Workspace, pk=pk)
if id:
panel = get_object_or_404(Panel, pk=id)
else:
panel = Panel(workspace = workspace)
if request.method == 'POST':
form = PanelForm(request.POST, instance=panel)
if form.is_valid():
panel = form.save(commit=True)
return panels(request, pk)
else:
print form.errors
else:
form = PanelForm(instance=panel)
return render(request, 'structures/Panel/panel_edit.html', {'form': form, 'panel': panel, 'workspace': workspace})
urls.py:
...
url(r'^workspace/(?P<pk>[0-9]+)/panel/new/$', views.panel_edit, name='panel_edit'),
...
panel_edit.html:
...
<form method="POST" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit"> Save</button>
{% endbuttons %}
</form>
....
And here is autocomplete version which i could not get working:
autocomplete_light_registry.py
class ElementAutocomplete(acl.AutocompleteModelBase):
search_fields = ['EID']
acl.register(Element, ElementAutocomplete)
forms.py:
import autocomplete_light.shortcuts as acl
class PanelForm(acl.ModelForm):
def __init__(self,*args,**kwargs):
super(PanelForm,self).__init__(*args,**kwargs)
ws = self.instance.workspace
self.fields['elements'].queryset = Element.objects.filter(fem=ws.fem)
class Meta:
model = Panel
fields = ('__all__')
This version throws no errors but does not provide Element choices filtered by form.instance.ws.fem attribute. Instead it gives all Element objects.
What am i doing wrong?
edit 1:
in forms.py super(Panel,self) was corrected as super(PanelForm,self)
indent typos were corrected
edit 2: required portions of url, view and template added
edit 3:
According to #jpic's answer here is the solution:
added to panel_edit.html:
{% block bootstrap3_extra_head %}
{{ block.super }}
<script type="text/javascript">
$( document ).ready(function() {
elements_autocomplete = $('input[name=elements-autocomplete]').yourlabsAutocomplete()
elements_autocomplete.data['ws_pk'] = {{ form.instance.workspace.pk }}
});
</script>
{% endblock %}
autocomplete_light_registry.py:
import autocomplete_light as acl
class ElementAutocomplete(acl.AutocompleteModelBase):
search_fields = ['EID']
model = Element
def choices_for_request(self):
ws = Workspace.objects.get(pk=self.request.GET.get('ws_pk', None))
self.choices = self.choices.filter(fem=ws.fem)
return super(ElementAutocomplete, self).choices_for_request()
acl.register(ElementAutocomplete)
forms.py:
class PanelForm(acl.ModelForm):
class Meta:
model = Panel
fields = ('__all__')
The autocomplete JS object needs the pk value to pass it on to the view which calls the Python autocomplete object, then you can filter on the instance pk in choices_for_request() method of the python autocomplete object.
One way to get the js autocomplete object is to get it from the input element itself with the jQuery plugin, ie.:
elements_autocomplete = $('input[name=elements-autocomplete]').yourlabsAutocomplete()
Ensure that this is called after jquery-autocomplete-light JS is loaded.
Then, add the fk to its data:
elements_autocomplete.data['panel_pk'] = {{ form.instance.pk }}
In choices_for_request(), you probably figured it out by now:
def choices_for_request(self):
choices = super(ElementAutocomplete, self).choices_for_request()
panel_pk = request.GET.get('panel_pk', None)
if panel_pk and panel_pk.isdigit():
choices = choices.filter(panel__pk=panel_pk)
return choices
Actually, that's very easy to test, in your browser, open the JS console and run: $('input[name=elements-autocomplete]').yourlabsAutocomplete().data['foo'] = 'bar' and you'll see that subsequent requests made by the autocomplete script will add &foo=bar to the URL it its, making it available to choices_for_request through self.request !
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 am trying to have a formset where each form (PropertySelector) has a drop-down menu (PropertySelector.property) whereas each item of that menu is ForeignKey reference to another model (Property).
Somehow when I am trying to submit and save the formset I am getting:
Exception Type: IntegrityError
Exception Value: testproj_propertyselector.property_id may not be NULL
What is wrong with it and how can I get around it? My entire code is below. Thanks.
EDIT: it looks like inline_formset problem to me (maybe MySQL also). Please, help me with workaround.
The project is called testproj and my app is called testproj too.
First we populate Property:
>>> from testproj.models import Property
>>> p = Property(name='prop1', basic=True)
>>> p.save()
>>> p = Property(name='prop2', basic=True)
>>> p.save()
models.py
from django.db import models
class PropertySet(models.Model):
name = models.CharField(max_length=50)
class Property(models.Model):
name = models.CharField(max_length=50)
basic = models.BooleanField()
def __unicode__(self):
return u'%s' % (self.name)
class PropertySelector(models.Model):
property_set = models.ForeignKey(PropertySet)
title = models.CharField(max_length=50)
property = models.ForeignKey(Property)
forms.py
from django.forms import ModelForm, TextInput, Select, ModelChoiceField
from django.db.models import Q
from testproj.models import Property, PropertySet, PropertySelector
class PropertySetForm(ModelForm):
class Meta:
model = PropertySet
def PropertySelForm():
PropertyQueryset = Property.objects.filter(Q(basic=True))
class PropertySelectorForm(ModelForm):
property = ModelChoiceField(
queryset=PropertyQueryset,
widget=Select(attrs={'class': 'property'})
)
def __init__(self, *args, **kwargs):
super(ModelForm, self).__init__(*args, **kwargs)
self.css_class = "prop_sel"
class Meta:
model = PropertySelector
fields = ("property_set", "title")
widgets = {"title" : TextInput(attrs={"class" : "title"})}
return PropertySelectorForm
views.py
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.forms.models import inlineformset_factory
from testproj.models import PropertySet, PropertySelector
from testproj.forms import PropertySetForm, PropertySelForm, PropertySelForm
def index(request):
property_selector_form = PropertySelForm()
PropertySelectorFormSet = inlineformset_factory(PropertySet, PropertySelector, form=property_selector_form)
if request.method == "POST":
property_set_form = PropertySetForm(request.POST)
if property_set_form.is_valid():
saved_property_set = property_set_form.save()
prop_sel_formset = PropertySelectorFormSet(request.POST, instance=saved_property_set)
if prop_sel_formset.is_valid():
prop_sel_formset.save()
else:
property_set_form = PropertySetForm()
prop_sel_formset = PropertySelectorFormSet()
return render_to_response(
"testproj/index.html",
{
"property_set_form": property_set_form,
"prop_sel_formset": prop_sel_formset
},
context_instance=RequestContext(request)
)
index.html (template):
{% block content %}
<head>
</head>
<body>
<form method="post" action=""> {% csrf_token %}
{{ property_set_form.as_p }}
{{ prop_sel_formset.management_form }}
{% for form in prop_sel_formset %}
{{ form }}
{% endfor %}
<input type="submit" value="Submit">
</form>
</body>
{% endblock %}
The property_id isn't saved because you don't include it in your form.Meta. Leave away the fields, and it works:
def PropertySelForm():
PropertyQueryset = Property.objects.filter(Q(basic=True))
class PropertySelectorForm(ModelForm):
property = ModelChoiceField(
queryset=PropertyQueryset,
widget=Select(attrs={'class': 'property'})
)
def __init__(self, *args, **kwargs):
super(ModelForm, self).__init__(*args, **kwargs)
self.css_class = "prop_sel"
class Meta:
model = PropertySelector
#fields = ("property_set", "title")
widgets = {"title" : TextInput(attrs={"class" : "title"})}
return PropertySelectorForm
I'd rename PropertySelForm to property_selectorform_factory, but that's
just me. :)
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()