How to update formset with new pk id? - django

I have a formset that I am rendering with the default {{ formset }}. The page does a post back to itself when the form is submitted.
The issue is when new instances are created, the fields containing the ids are still blank like <input id="id_form-0-id" name="form-0-id" type="hidden">. So if I resubmit the form after it comes back, it ends up creating new instances instead of updating the newly-created instances. If I refresh the page then the fields contain the ids <input id="id_form-0-id" name="form-0-id" type="hidden" value="18">.
This is my controller function:
def main_categories (request):
dict = {}
FormSet = modelformset_factory (MainCategory, formset = MainCategoryFormSet, fields = ['name'], extra = 1, can_delete = True)
if request.method == 'POST':
formset = FormSet (request.POST)
if formset.is_valid ():
with transaction.atomic ():
formset.save ()
else:
formset = FormSet ()
dict ['formset'] = formset
return render (request, 'equipment/admin/main_categories.html', dict)

If you want to edit previously submitted queryset, you must provide queryset into FormSet class. See documentation

Related

Inline formset returns empty list on save?

When I try to save my inline formset it just returns an empty list and no changes are reflected in the database. I have tried doing it with no option and commit=False but they both have the same result. I know there is data because I printed the formset as a table, and I know it is valid because the property is_valid() method returns true. Here is the code:
def edit(request):
if request.method == 'POST':
print(request.POST)
form = TombstoneForm(request.POST)
print(form.is_valid())
t = form.save(commit=False)
t.edit_date = datetime.now()
t.user_editor = request.user
t.save()
print(t)
formset_construct = inlineformset_factory(Tombstone, Tank, form=TombstoneForm)
formset = formset_construct(request.POST)
print("Passed the inline formset")
print(formset.as_table())
print(formset.is_valid())
l = formset.save()
print(l)
return render(request, 'main.html')
So I believe I have found the source of my problem and a workaround. The problem was occuring in the BaseModelFormSet class in this method:
def save_existing_objects(self, commit=True):
self.changed_objects = []
self.deleted_objects = []
if not self.initial_forms:
return []
saved_instances = []
forms_to_delete = self.deleted_forms
for form in self.initial_forms:
obj = form.instance
# If the pk is None, it means either:
# 1. The object is an unexpected empty model, created by invalid
# POST data such as an object outside the formset's queryset.
# 2. The object was already deleted from the database.
if obj.pk is None:
continue
if form in forms_to_delete:
self.deleted_objects.append(obj)
self.delete_existing(obj, commit=commit)
elif form.has_changed():
self.changed_objects.append((obj, form.changed_data))
saved_instances.append(self.save_existing(form, obj, commit=commit))
if not commit:
self.saved_forms.append(form)
return saved_instances
The problem was occuring in:
if obj.pk is None:
continue
Where it was always hitting continue, so my solution has just been to save the individual form data instead of the formset data:
for form in formset:
val = form.save(commit=False)
val.Vessel = t
val.save()
To help out others, where they are having similar problems with formset save returning empty lists, and theres is not much else on SO:
According to the Django docs:
After calling save(), your model formset will have three new attributes containing the formset’s changes:
models.BaseModelFormSet.changed_objects
models.BaseModelFormSet.deleted_objects
models.BaseModelFormSet.new_objects
new_objects is simply list of new objs, i.e.
[obj1, obj2]
changed_objects is a list of tuples, i.e.
[
(obj1, [list of field names changed]),
(obj2, [list of field names changed]),
...
]
I was having a problem where deleted_objects was always [].
First ensure you have the required hidden field in your form:
{% for form in formset %}
{{ form.id }} # <- have you got this?
{{ form.DELETE }}
...
{{ form.foo }}
{% endfor %}
And secondly, I had the bright idea of disabling all fields when the user the check the delete checkbox. Disabled items are not posted, so then the items to be deleted would not a PK, as OP mentions above if obj.pk is blank, then it would not be added to the (.deleted_objects in this case) list . The solution is to rather use readonly, in JavaSript use .readOnly = true and in css :read-only.

