How to fill a hidden field in a Django form? - python

I have a Django ModelForm with a required hidden field (required means that the corresponding field in the model is null=True and blank=True). Where and how should I fill this field (after submitting the form by the user) so that it is possible to save this form? My first idea is to tamper with clean() in the form and do something like:
def clean(self):
cleaned_data = super(SomeForm, self).clean()
cleaned_data['hidden_field'] = value_i_want_to_be_here
del self._errors['hidden_field']
return cleaned_data
It works, but it doesn't seem to be the best way.

If I understand your question correctly you can go about this by creating an instance of form.save() then put the value you want in the specified field of the newly created instance. Finally save the instance. Example below:
def form(request):
if form.is_valid():
instance = form.save()
instance.your_hidden_field = value_i_want_to_be_here
instance.save()
That should do the trick.

You could do as you're doing OR you could:
Override save in ModelForm -> alter the field there directly in the model. In this way, you should not include the field in the modelform -> use an excludes = attribute specifying such exclusion.
Override save in the model class or, even better and more separate, use a pre_save signal to modify the value. You will also need to exclude the field in the form.
To override a modelform save:
def save(self, commit=True):
instance = super(YourModelFormClass, self).save(False)
instance.your_hidden_field = a_value
if commit:
instance.save()
return instance
Remember this is a method you must write in your custom model form class.
To override a model save:
def save(self, *a, **kwa):
self.your_hidden_field = a_value
super(YourModelClass, self).save(*a, **kwa)
To use a signal:
from django.core.signals import pre_save
from django.dispatch import receiver
#receiver(pre_save, sender=YourModelClass)
def your_custom_signal_handler(sender, **kwargs):
instance = kwargs['instance']
instance.your_hidden_field = a_value
Usually the third is recommended if the change has to be considered in the model class. If, on the other hand, the change is in the form, you could do what you're doing or use the first alternative I gave you.

Related

Django custom model save() method for a non-persisted attribute

In Django 4 custom model save() method, how do you pass a form non-persistent form value?
For example:
The model form below has the non-persistent field called client_secret.
class ClientModelForm(forms.ModelForm):
client_secret = forms.CharField(initial=generate_urlsafe_uuid)
This field will never be saved, it is auto-generated and will be required to make a hash for a persisted field in my model save() method.
class Client(models.Model):
client_hash = models.BinaryField(editable=False, blank=True, null=True)
def save(self, *args, **kwargs):
""" Save override to hash the client secret on creation. """
if self._state.adding:
"GET THE CLIENT SECRET FROM THE FORM"
client_hash = make_hash_key(client_secret)
self.client_hash = client_hash
How do I get the client secret value from the form above code example? Is this the most appropriate approach?
You would need to call the super class' save method at the end like so:
super(Client, self).save(*args, **kwargs)
For saving the form, you would need to add a model to the modelform's Meta class, and you would need to add the following logic into a view:
form = Form(request.METHOD)
if form.is_valid():
uuid = form.instance.client_secret
form.save()
Where form would look like:
class ClientForm(forms.ModelForm):
fields...
class Meta:
model = ClientModel
Highly recommend taking a look at the docs
Edit
To get the logic into the model save, you could pass it into the kwargs of the save, save(client_secret=client_secret), and then in the save method you could try:
self.field = kwargs.get('client_secret')
then when saving the form, as shown above, you could do
save(client_secret=uuid)

Django form User is 'not-null' even though it's manually set

For ease of use, one of the fields in my form can have multiple foos, but eachfoo should/will create it's own record in the database. I have a ModelForm (MyForm) that captures the fields that are repeated for each instance of foo.
Inside my views.py I copy MyForm for each instance of foo. All that is working, except for the bit where the logged in user is set as the submitter of the form. The following error is thrown: null value in column "submitter_id" violates not-null constraint.
models.py
class MyModel(models.Model):
foo_bar = models.CharField(max_length=10)
foo_baz = models.CharField(max_length=10)
submitter = models.ForeignKey(User)
forms.py
class MyForm(forms.ModelForm):
class Meta:
Model = MyModel
exclude = ['foo_bar','foo_baz','submitter']
views.py
Note: obj is a dictionary of however many foos were entered into the form
my_form = MyForm(request.POST)
if my_form.is_valid():
for k,v in obj:
copy = MyForm(request.post)
copy.save(commit=False)
copy.foo_bar = k
copy.foo_baz = v
copy.submitter = request.user # I have inspected this, and request.user is an instance of the User model
copy.save() # <-- here is where I get the error above
You need to set it on the model instance, which is returned from the form save. Also, for some reason you are re-instantiating the form after checking it is valid; you should not do that.
if my_form.is_valid():
instance = copy.save(commit=False)
instance.submitter = request.user # I have inspected this, and request.user is an instance of the User model
instance.save()
(Instead of messing about with dicts containing copies of forms, you should use formsets.)
Try setting it in the following way
copy.instance.submitter = request.user

