I would like to display a warning message if I am into an editing form and hide it if I am in a creation form of a Django ModelForm.
form.is_bound tell me if the form was previously populated but how to test if the ModelForm was set with an existing instance ?
I tried this hasattr(form.instance, 'pk') but is it the right way to do so ?
Cheers,
Natim
Try checking if form.instance.pk is None.
hasattr(form.instance, 'pk') will always return True, because every model instance has a pk field, even when it has not yet been saved to the database.
As pointed out by #Paullo in the comments, this will not work if you manually define your primary key and specify a default, e.g. default=uuid.uuid4.
Since the existed instance would be passed as an argument with the keyword instance to create the model-form, you can observe this in your custom initializer.
class Foo(ModelForm):
_newly_created: bool
def __init__(self, *args, **kwargs):
self._newly_created = kwargs.get('instance') is None
super().__init__(*args, **kwargs)
I encountered this issue but in my case, am using UUID for PK. Though the accepted answer is correct for most cases but fails if you are not using Django default auto increment PK.
Defining a model property gives me the ability to access this value from both, Model, View and Template as attribute of the model
#property
def from_database(self):
return not self._state.adding
I found that self.instance is set in super().init anyway
class BaseModelForm(BaseForm):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
empty_permitted=False, instance=None, use_required_attribute=None,
renderer=None):
...
if instance is None:
# if we didn't get an instance, instantiate a new one
self.instance = opts.model()
https://github.com/django/django/blob/65e03a424e82e157b4513cdebb500891f5c78363/django/forms/models.py#L300
so we can track instance just before super().init called.
So my solution is to override init method and set custom field to track in all followed form's methods.
def __init__(self, *args: Any, instance=None, **kwargs: Any) -> None:
super().__init__(*args, instance=instance, **kwargs)
self.is_new_instance = not bool(instance)
and usage:
def _any_form_method(self):
if self.is_new_instance:
Related
It seems like if a ModelForm is given an instance, it ignores any values you provide for initial and instead sets it to the value of the instance -- even if that instance is an empty model record.
Is there any way to create a form with an instance and have it set initial data?
I need it because I'm saving related records and they don't appear to save correctly unless the ModelForm is given an instance when created.
I'm sure the answer to this is straightforward and I'm just missing something obvious.
Here is the relevant code:
in the view:
form = form_class(person=person, conference=conference, initial=initial, instance=registration)
where form_class is RegistrationForm and then in the registration form:
class RegisterForm(forms.ModelForm):
... fields here ...
def __init__(self, *args, **kwargs):
... other code ...
self.person = kwargs.pop('person')
super(RegisterForm, self).__init__(*args, **kwargs)
for key, in self.fields.keys():
if hasattr(self.person, key):
self.fields[k].initial = getattr(self.person, key)
Then when I call the field, the related fields are empty.
Figured this out after a little bit of googling.
You have to set the initial value before calling super.
So instead of looping through self.fields.keys(), I had to type out the list of fields that I wanted and looped through that instead:
class RegisterForm(forms.ModelForm):
... fields here ...
initial_fields = ['first_name', 'last_name', ... ]
def __init__(self, *args, **kwargs):
... other code ...
self.person = kwargs.pop('person')
for key in self.initial_fields:
if hasattr(self.person, key):
self.fields[k].initial = getattr(self.person, key)
super(RegisterForm, self).__init__(*args, **kwargs)
#Daria rightly points out that you don't have self.fields before calling super. I'm pretty sure this will work:
class RegisterForm(forms.ModelForm):
... fields here ...
initial_fields = ['first_name', 'last_name', ... ]
def __init__(self, *args, **kwargs):
... other code ...
initial = kwargs.pop('initial', {})
self.person = kwargs.pop('person')
for key in self.initial_fields:
if hasattr(self.person, key):
initial[key] = initial.get(key) or getattr(self.person, key)
kwargs['initial'] = initial
super(RegisterForm, self).__init__(*args, **kwargs)
In this version, we use the initial argument to pass the values in. It's also written so that if we already have a value in initial for that field, we don't overwrite it.
Sounds to me that you may be looking for a bound form. Not entirely sure, I'm trying to unpick a similar issue:
Django forms can be instantiated with two arguments which control this kind of thing. As I understand it:
form = MyForm(initial={...}, data={...}, ...)
initial will set the possible values for the fields—like setting a queryset—data will set the actual (or selected) values of a form and create a bound form. Maybe that is what you want. Another, tangental, point you might find interesting is to consider a factory method rather than a constructor, I think the syntax is more natural:
class MyForm(forms.ModelForm):
...
#staticmethod
def makeBoundForm(user):
myObjSet = MyObject.objects.filter(some_attr__user=user)
if len(myObjSet) is not 0:
data = {'myObject': myObjSet[0]}
else:
raise ValueError()
initial = {'myObject': myObjSet}
return MyForm(initial=initial, data=data)
You can also pass extra variables to the class when initializing it. The values you pass can then override initial or POST data.
class RegisterForm(forms.ModelForm):
... fields here ...
def __init__(self, person, conference, *args, **kwargs):
... other code ...
super(RegisterForm, self).__init__(*args, **kwargs)
self.fields['person'] = person
self.fields['conference'] = conference
form = RegisterForm(person, conference, initial=initial, instance=registration)
Use ModelAdmin.get_changeform_initial_data. For example, if you add initial data for form field "report_datetime"
def get_changeform_initial_data(self, request):
initial_data = super().get_changeform_initial_data(request)
initial_data.update(report_datetime=<my_initial_datetime>)
return initial_data
Works for 3.2+. I'm not sure about older versions.
See django docs
How can I set an initial value of a field in the automatically generated form for adding a Django model instance, before the form is displayed? I am using Django 1.3.1.
My model is the following:
class Foo(models.Model):
title = models.CharField(max_length=50)
description = models.TextField()
and the current admin form is really nothing special
class FooAdmin(admin.ModelAdmin):
ordering = ('title',)
When I use the admin page to add a new instance of Foo, I get a nice form with empty fields for title and description. What I would like is that the description field is set with a template that I obtain by calling a function.
My current best attempt at getting there is this:
def get_default_content():
return 'this is a template for a Foo description'
class FooAdminForm(django.forms.ModelForm):
class Meta:
model = Foo
def __init__(self, *args, **kwargs):
kwargs['initial'].update({'description': get_default_content()})
super(FooAdminForm, self).__init__(self, *args, **kwargs)
class FooAdmin(admin.ModelAdmin):
ordering = ('title',)
form = FooAdminForm
but if I try this I get this Django error:
AttributeError at /admin/bar/foo/add/
'FooForm' object has no attribute 'get'
Request Method: GET
Request URL: http://localhost:8000/admin/bar/foo/add/
Django Version: 1.3.1
Exception Type: AttributeError
Exception Value: 'FooForm' object has no attribute 'get'
Exception Location: /www/django-site/venv/lib/python2.6/site-packages/django/forms/widgets.py in value_from_datadict, line 178
I don't know what is wrong here, and what I should do to make it work. What I also find strange about this error (apart from the fact that I see it at all) is that there is no FooForm in my code at all?
Alasdair's approach is nice but outdated. Radev's approach looks quite nice and as mentioned in the comment, it strikes me that there is nothing about this in the documentation.
Apart from those, since Django 1.7 there is a function get_changeform_initial_data in ModelAdmin that sets initial form values:
def get_changeform_initial_data(self, request):
return {'name': 'custom_initial_value'}
You need to include self as the first argument in your __init__ method definition, but should not include it when you call the superclass' method.
def __init__(self, *args, **kwargs):
# We can't assume that kwargs['initial'] exists!
if 'initial' not in kwargs:
kwargs['initial'] = {}
kwargs['initial'].update({'description': get_default_content()})
super(FooAdminForm, self).__init__(*args, **kwargs)
Having said that, a model field can take a callable for its default, so you may not have to define a custom admin form at all.
class Foo(models.Model):
title = models.CharField(max_length=50)
description = models.TextField(default=get_default_content)
More then 3 years later,
But actually what you should do is override admin.ModelAdmin formfield_for_dbfield .. like this:
class FooAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, **kwargs):
field = super(FooAdmin, self).formfield_for_dbfield(db_field, **kwargs)
if db_field.name == 'description':
field.initial = 'My initial description'
elif db_field.name == 'counter':
field.initial = get_counter() + 1
return field
Cheers;
When adding new objects, it is convenient to use get_changeform_initial_data() as suggested by Wtower.
However, when changing existing objects, that does not work (see source).
In that case, you could extend ModelAdmin.get_form() as follows (using the OP's example):
def get_form(self, request, obj=None, change=False, **kwargs):
if obj and not obj.description:
obj.description = get_default_content()
return super().get_form(request, obj, change, **kwargs)
I've just created a forms.models.BaseInlineFormSet to override the default formset for a TabularInline model. I need to evaluate the user's group in formset validation (clean) because some groups must write a number inside a range (0,20).
I'm using django admin to autogenerate the interface.
I've tried getting the request and the user from the kwargs in the init method, but I couldn't get the reference.
This is what I have now:
class OrderInlineFormset(forms.models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(OrderInlineFormset, self).__init__(*args, **kwargs)
def clean(self):
# get forms that actually have valid data
count = 0
for form in self.forms:
try:
if form.cleaned_data:
count += 1
if self.user.groups.filter(name='Seller').count() == 1:
if form.cleaned_data['discount'] > 20:
raise forms.ValidationError('Not authorized to specify a discount greater than 20%')
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
if count < 1:
raise forms.ValidationError('You need to specify at least one item')
class OrderItemInline(admin.TabularInline):
model = OrderItem
formset = OrderInlineFormset
Then I use it as inlines = [OrderItemInline,] in my ModelAdmin.
Unfortunatly self.user is always None so I cannot compare the user group and the filter is not applied. I need to filter it because other groups should be able to specify any discount percent.
How can I do? If you also need the ModelAdmin code I'll publish it (I just avoided to copy the whole code to avoid confusions).
Well, I recognise my code there in your question, so I guess I'd better try and answer it. But I would say first of all that that snippet is really only for validating a minimum number of forms within the formset. Your use case is different - you want to check something within each form. That should be done with validation at the level of the form, not the formset.
That said, the trouble is not actually with the code you've posted, but with the fact that that's only part of it. Obviously, if you want to get the user from the kwargs when the form or formset is initialized, you need to ensure that the user is actually passed into that initialization - which it isn't, by default.
Unfortunately, Django's admin doesn't really give you a proper hook to intercept the initialization itself. But you can cheat by overriding the get_form function and using functools.partial to wrap the form class with the request argument (this code is reasonably untested, but should work):
from functools import partial
class OrderForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(OrderForm, self).__init__(*args, **kwargs)
def clean(self)
if self.user.groups.filter(name='Seller').count() == 1:
if self.cleaned_data['discount'] > 20:
raise forms.ValidationError('Not authorized to specify a discount greater than 20%')
return self.cleaned_data
class MyAdmin(admin.ModelAdmin):
form = OrderForm
def get_form(self, request, obj=None, **kwargs):
form_class = super(MyAdmin, self).get_form(request, obj, **kwargs)
return functools.partial(form_class, user=request.user)
Here's another option without using partials. First override the get_formset method in your TabularInline class.
Assign request.user or what ever extra varaibles you need to be available in the formset as in example below:
class OrderItemInline(admin.TabularInline):
model = OrderItem
formset = OrderInlineFormset
def get_formset(self, request, obj=None, **kwargs):
formset = super(OrderProductsInline, self).get_formset(request, obj, **kwargs)
formset.user = request.user
return formset
Now the user is available in the formset as self.user
class OrderInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
print(self.user) # is available here
I am trying to add dynamically new form fields (I used this blog post), for a form used in admin interface :
class ServiceRoleAssignmentForm(forms.ModelForm):
class Meta:
model = ServiceRoleAssignment
def __init__(self, *args, **kwargs):
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
self.fields['test'] = forms.CharField(label='test')
class ServiceRoleAssignmentAdmin(admin.ModelAdmin):
form = ServiceRoleAssignmentForm
admin.site.register(ServiceRoleAssignment, ServiceRoleAssignmentAdmin)
However, no matter what I try, the field doesn't appear on my admin form ! Could it be a problem related to the way admin works ? Or to ModelForm ?
Thank for any help !
Sébastien
PS : I am using django 1.3
When rendering your form in template, fields enumerating from fieldsets variable, not from fields. Sure you can redefine fieldsets in your AdminForm, but then validations will fail as original form class doesn't have such field. One workaround I can propose is to define this field in form definition statically and then redefine that field in form's init method dynamically. Here is an example:
class ServiceRoleAssignmentForm(forms.ModelForm):
test = forms.Field()
class Meta:
model = ServiceRoleAssignment
def __init__(self, *args, **kwargs):
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
# Here we will redefine our test field.
self.fields['test'] = forms.CharField(label='test2')
I actually have a the same issue which I'm working through at the moment.
While not ideal, I have found a temporary workaround that works for my use case. It might be of use to you?
In my case I have a static name for the field, so I just declared it in my ModelForm. as normal, I then override the init() as normal to override some options.
ie:
def statemachine_form(for_model=None):
"""
Factory function to create a special case form
"""
class _StateMachineBaseModelForm(forms.ModelForm):
_sm_action = forms.ChoiceField(choices=[], label="Take Action")
class Meta:
model = for_model
def __init__(self, *args, **kwargs):
super(_StateMachineBaseModelForm, self).__init__(*args, **kwargs)
actions = (('', '-----------'),)
for action in self.instance.sm_state_actions():
actions += ((action, action),)
self.fields['_sm_action'] = forms.ChoiceField(choices=actions,
label="Take Action")
if for_model: return _StateMachineBaseModelForm
class ContentItemAdmin(admin.ModelAdmin):
form = statemachine_form(for_model=ContentItem)
Now as I mentioned before, this is not entirely 'dynamic', but this will do for me for the time being.
I have the exact same problem that, if I add the field dynamically, without declaring it first, then it doesn't actually exist. I think this does in fact have something to do with the way that ModelForm creates the fields.
I'm hoping someone else can give us some more info.
Django - Overriding get_form to customize admin forms based on request
Try to add the field before calling the super.init:
def __init__(self, *args, **kwargs):
self.fields['test'] = forms.CharField(label='test')
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
I have a FormWizard where I need data from the first form to pass to the constructor of the second form so I can build a dynamic form.
I can get the first form's data via the process_step of the FormWizard.
I create the fields of the second form with a database call of the list of fields.
class ConditionWizardDynamicQuestions(forms.Form):
def __init__(self, DynamicQuestions=None, *args, **kwargs):
super(ConditionWizardDynamicQuestions, self).__init__(*args, **kwargs)
questions = Question.objects.filter(MYDATA = DATA_FROM_1STFORM)
for q in questions:
dynField = FieldFactory(q)
self.fields[q.label] = dynField
How can I pass over the DATA_FROM_1STFORM ?
my resultant code:
I abandoned the init of the form, and switched it to the CreateQuestions def. Then used the wizard's get_form override to alter the form after creation.
class ConditionWizard(SessionFormWizard):
def get_form(self, request, storage, step=None, data=None, files=None):
form = super(ConditionWizard, self).get_form(request, storage, step, data, files)
stepIndex = self.get_step_index(request, storage, step)
if stepIndex == 1:
form.CreateQuestions(request.session["WizardConditionId"])
if stepIndex == 3:
form.fields['hiddenConditionId'].initial = request.session["WizardConditionId"]
form.fields['medicationName'].queryset = Medication.objects.filter(condition = request.session["WizardConditionId"])
return form
I solved this by overriding get_form_kwargs for the WizardView. It normally just returns an empty dictionary that get_form populates, so by overriding it to return a dictionary with the data you need prepopulated, you can pass kwargs to your form init.
def get_form_kwargs(self, step=None):
kwargs = {}
if step == '1':
your_data = self.get_cleaned_data_for_step('0')['your_data']
kwargs.update({'your_data': your_data,})
return kwargs
Then, in your form init method you can just pop the kwarg off before calling super:
self.your_data = kwargs.pop('client', None)
FormWizard already passes the data from each previous form to the next form. If you want to get that data in order to instantiate a class (for example, if a form has special keyword arguments that it requires), one way of doing it is to grab the querydict by overriding get_form in your form wizard class. For example:
class SomeFormWizard(FormWizard):
def get_form(self, step, data=None):
if step == 1 and data: # change this to whatever step requires
# the extra data
extra_data = data.get('key_from_querydict')
if extra_data:
return self.form_list[step](data,
keyword_argument=extra_data,
prefix=self.prefix_for_step(step),
initial=self.initial.get(step, None))
# Fallback for the other forms.
return self.form_list[step](data,
prefix=self.prefix_for_step(step),
initial=self.initial.get(step, None))
Note that you can also override parse_params(self, request, *args, **kwargs) in FormWizard to access the url/request data, just like you would in a view, so if you have request data (request.user, for instance) that is going to be needed for all of the forms, it might be better to get the data from there.
Hope this helps.
Override the get_form_kwargs method of your form wizard in views
view.py
class FormWizard(SessionWizardView):
def get_form_kwargs(self, step=None):
kwargs = {}
if step == '1':
step0_form_field = self.get_cleaned_data_for_step('0')['previous_form_field_data']
kwargs.update({'step0_form_field': step0_form_field})
return kwargs
Override the init of your form by popping up the data you got from the previous field to create a dynamic field.
forms.py
class MyForm(forms.Form):
#some fields
class MyForm1(forms.Form):
def __init__(self, *args, **kwargs):
extra = kwargs.pop('step0_form_field')
super(MyForm1, self).__init__(*args, **kwargs)
for i in range(extra):
self.fields['name_%s' % i] = forms.CharField()
I was recently working with django form wizard, and i was solving the similar issue. I don't think you can pass data to init, however, what you can do, is override the init constructor:
next_form = self.form_list[1]
# let's change the __init__
# function which will set the choices :P
def __init__(self, *args, **kw):
super(next_form, self).__init__(*args, **kw)
self.fields['availability'].choices = ...
next_form.__init__ = __init__
It's quite annoying that in python you can't declare and assign a function in one go and have to put it in the namespace (unless you use lambdas), but oh well.