django Formset won't save

I have the below view for a Formset, but when I save the form it doesn't save changes to the database?
def schedule(request, year, month, day):
EntriesFormset = modelformset_factory(Entry, extra = 1, exclude=("creator", "date"),can_delete=True)
if request.method == 'POST':
formset = EntriesFormset(request.POST)
if formset.is_valid():
# add current user and date to each entry & save
entries = formset.save(commit=False)
for entry in entries:
entry.creator = request.user
entry.date = date(int(year), int(month), int(day))
entry.save()
return HttpResponseRedirect(reverse("Pipettes.views.month", args=(year, month)))
else:
# display formset for existing enties and one extra form
formset = EntriesFormset(queryset=Entry.objects.filter(date__year=year,date__month=month, creator=request.user))
return render_to_response("Scheduler.html", add_csrf(request, entries=formset, year=year,
month=month, day=day))
I suspect that the formset is invalid but instead of displaying of the formset with errors you returning the redirect. You should move the redirecting to one level right, into the if statement:
if formset.is_valid():
...
return HttpResponseRedirect(reverse("Pipettes.views.month", args=(year, month)))
UPDATE: If your formset is not validated but you don't see any errors on the page then your formset rendering may be invalid. For testing purposes try to use the simplest possible template:
<table>
{{ formset }}
</table>
Also note that with formset.save(commit=False) deleted objects are not deleted automatically. See the side note in this chapter of the docs.

Validate a form against a formset in Django

I have a formset with three forms that the user inputs. Next to this, I have another form with one field that I want to validate against the fields inputted in the formset. Views.py looks something like:
FormSet = formset_factory(Form1, formset=BaseFormSet, extra=3)
if request.method == 'POST':
formset = FormSet(request.POST, request.FILES, prefix='first_form')
form2 = Form2(request.POST)
if formset.is_valid() and form2.is_valid():
# do something with the data
pass
else:
formset = FormSet(prefix='first_form')
target_shoe_form = TargetShoeForm()
return render(request, 'my_template.html', {
'formset': formset,
'form2': form2,
})
Is there a way to validate Form2 against the values in my Formset? As currently above they only validate internally, not against each other. Or, is it necessary to either nest the singleton form inside the formset, or nest the formset in the singleton form somehow? Thanks!
Update on validation:
Form1 has two fields, and rendered three times as part of a formset. Form2 has one field. When the user submits, I want to check that Form2's field is distinct from any of the values submitted in Form1

Django Forms choice-like field, without choice limitation