ModelForm with a reverse ManytoMany field

I'm having trouble getting ModelMultipleChoiceField to display the initial values of a model instance. I haven't been able to find any documentation about the field, and the examples I've been reading are too confusing. Django: ModelMultipleChoiceField doesn't select initial choices seems to be similar, but the solution that was given there is not dynamic to the model instance.
Here is my case (each database user is connected to one or more projects):
models.py
from django.contrib.auth.models import User
class Project(Model):
users = ManyToManyField(User, related_name='projects', blank=True)
forms.py
from django.contrib.admin.widgets import FilteredSelectMultiple
class AssignProjectForm(ModelForm):
class Meta:
model = User
fields = ('projects',)
projects = ModelMultipleChoiceField(
queryset=Project.objects.all(),
required=False,
widget=FilteredSelectMultiple('projects', False),
)
views.py
def assign(request):
if request.method == 'POST':
form = AssignProjectForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
return HttpResponseRedirect('/index/')
else:
form = AssignProjectForm(instance=request.user)
return render_to_response('assign.html', {'form': form})
The form that it returns is not selecting the instance's linked projects (it looks like: Django multi-select widget?). In addition, it doesn't update the user with any selections made when the form is saved.
Edit: Managed to solve this using the approach here: http://code-blasphemies.blogspot.com/2009/04/dynamically-created-modelmultiplechoice.html
Here's a solution that is better than the older ones, which really don't work.
You have to both load the existing related values from the database when creating the form, and save them back when saving the form. I use the set() method on the related name (manager) which does all the work for you: taking away existing relations that are not selected anymore, and adding new ones which have become selected. So you don't have to do any looping or checking.
class AssignProjectForm(ModelForm):
def __init__(self, *args, **kwargs):
super(AssignProjectForm, self).__init__(*args, **kwargs)
# Here we fetch your currently related projects into the field,
# so that they will display in the form.
self.fields['projects'].initial = self.instance.projects.all(
).values_list('id', flat=True)
def save(self, *args, **kwargs):
instance = super(AssignProjectForm, self).save(*args, **kwargs)
# Here we save the modified project selection back into the database
instance.projects.set(self.cleaned_data['projects'])
return instance
Aside from simplicity, using the set() method has another advantage that comes into play if you use Django signals (eg. post_save etc) on your m2m relation: If you add and remove entries one at a time in a loop, you'll get signals for each object. But if you do it in one operation using set(), you'll get just one signal with a list of objects. If the code in your signal handler does significant work, this is a big deal.
ModelForm's don't automatically work for reverse relationships.
Nothing is happening on save() because a ModelForm only knows what to do with its own fields - projects is not a field on the User model, it's just a field on your form.
You'll have to tell your form how to save itself with this new field of yours.
def save(self, *args, **kwargs):
for project in self.cleaned_data.get('projects'):
project.users.add(self.instance)
return super(AssignProjectForm, self).save(*args, **kwargs)

Django: Faking a field in the admin interface?

