I'd like to display a number of forms via a ModelFormSet where each one of the forms displays in turn InlineFormSets for all objects connected to the object.
Now I'm not really sure how to provide the instances for each ModelFormSet. I thought about subclassing BaseModelFormSet but I have no clue on where to start and would like to know whether this is possible at all before I go through all the trouble.
Thanks in advance!
I found an article which focuses on the exact problem. It works fine!
http://yergler.net/blog/2009/09/27/nested-formsets-with-django/
For the sake of completeness I copied the code fragments:
class Block(models.Model):
description = models.CharField(max_length=255)
class Building(models.Model):
block = models.ForeignKey(Block)
address = models.CharField(max_length=255)
class Tenant(models.Model):
building = models.ForeignKey(Building)
name = models.CharField(max_length=255)
unit = models.CharField(max_length=255)
form django.forms.models import inlineformset_factory, BaseInlineFormSet
TenantFormset = inlineformset_factory(models.Building, models.Tenant, extra=1)
class BaseBuildingFormset(BaseInlineFormSet):
def add_fields(self, form, index):
# allow the super class to create the fields as usual
super(BaseBuildingFormset, self).add_fields(form, index)
# created the nested formset
try:
instance = self.get_queryset()[index]
pk_value = instance.pk
except IndexError:
instance=None
pk_value = hash(form.prefix)
# store the formset in the .nested property
form.nested = [
TenantFormset(data=self.data,
instance = instance,
prefix = 'TENANTS_%s' % pk_value)]
BuildingFormset = inlineformset_factory(models.Block, models.Building, formset=BaseBuildingFormset, extra=1)
As jnns said, you want to use inlineformset_factory
Related
I iterate through a list of Block objects, instantiate a ModelForm for each of them with a mapping dictionary that links a block_type to a ModelForm model, and then append the form to a list which I pass off to a template for display.
for block in blocks:
block_instance = block_map[block.block_type].objects.get(id=block.id)
new_form = block_forms[block.block_type]
new_form_instance = new_form(
request.user,
request.POST or None,
instance=block_instance,
prefix = block.id
)
form_zones.append(new_form_instance)
Later, while checking request.POST I validate each form
if request.POST.get("save_submit"):
for zone_form_check in story_zones:
for block_form_check in zone_form_check:
if block_form_check.is_valid():
print(block_form_check.cleaned_data.get("content"))
saved = block_form_check.save()
print(saved.content)
valid = True
if valid:
return redirect("Editorial:content", content_id=content_id)
cleaned_data.get("content") produces the updated data, but even after calling save() on the valid form, saved.content produces the object's old content attribute. In other words, a valid form is having save() called upon it, but it is not saving.
One of the forms in question (and currently my only one) is:
class Edit_Text_Block_Form(ModelForm):
content = forms.CharField(widget = forms.Textarea(
attrs = {
"class": "full_tinymce"
}),
label = "",
)
class Meta:
model = TextBlock
fields = []
def __init__(self, user, *args, **kwargs):
self.user = user
super(Edit_Text_Block_Form, self).__init__(*args, **kwargs)
The model in question is a TextBlock, which inherits from a Block objets. Both of those are below:
class Block(models.Model):
zone = models.ForeignKey(Zone)
order = models.IntegerField()
weight = models.IntegerField()
block_type = models.CharField(max_length=32, blank=True)
class Meta:
ordering = ['order']
def delete(self, *args, **kwargs):
# Calling custom delete methods of child blocks
child = block_map[self.block_type].objects.get(id=self.id)
if getattr(child, "custom_delete", None):
child.custom_delete()
# Overriding delete to check if there are any other blocks in the zone.
# If not, the zone itself is deleted
zones = Block.objects.filter(zone=self.zone).count()
if zones <= 1:
self.zone.delete()
# Children of Block Object
class TextBlock(Block):
content = models.TextField(blank=True)
Any ideas for why calling saved = block_form_check.save() isn't updating my model?
Thanks!
I think this is because you've effectively excluded all the model fields from the form by setting fields = [] in the form's Meta class. This means that Django no longer relates the manually-defined content field on the form with the one in the model.
Instead, set fields to ['content'], and it should work as expected.
TL;DR form name cannot start with a number as per html4 specs
Try prefix = "block_%s" % block.id
I'm trying to find best approach for what i want. And i could use some help for that.
I have Model A and Model B. Model B has
modela = forms.ForeignKey(Model a)
I want to create a view where you can edit both single Model A and several Model B's on same page. Django has formsets for this and they work great.
I have one detail though, that messes things up tiny bit. Namely - i want the widgets or model B fields to be different based on what choices they have done in previous fields of same object. Because - based on type, the widget has to be datetime picker input or plain textinput.
Model B looks like this:
class ModelB(models.Model):
m0odela = models.ForeignKey(ModelA)
target_value = models.CharField()
target_type = models.CharField( choices = ( there are choices))
target_threshold = models.CharField()
I know i can provide my own form for formset and i could do this widget assignment in that form.
But the problem is, that when formset has no instances/queryset then i cant check if 'target_type' has been set for forms instance. So i would have to do it based on self.data or self.initial in form. But self.initial is also not present in form.__init__(). What i can work with is self.data - but that is raw request.POST or request.GET data - which contains all keys like 'mymodelb_set-0-target_type'.
So i'm bit lost here. Do i have to do some key parsing and figure out which -target_type belongs to current form and get chosen value there and assign widgets based on this value?
Or do i have to create my own subclass of BaseInlineFormSet and override _construc_form there somehow? So that form would have initial key with related data in **kwargs.
Has someone ran into this kind of problem before?
Alan
Well i had to solve it so i solved it as good/bad i could.
I created my own subclass of inline formset:
class MyInlineFormSet(BaseInlineFormSet):
def _construct_form(self, i, **kwargs):
initial = {}
fname = '%s-%s-%s' % (self.prefix, i, 'important_field_name')
initial['target_type'] = self.data[fname] if fname in self.data.keys() else 'km'
kwargs.update({'initial':initial})
form = super(MyInlineFormSet, self)._construct_form(i, **kwargs)
return form
And then in the form class:
class MyNiftyForm(forms.ModelForm):
class Meta:
model = MyAwesomeObject
fields=('field_one', 'field_two', 'field_three')
def __init__(self, *args, **kwargs):
super(ServiceTargetForm, self).__init__(*args, **kwargs)
if self.instance:
if self.instance.field_one == 'date':
self.fields['field_one'].widget.attrs['class'] = 'datepicker'
if self.initial:
if self.initial['field_one'] == 'date':
self.fields['field_one'].widget.attrs['class'] = 'datepicker'
and then in view:
MySuperCoolFormSet = inlineformset_factory(ImportantObject, MyAwesomeObject, extra = 1, form = MyNiftyForm, formset = MyInlineFormSet)
And it works.
Alan
I have a M2M relationship between two Models which uses an intermediate model. For the sake of discussion, let's use the example from the manual:
class Person(models.Model):
name = models.CharField(max_length=128)
def __unicode__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __unicode__(self):
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
I'd like to make use of Django's Class-based views, to avoid writing CRUD-handling views. However, if I try to use the default CreateView, it doesn't work:
class GroupCreate(CreateView):
model=Group
This renders a form with all of the fields on the Group object, and gives a multi-select box for the members field, which would be correct for a simple M2M relationship. However, there is no way to specify the date_joined or invite_reason, and submitting the form gives the following AttributeError:
"Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead."
Is there a neat way to override part of the generic CreateView, or compose my own custom view to do this with mixins? It feels like this should be part of the framework, as the Admin interface atomatically handles M2M relationships with intermediates using inlines.
You must extend CreateView:
from django.views.generic import CreateView
class GroupCreate(CreateView):
model=Group
and override the form_valid():
from django.views.generic.edit import ModelFormMixin
from django.views.generic import CreateView
class GroupCreate(CreateView):
model = Group
def form_valid(self, form):
self.object = form.save(commit=False)
for person in form.cleaned_data['members']:
membership = Membership()
membership.group = self.object
membership.person = person
membership.save()
return super(ModelFormMixin, self).form_valid(form)
As the documentation says, you must create new memberships for each relation between group and person.
I saw the form_valid override here:
Using class-based UpdateView on a m-t-m with an intermediary model
class GroupCreate(CreateView):
model = Group
def form_valid(self, form):
self.object = form.save(commit=False)
### delete current mappings
Membership.objects.filter(group=self.object).delete()
### find or create (find if using soft delete)
for member in form.cleaned_data['members']:
x, created = Membership.objects.get_or_create(group=self.object, person=member)
x.group = self.object
x.person = member
#x.alive = True # if using soft delete
x.save()
return super(ModelFormMixin, self).form_valid(form)
'For reference, I didn't end up using a class-based view, instead I did something like this:
def group_create(request):
group_form = GroupForm(request.POST or None)
if request.POST and group_form.is_valid():
group = group_form.save(commit=False)
membership_formset = MembershipFormSet(request.POST, instance=group)
if membership_formset.is_valid():
group.save()
membership_formset.save()
return redirect('success_page.html')
else:
# Instantiate formset with POST data if this was a POST with an invalid from,
# or with no bound data (use existing) if this is a GET request for the edit page.
membership_formset = MembershipFormSet(request.POST or None, instance=Group())
return render_to_response(
'group_create.html',
{
'group_form': recipe_form,
'membership_formset': membership_formset,
},
context_instance=RequestContext(request),
)
This may be a starting point for a Class-based implementation, but it's simple enough that it's not been worth my while to try to shoehorn this into the Class-based paradigm.
I was facing pretty the same problem just a few days ago. Django has problems to process intermediary m2m relationships.
This is the solutions what I have found useful:
1. Define new CreateView
class GroupCreateView(CreateView):
form_class = GroupCreateForm
model = Group
template_name = 'forms/group_add.html'
success_url = '/thanks'
Then alter the save method of defined form - GroupCreateForm. Save is responsible for making changes permanent to DB. I wasn't able to make this work just through ORM, so I've used raw SQL too:
1. Define new CreateView
class GroupCreateView(CreateView):
class GroupCreateForm(ModelForm):
def save(self):
# get data from the form
data = self.cleaned_data
cursor = connection.cursor()
# use raw SQL to insert the object (in your case Group)
cursor.execute("""INSERT INTO group(group_id, name)
VALUES (%s, %s);""" (data['group_id'],data['name'],))
#commit changes to DB
transaction.commit_unless_managed()
# create m2m relationships (using classical object approach)
new_group = get_object_or_404(Group, klient_id = data['group_id'])
#for each relationship create new object in m2m entity
for el in data['members']:
Membership.objects.create(group = new_group, membership = el)
# return an object Group, not boolean!
return new_group
Note:I've changed the model a little bit, as you can see (i have own unique IntegerField for primary key, not using serial. That's how it got into get_object_or_404
Just one comment, when using CBV you need to save the form with commit=True, so the group is created and an id is given that can be used to create the memberships.
Otherwise, with commit=False, the group object has no id yet and an error is risen.
I need to make a form, which have 1 select and 1 text input. Select must be taken from database.
model looks like this:
class Province(models.Model):
name = models.CharField(max_length=30)
slug = models.SlugField(max_length=30)
def __unicode__(self):
return self.name
It's rows to this are added only by admin, but all users can see it in forms.
I want to make a ModelForm from that. I made something like this:
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=CHOICES),
}
but it doesn't work. The select tag is not displayed in html. What did I wrong?
UPDATE:
This solution works as I wanto it to work:
class ProvinceForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProvinceForm, self).__init__(*args, **kwargs)
user_provinces = UserProvince.objects.select_related().filter(user__exact=self.instance.id).values_list('province')
self.fields['name'].queryset = Province.objects.exclude(id__in=user_provinces).only('id', 'name')
name = forms.ModelChoiceField(queryset=None, empty_label=None)
class Meta:
model = Province
fields = ('name',)
Read Maersu's answer for the method that just "works".
If you want to customize, know that choices takes a list of tuples, ie (('val','display_val'), (...), ...)
Choices doc:
An iterable (e.g., a list or tuple) of
2-tuples to use as choices for this
field.
from django.forms.widgets import Select
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=( (x.id, x.name) for x in CHOICES )),
}
ModelForm covers all your needs (Also check the Conversion List)
Model:
class UserProvince(models.Model):
user = models.ForeignKey(User)
province = models.ForeignKey(Province)
Form:
class ProvinceForm(ModelForm):
class Meta:
model = UserProvince
fields = ('province',)
View:
if request.POST:
form = ProvinceForm(request.POST)
if form.is_valid():
obj = form.save(commit=True)
obj.user = request.user
obj.save()
else:
form = ProvinceForm()
If you need to use a query for your choices then you'll need to overwrite the __init__ method of your form.
Your first guess would probably be to save it as a variable before your list of fields but you shouldn't do that since you want your queries to be updated every time the form is accessed. You see, once you run the server the choices are generated and won't change until your next server restart. This means your query will be executed only once and forever hold your peace.
# Don't do this
class MyForm(forms.Form):
# Making the query
MYQUERY = User.objects.values_list('id', 'last_name')
myfield = forms.ChoiceField(choices=(*MYQUERY,))
class Meta:
fields = ('myfield',)
The solution here is to make use of the __init__ method which is called on every form load. This way the result of your query will always be updated.
# Do this instead
class MyForm(forms.Form):
class Meta:
fields = ('myfield',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make the query here
MYQUERY = User.objects.values_list('id', 'last_name')
self.fields['myfield'] = forms.ChoiceField(choices=(*MYQUERY,))
Querying your database can be heavy if you have a lot of users so in the future I suggest some caching might be useful.
the two solutions given by maersu and Yuji 'Tomita' Tomita perfectly works, but there are cases when one cannot use ModelForm (django3 link), ie the form needs sources from several models / is a subclass of a ModelForm class and one want to add an extra field with choices from another model, etc.
ChoiceField is to my point of view a more generic way to answer the need.
The example below provides two choice fields from two models and a blank choice for each :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=([['','-'*10]]+[[x.id, x.__str__()] for x in Speakers.objects.all()]))
event = forms.ChoiceField(choices=( [['','-'*10]]+[[x.id, x.__str__()] for x in Events.objects.all()]))
If one does not need a blank field, or one does not need to use a function for the choice label but the model fields or a property it can be a bit more elegant, as eugene suggested :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=((x.id, x.__str__()) for x in Speakers.objects.all()))
event = forms.ChoiceField(choices=(Events.objects.values_list('id', 'name')))
using values_list() and a blank field :
event = forms.ChoiceField(choices=([['','-------------']] + list(Events.objects.values_list('id', 'name'))))
as a subclass of a ModelForm, using the one of the robos85 question :
class MixedForm(ProvinceForm):
speaker = ...
I have two models related by a foreign key:
# models.py
class TestSource(models.Model):
name = models.CharField(max_length=100)
class TestModel(models.Model):
name = models.CharField(max_length=100)
attribution = models.ForeignKey(TestSource, null=True)
By default, a django ModelForm will present this as a <select> with <option>s; however I would prefer that this function as a free form input, <input type="text"/>, and behind the scenes get or create the necessary TestSource object and then relate it to the TestModel object.
I have tried to define a custom ModelForm and Field to accomplish this:
# forms.py
class TestField(forms.TextInput):
def to_python(self, value):
return TestSource.objects.get_or_create(name=value)
class TestForm(ModelForm):
class Meta:
model=TestModel
widgets = {
'attribution' : TestField(attrs={'maxlength':'100'}),
}
Unfortunately, I am getting: invalid literal for int() with base 10: 'test3' when attempting to check is_valid on the submitted form. Where am I going wrong? Is their and easier way to accomplish this?
Something like this should work:
class TestForm(ModelForm):
attribution = forms.CharField(max_length=100)
def save(self, commit=True):
attribution_name = self.cleaned_data['attribution']
attribution = TestSource.objects.get_or_create(name=attribution_name)[0] # returns (instance, <created?-boolean>)
self.instance.attribution = attribution
return super(TestForm, self).save(commit)
class Meta:
model=TestModel
exclude = ('attribution')
There are a few problems here.
Firstly, you have defined a field, not a widget, so you can't use it in the widgets dictionary. You'll need to override the field declaration at the top level of the form.
Secondly get_or_create returns two values: the object retrieved or created, and a boolean to show whether or not it was created. You really just want to return the first of those values from your to_python method.
I'm not sure if either of those caused your actual error though. You need to post the actual traceback for us to be sure.
TestForm.attribution expects int value - key to TestSource model.
Maybe this version of the model will be more convenient for you:
class TestSource(models.Model):
name = models.CharField(max_length=100, primary_key=True)
Taken from:
How to make a modelform editable foreign key field in a django template?
class CompanyForm(forms.ModelForm):
s_address = forms.CharField(label='Address', max_length=500, required=False)
def __init__(self, *args, **kwargs):
super(CompanyForm, self).__init__(*args, **kwargs)
try:
self.fields['s_address'].initial = self.instance.address.address1
except ObjectDoesNotExist:
self.fields['s_address'].initial = 'looks like no instance was passed in'
def save(self, commit=True):
model = super(CompanyForm, self).save(commit=False)
saddr = self.cleaned_data['s_address']
if saddr:
if model.address:
model.address.address1 = saddr
model.address.save()
else:
model.address = Address.objects.create(address1=saddr)
# or you can try to look for appropriate address in Address table first
# try:
# model.address = Address.objects.get(address1=saddr)
# except Address.DoesNotExist:
# model.address = Address.objects.create(address1=saddr)
if commit:
model.save()
return model
class Meta:
exclude = ('address',) # exclude form own address field
This version sets the initial data of the s_address field as the FK from self, during init , that way, if you pass an instance to the form it will load the FK in your char-field - I added a try and except to avoid an ObjectDoesNotExist error so that it worked with or without data being passed to the form.
Although, I would love to know if there is a simpler built in Django override.