I am writing a writing a webapp that is basically just a form, and it has a button that duplicates a field so that multiple items can be entered. I can't use a SelectMultiple field or any of its variations because there is not a set number of choices to choose from. The user should be able to enter whatever they want into the fields and they must be saved in the model and linked to a record through a manytomany field. Here is a jsfiddle link for demonstration.
HTML
<form>
<label>Field 1
<textarea rows="3"></textarea>
</label>
<label>Multiple Values Possible</label>
<div>
<input type="text">
<select>
<option value="1">1</option>
<option value="2">2</option>
</select>
</div>
<button id="add_div">Add</button>
</form>
JS
add_div = document.getElementById("add_div");
add_div.onclick = function () {
var div = this.previousElementSibling;
var new_div = div.cloneNode(true);
this.parentNode.insertBefore(new_div, this);
return false;
}.bind(add_div);
I cannot figure out how to create the form backend for this. There aren't any field classes that can take in a variable amount of data and validate each one against another field.
What I have tried to do is create a MultiWidget/MultiValueField for the textbox/select dropdown pair, and then subclass my MultiValueField in a class closely following django's ModelMultipleChoiceField. I got stuck trying to get the form field to work with templates, allowing me to add all fields back to the rendered page when rendering with a particular form instance (like how when you use the CheckboxSelectMultiple widget, boxes that are checked in a form instance are rendered checked)
Is there any way to do this and have the ModelForm's save method also save the manytomany fields properly? I know I can override the form's save method and do something like in this stackoverflow question, but I would rather have all the save logic handled by the form fields themselves.
Based on looking at your example jsfiddle, it looks like you don't really need a "Choice Field", what you're looking for are Formsets.
In essence, you would have 2 forms on the page, one which is a normal form and would take care of Field 1, and one which is a Formset which deals with all the many-to-many relations for Field 2
Field2FormSet = formset_factory(Field2ToForm)
Make sure you output the management_form which you can clone with your "add" button.
What you are probably looking for is an inline formset, which can only be used if you are using django models (which you hinted at in your question).
Check out this guide: http://lab305.com/news/2012/jul/19/django-inline-formset-underscore/.
For the lazy, here is a quick example that gets you most of the way there. This app will allow you to continuously add Parent model objects to the database, along with any children that are filled out.
app/models.py
from django.db import models
class ParentModel(models.Model):
parent_field = models.CharField(choices=[(1, 1), (2, 2)])
class ChildModel(models.Model):
parent = models.ForeignKey(ParentModel)
child_field = models.IntegerField(choices=[(1, 1), (2, 2)])
app/views.py
from app import models
from django import forms
from django.forms.models import inlineformset_factory
from django.template import RequestContext, Template
from django.http import HttpResponse
class ParentForm(forms.ModelForm):
class Meta:
model = models.ParentModel
ChildFormSet = inlineformset_factory(models.ParentModel, models.ChildModel)
def test_view(request):
if request.method == "POST":
form = ParentForm(request.POST, request.FILES)
formset = ChildFormSet(request.POST, request.FILES, form.instance)
if form.is_valid() and formset.is_valid():
form.save()
formset.save()
else:
pass # Handle validation error
template = Template(
"<form action='<url for view>' method='post'>"
"{% csrf_token %}"
"{{ form.as_p }}"
"<p>{{ formset.as_table }}</p>"
"<input type='submit' value='Submit'/>"
"</form>"
)
context = RequestContext(request, {
"form": ParentForm(),
"formset": ChildFormSet(),
})
return HttpResponse(template.render(context))
What is shown above will only allow you add up to three children (the default number of extra forms the inline form set produces). To add dynamically, you are going to have to add some java script that creates new forms in the form set on the client side. For that, I suggest you look at the guide I posted above since I don't think I can do better job of explaining it.
Thanks to #Kevin and #Thomas for pointing me towards formsets! Here is how I did it:
models.py
from django.db import models
class RelatedField(models.Model):
field1 = models.CharField(max_length=50)
field2 = models.IntegerField(choices=[(x, x) for x in xrange(1, 11)])
class Record(models.Model):
user = models.ForeignKey(User)
field = models.CharField(max_length=20)
relatedA = models.ManyToManyField(RelatedField, related_name='relatedA')
relatedB = models.ManyToManyField(RelatedField, related_name='relatedB')
views.py
def getIndexContext(data):
if data is None:
recordForm = RecordForm()
relatedFormA = RelatedFormSet(queryset=RelatedField.objects.none(), prefix='related-a')
relatedFormB = RelatedFormSet(queryset=RelatedField.objects.none(), prefix='related-b')
else:
recordForm = RecordForm(data)
relatedFormA = RelatedFormSet(data, prefix='related-a')
relatedFormB = RelatedFormSet(data, prefix='related-b')
return {
'form': recordForm,
'relatedA': relatedFormA,
'relatedB': relatedFormB,
'title': 'Index',
}
def index(request):
if request.method == 'GET':
return render(request, 'record/index.html', getIndexContext(None))
else:
context = getIndexContext(request.POST)
form = context['form']
relatedA = context['relatedA']
relatedB = context['relatedB']
if form.is_valid() and relatedA.is_valid() and relatedB.is_valid():
obj = form.save(commit=False)
obj.user_id = request.user
obj.save()
form.save_m2m()
instances = relatedA.save()
obj.relatedA.add(*instances)
instances = relatedB.save()
obj.relatedB.add(*instances)
return HttpResponse('success!')
else:
return render(request, 'record/index.html', context)
And then some javascript that can duplicate the fields in the formsets, and increment the names by one.

