I'm wondering if there is a simple way of creating a "duplicate" ModelForm in Django - i.e. a form that is prefilled with the content of an existing model instance (excepting certain fields, such as those that are unique), but creates a new instance when saved.
I was thinking along the lines of supplying an instance to a ModelForm so that the data is prefilled as with an "edit" form, then setting the instance to None before saving, but this gives a "'NoneType' object has no attribute 'pk'" error when calling .save() on the form. It seems the act of supplying an instance when constructing the form creates some dependency on it being there at the end.
I have had trouble finding a solution to this problem, but I can't imagine a "duplicate" form being too unique, so maybe I am missing something simple?
Any help would be appreciated.
I think what you need is a way to fill in the initial values for the fields in the form. The best way to accomplish this would be to create a dictionary of initial values (keyed by field name) from an existing instance and supply this to the form.
Something like this:
class AddressForm(forms.ModelForm):
class Meta:
model = Address
# Inside view:
address = Address.object.get(**conditions)
initial = dict()
for field in ('state', 'zipcode'): # Assuming these are the fields you want to pre-fill
initial[field] = getattr(address, field)
form = AddressForm(initial = initial)
class AddressForm(forms.ModelForm):
class Meta:
model = Address
# Inside view:
address = Address.object.get(pk=<your-id>)
address.pk = None # that's the trick, after form save new object will be created
form = AddressForm(instance=address)
Related
I want to show only options already stored in models' ManyToManyField.
I have model Order which I want to have a Model based form like this:
class OrderForm(ModelForm):
class Meta:
model = Order
fields = ['amount', 'color']
Now I do not want to display all colors as choices, but instead only color instances saved in ManyToManyField of another model. The other model is Design:
class Design(models.Model):
color = models.ManyToManyField('maker.Color')
# ...
Is this at all possible while using ModelForm?
Attempt
I have tried doing it by having a ModelForm of Design and setting instance:
class ColorForm(ModelForm):
class Meta:
model = Design
fields = ['color']
And then in view:
color_form = ColorForm(instance=design)
But I don't exactly understand what setting instance does, and I think instance is not what I am looking for as it still lists all colors.
The instance setting has nothing to do with limiting the choices. In essence, it simply populates the form's values with the ones from a specific record. You usually provide an instance in an edit operation, whereas you skip it in an add operation.
The representation of a models.ManyToManyField in the ModelForm is a forms.ChoiceField for which you can simply override its queryset property, and specify the queryset you desire.
Therefore, in your view:
form = OrderForm()
form.fields['color'].queryset = Design.object.all() # for example
I was using a model formset to generate a table of forms for a list of objects.
Forms:
class UserTypeModelForm(ModelForm):
account_type = ChoiceField(label='User type',
choices=ACCOUNT_OPTIONS, required=False)
class Meta:
model = get_user_model()
fields = ('account_type',)
UserTypeModelFormSet = modelformset_factory(get_user_model(),
form=UserTypeModelForm,
extra=0)
View:
formset = UserTypeModelFormSet(queryset=users, prefix='formset')
Now my client wants to be able to modify a related field: user.employee_profile.visible.
I tryed to add a field to the form, and then passing "initial" and "queryset" to the formset, but It looks like it just takes one.
How would you guys do this?
Thanks
with model formsets, the initial values only apply to extra forms, those that aren’t bound to an existing object instance.
Django docs
The queryset provides the selected/entered values for the bound fields, the initial for the extra fields (in your case 0).
But you can override the initial value in e.g. your views when you created a field called employee in this case:
for form in forms:
# Don't override a selected value.
if not form.fields['employee'].initial:
form.fields['employee'].initial = my_init
I am new to Django and I am using 1.7. I am looking for an example, on how to create a form that is populated with the current values which are stored. This way the user can see the forms current values and just modify the the ones they want. Then click save.
Assuming you're talking about values stored on a model, you can prepopulate a ModelForm using instance:
#######forms.py#############
class ArticleForm(ModelForm):
class Meta:
model = Article
fields = ['title']
#######views.py################
#create a normal ModelForm without any pre-populated values
blank_form = ArticleForm()
#create a ModelForm pre-populated with a specific object. first, get that object
article = Article.objects.get(pk=1)
form = ArticleForm(instance=article)
Taken from https://docs.djangoproject.com/en/1.7/topics/forms/modelforms/
In an my model, I've the following
--- models.py ---
class A(models.Model):
my_Bs = models.ManyToManyField('B', through='AlinksB')
...
class B(models.Model):
...
class AlinksB(models.Model):
my_A = models.ForeignKey(A)
my_B = models.models.ForeignKey(B)
order = models.IntegerField()
So is the corresponding admin (A admin view has an inline to link B instances, and I prepared the required to custom this inline's formset and forms):
--- admin.py ---
class AlinksBInlineForm(forms.ModelForm):
class Meta:
model = AlinksB
class AlinksBInlineFormset(forms.models.BaseInlineFormSet): # there also is a BaseModelFormset
form = AlinksBInlineForm
class AlinksBInline(admin.TabularInline):
formset = AlinksBInlineFormset
model = AlinksB
class AAdmin(admin.ModelAdmin):
form = AForm
inlines = (AlinksBInline,)
...
class BAdmin(admin.ModelAdmin):
...
Now to custom the forms validation, nothing difficult: just override the "clean" method of the form object. If you want many different forms in the formset, I think you just have to change some manually in the "init" method of the formset. But what about programatically validating all the forms when we clean the formset, and that only under some conditions.
In my case: how to automatically set the "order" field (in the inline of A admin view) with an autoincrement if all the orders (inline rows to remove excluded) are empty ?!
I just spent a lot of time Googling about trying to perform automatic form cleaning during a formset validation in Django Framework. After a few days a couldn't figure a solution so I started looking right into Django's source code to see how work fields, widgets, forms and formsets.
Here is what I understood:
-All the data POSTed by the user when he submits the formset it stored in the "data" attribute of the formset. This attribute is very ugly and cannot be directly used.
- The form is just a wrapper for fields (it calls all the fields' clean methods and fill error buffers, and only a few more)
-The form fields have a widget. This widget allow getting back the field's raw value from the "data" attribute of the formset
form.add_prefix('field name') # returns the 'field prefix', the key of formset.data used to retrieve the field's raw value
form.fields['field name'].widget.value_from_datadict(form.data, form.files, 'field prefix') # returns the raw value
-The form fields also have a method to transform the raw value into a right python value (in my case: order is an integer, or None if the field has been left empty)
form.fields['field name'].to_python(raw_value) # returns a value with the right type
-You can change the value of one of the fields from the formset with the following code
form.data.__setitem__('field prefix', value) # code to update an iterable knowing the key to change
-Once you have modified the fields value, you can call the "full_clean" method of the forms to retry cleaning them (this will remove the previous errors).
-Once you have validated again the forms, you can retry validating the formset with its "full_clean" method too. But take care to avoid infinite loops
-The forms clean data can only be used has a read-only data, to add more error messages in the form or the formset
An other solution would be to manually change the "form.clean_data" attribute, and clean the formset.errors and all the form.errors
Hope it could help somebody in the same situation as me !
Ricola3D
I have a System that can have one or more Models. I have modeled this relationship in the database with a manytomany field. The code below is for editing the system and its associated methods in a single form.
Adding a new method by filling out its form and pressing submit works only the first time. If I then make a small change and submit again, I get the following message (generated by the code below):
METHODFORMSET.ERRORS: [{}, {'name': [u'Method with this Name already exists.']}]
This is caused by the fact that the name field is unique, but it should have updated, not created a new record, even though I am using the POST data to generate the methodformset instance...
Note that this behaviour only applies to the last appended method instance, not to ones that were already present in the table.
Here is the relevant code, can anyone let me know what I am doing wrong?
def sysedit(request, sys_id):
system = System.objects.get(id=sys_id)
MethodFormSet = modelformset_factory(Method, form=MethodForm)
post = None
if request.POST:
post = request.POST.copy()
if 'add_method' in request.POST:
post['method-TOTAL_FORMS'] = repr(int(
post['method-TOTAL_FORMS'])+ 1)
systemform = SystemForm(data=post, instance=system)
methodformset = MethodFormSet(data=post, prefix='method',
queryset=Method.objects.filter(id__in=system.method.all()))
if methodformset.is_valid():
mfs = methodformset.save()
print 'SAVED-method', mfs
for mf in mfs:
if systemform.is_valid():
sp = systemform.save(mf)
print 'SYSTEM', sp
else:
print 'SYSFORMSET.ERRORS:', systemform.errors
else:
print 'METHODFORMSET.ERRORS:', methodformset.errors
return render_to_response('sysedit.html',
{'systemform': systemform,
'methodformset': methodformset,
'system': system},
context_instance=RequestContext(request))
class System(models.Model):
method = models.ManyToManyField(Method)
...
class Method(models.Model):
name = models.CharField(unique=True)
...
class MethodForm(ModelForm):
class Meta:
model = Method
class SystemForm(ModelForm):
def save(self, new_method=None, commit=True, *args, **kwargs):
m = super(SystemForm, self).save(commit=False, *args, **kwargs)
if new_method:
m.method.add(new_method)
if commit:
m.save()
return m
class Meta:
model = System
exclude = ('method')
[EDIT after Sergzach's answer]:
The problem is not how to deal with the Method with this name already exists error, but to prevent that from occurring in the first place. I think the actual problem may have something to do with the way modelformsets deal with new forms. Somehow it looks like it always tries to create a new instance for the last formset, regardless of whether it already exits.
So if I do not add a new formset after the last one was appended, the modelformset will try to re-create the last one (even though it was just created on the previous submit).
The initial situation is that I have 1 valid Method instance and 1 new unbound instance in the methodformset. I then fill out the form and hit save, which validates both Methods and binds the 2nd one, which is then saved to the table.
So far all is well, but if I then hit save the 2nd time the error occurs. Maybe this has to do with the fact that method-TOTAL_FORMS=2 and method-INITIAL_FORMS=1. Could it be that this causes modelformset to force a create on the 2nd Method?
Can anyone confirm/deny this?
[Edit after a weekend of not looking at the code]:
The problem is caused by the fact that I am saving the forms in the view and after saving, I am sending the original methodformset instance (from before the save) to the template. The problem can be solved by re-instantiating modelformset after the save, using the queryset and NOT the POST data.
So the general rule to prevent errors like this, is either to go to a different page after a save (avoid it altogether), or use the above solution.
Before I post this as THE solution, I need to do more testing.
You can validate each form when saving a formset. I have created a simple example (similar to your code) and it works well for me. It creates new objects if there is no object with a such name otherwise it edits an existing object.
You need a form to edit your model objects:
class EditMethodForm( forms.ModelForm ):
class Meta:
model = Method
exclude = ( 'name', )
Then instead of methodformset.is_valid() you do the next:
for methodform in methodformset:
try:
instance = Method.objects.get( name = request.POST[ 'name' ] )
except Method.DoesNotExist:
methodform.save()
else:
editmethodform = EditMethodForm( request.POST, instance = instance )
if editmethodform.is_valid():
editmethodform.save()
There are some additional features in your code. I show the working principle. Is it enough to understand the solution?
I have solved the problem by re-instantiating modelformset after the save (see edit at the bottom of the question)