I have a model, Foo. It has several database properties, and several properties that are calculated based on a combination of factors. I would like to present these calculated properties to the user as if they were database properties. (The backing factors would be changed to reflect user input.) Is there a way to do this with the Django admin interface?
I would suggest you subclass a modelform for Foo (FooAdminForm) to add your own fields not backed by the database. Your custom validation can reside in the clean_* methods of ModelForm.
Inside the save_model method of FooAdmin you get the request, an instance of Foo and the form data, so you could do all processing of the data before/after saving the instance.
Here is an example for a model with a custom form registered with django admin:
from django import forms
from django.db import models
from django.contrib import admin
class Foo(models.Model):
name = models.CharField(max_length=30)
class FooAdminForm(forms.ModelForm):
# custom field not backed by database
calculated = forms.IntegerField()
class Meta:
model = Foo
class FooAdmin(admin.ModelAdmin):
# use the custom form instead of a generic modelform
form = FooAdminForm
# your own processing
def save_model(self, request, obj, form, change):
# for example:
obj.name = 'Foo #%d' % form.cleaned_data['calculated']
obj.save()
admin.site.register(Foo, FooAdmin)
Providing initial values for custom fields based on instance data
(I'm not sure if this is the best solution, but it should work.)
When a modelform for a existing model instance in the database is constructed, it gets passed this instance. So in FooAdminForm's __init__ one can change the fields attributes based on instance data.
def __init__(self, *args, **kwargs):
super(FooAdminForm, self).__init__(*args, **kwargs)
# only change attributes if an instance is passed
instance = kwargs.get('instance')
if instance:
self.fields['calculated'].initial = (instance.bar == 42)
It's easy enough to get arbitrary data to show up in change list or make a field show up in the form: list_display arbitrarily takes either actual model properties, or methods defined on the model or the modeladmin, and you can subclass forms.ModelForm to add any field type you'd like to the change form.
What's far more difficult/impossible is combining the two, i.e. having an arbitrary piece of data on the change list that you can edit in-place by specifying list_editable. Django seems to only accept a true model property that corresponds to a database field. (even using #property on the method in the model definition is not enough).
Has anyone found a way to edit a field not actually present on the model right from the change list page?
In the edit form, put the property name into readonly_fields (1.2 upwards only).
In the changelist, put it into list_display.
You can use the #property decorator in your model (Python >= 2.4):
class Product(models.Model):
#property
def ranking(self):
return 1
"ranking" can then be used in list_display:
class ProductAdmin(admin.ModelAdmin):
list_display = ('ranking', 'asin', 'title')

How to show hidden autofield in django formset

A Django autofield when displayed using a formset is hidden by default. What would be the best way to show it?
At the moment, the model is declared as,
class MyModel:
locid = models.AutoField(primary_key=True)
...
When this is rendered using Django formsets,
class MyModelForm(ModelForm):
class Meta:
model = MyModel
fields = ('locid', 'name')
it shows up on the page as,
<input id="id_form-0-locid" type="hidden" value="707" name="form-0-locid"/>
Thanks.
Edit
I create the formset like this -
LocFormSet = modelformset_factory(MyModel)
pformset = LocFormSet(request.POST, request.FILES, queryset=MyModel.objects.order_by('name'))
Second Edit
Looks like I'm not using the custom form class I defined there, so the question needs slight modification..
How would I create a formset from a custom form (which will show a hidden field), as well as use a custom queryset?
At the moment, I can either inherit from a BaseModelFormSet class and use a custom query set, or I can use the ModelForm class to add a custom field to a form. Is there a way to do both with a formset?
Third Edit
I'm now using,
class MyModelForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
locid = forms.IntegerField(min_value = 1, required=True)
self.fields['locid'].widget.attrs["type"] = 'visible'
self.queryset = MyModel.objects.order_by('name')
class Meta:
model = MyModel
fields = ('locid', 'name')
LocFormSet = modelformset_factory(MyModel, form = MyModelForm)
pformset = LocFormSet()
But this still doesn't
Show locid
Use the custom query that was specified.
Try changing the default field type:
from django import forms
class MyModelForm(ModelForm):
locid = forms.IntegerField(min_value=1, required=True)
class Meta:
model = MyModel
fields = ('locid', 'name')
EDIT: Tested and works...
As you say, you are not using the custom form you have defined. This is because you aren't passing it in anywhere, so Django can't know about it.
The solution is simple - just pass the custom form class into modelformset_factory:
LocFormSet = modelformset_factory(MyModel, form=MyModelForm)
Edit in response to update 3:
Firstly, you have the redefinition for locid in the wrong place - it needs to be at the class level, not inside the __init__.
Secondly, putting the queryset inside the form won't do anything at all - forms don't know about querysets. You should go back to what you were doing before, passing it in as a parameter when you instantiate the formset. (Alternatively, you could define a custom formset, but that seems like overkill.)
class MyModelForm(ModelForm):
locid = forms.IntegerField(min_value=1, required=True)
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.fields['locid'].widget.attrs["type"] = 'visible'
class Meta:
model = MyModel
fields = ('locid', 'name')
LocFormSet = modelformset_factory(MyModel, form = MyModelForm)
pformset = LocFormSet(request.POST, request.FILES,
queryset=MyModel.objects.order_by('name')))
Okay, none of the approaches above worked for me. I solved this issue from the template side, finally.
There is a ticket filed (http://code.djangoproject.com/ticket/10427), which adds a "value" option to a template variable for a form. For instance, it allows,
{{form.locid.value}}
to be shown. This is available as a patch, which can be installed in the SVN version of django using "patch -p0 file.patch"
Remember, the {{form.locid.value}} variable will be used in conjunction with the invisible form - otherwise, the submit and save operations for the formset will crash.
This is Not the same as {{form.locid.data}} - as is explained in the ticket referred to above.
The reason that the autofield is hidden, is that both BaseModelFormSet and BaseInlineFormSet override that field in add_field. The way to fix it is to create your own formset and override add_field without calling super. Also you don't have to explicitly define the primary key.
you have to pass the formset to modelformset_factory:
LocFormSet = modelformset_factory(MyModel,
formset=VisiblePrimaryKeyFormSet)
This is in the formset class:
from django.forms.models import BaseInlineFormSet, BaseModelFormSet, IntegerField
from django.forms.formsets import BaseFormSet
class VisiblePrimaryKeyFormset(BaseModelFormSet):
def add_fields(self, form, index):
self._pk_field = pk = self.model._meta.pk
if form.is_bound:
pk_value = form.instance.pk
else:
try:
pk_value = self.get_queryset()[index].pk
except IndexError:
pk_value = None
form.fields[self._pk_field.name] = IntegerField( initial=pk_value,
required=True) #or any other field you would like to display the pk in
BaseFormSet.add_fields(self, form, index) # call baseformset which does not modify your primary key field

Categories

Resources