Django - Can't Initialize Selected Radio Button when using ModelChoiceField with RadioSelect Widget

I have a form that I want to be pre-populated with data from a model instance when a user is attempting to update an existing database record. When the form is rendered none of the radio buttons pre-selected even though a model instance has been passed to the ModelForm. In a much larger form than listed below, all of the fields except the radio buttons are pre-populated with the correct data from the model instance. How do I get the correct radio buttons pre-selected?
My Models:
class TicketType(models.Model):
type = models.CharField(max_length=15, unique=True)
def __unicode__(self):
return self.type.title()
class TestTicket(models.Model):
ticket_type = models.ForeignKey(TicketType, to_field='type')
My Form
class TestTicketForm(ModelForm):
ticket_type = ModelChoiceField(TicketType.objects.all(),
widget=RadioSelect,
empty_label=None)
class Meta:
model = TestTicket
fields = ['ticket_type']
My View
def test_ticket_update(request, ticket_num=None):
# initialize an update flag to distinguish between a request
# to add a new ticket or an update to an existing ticket.
update_requested = False
ticket_instance = None
if ticket_num:
# This is a request to update a specific ticket.
# Attempt to retrieve the ticket or show a 404 error.
# If a ticket is retrieved, it is locked for editing
# by using 'select_for_update()' to prevent a race condition.
ticket_instance = get_object_or_404(
TestTicket.objects.select_for_update(),pk=ticket_num)
update_requested = True
if request.method == 'POST':
form = TestTicketForm(request.POST, instance=ticket_instance)
if form.is_valid():
ticket = form.save(commit=False)
ticket.save()
return HttpResponseRedirect('/tickets/')
else:
if update_requested:
# This is a requested to update an existing ticket.
# Bind the ticket data to a form.
form = TestTicketForm(instance=ticket_instance)
else:
form = TestTicketForm()
return render(request, 'ticket_tracker/ticket_update.html',
{ 'form': form, 'ticket': ticket_instance})
My Template
{% block content %}
<div class="container">
<form action="/tickets/test-ticket/{{ ticket.id }}/" method="post">
{% csrf_token %}
{{ form.as_p }}
<div class="form-group">
<button type="submit">Save</button>
</div>
</form>
</div>
{% endblock content %}
It appears this has been answered here before. In my model I used the to_field argument in the creation of the ForeignKey field, but the ModelChoiceField is expecting to use the id when the 'initial' value is passed to it. There are several options to fix this in my example including:
Remove the to_field parameter from the ForeignKey field in the model.
When creating the form instance in the view, set the 'initial' parameter for the field using the field's id from the model instance, e.g.,
form = TestTicketForm(request.POST,
instance=ticket_instance,
initial={'ticket_type': instance.ticket_type.id)
Set the form field's initial value in the forms __init__() method. Again this uses the field's id from the model instance. For example:
class TestTicketForm(ModelForm):
ticket_type = ModelChoiceField(TicketType.objects.all(),
widget=RadioSelect,
empty_label=None)
def __init__(self, *args, **kwargs):
super(TestTicketForm, self).__init__(*args, **kwargs)
if self.instance is not None:
self.initial['ticket_type'] = self.instance.ticket_type.id
Option #1 above would require a schema and data migrations in my database. Options #2 and #3 are similar but I chose option #3 since it makes my view code slightly cleaner.