I've got a Form. I want to include a hidden field that returns a model. I'll set it's value in the view; I just need it to be posted along to the next page.
What field am I supposed to use in the form class?
A hidden field that returns a model? So a model instance ID?
The forms.HiddenInput widget should do the trick, whether on a FK field or CharField you put a model instance ID in.
class MyForm(forms.Form):
hidden_2 = forms.CharField(widget=forms.HiddenInput())
hidden_css = forms.CharField(widget=forms.MostWidgets(attrs={'style': 'display:none;'}))
I suppose the fastest way to get this working is
class MyForm(forms.Form):
model_instance = forms.ModelChoiceField(queryset=MyModel.objects.all(), widget=forms.HiddenInput())
form = MyForm({'model_instance': '1'})
form.cleaned_data['model_instance']
But I don't like the idea of supplying MyModel.objects.all() if you're going to specify one item anyways.
It seems like to avoid that behavior, you'd have to override the form __init__ with a smaller QuerySet.
I think I prefer the old fashioned way:
class MyForm(forms.Form):
model_instance = forms.CharField(widget=forms.HiddenInput())
def clean_model_instance(self):
data = self.cleaned_data['model_instance']
if not data:
raise forms.ValidationError()
try:
instance = MyModel.objects.get(id=data)
except MyModel.DoesNotExist:
raise forms.ValidationError()
return instance
The approach in Yuji's answer uses a clean_model_instance method on the form which is fine if you're only ever doing this once in your code base. If you do it more often, then you might benefit from implementing a custom model field.
This is the code I have:
from django import forms
class ModelField(forms.Field):
Model = None
def prepare_value(self, value):
"""Inject entities' id value into the form's html data"""
if isinstance(value, self.Model):
return value.id
return value
def to_python(self, value):
"""More or less stolen from ModelChoiceField.to_python"""
if value in self.empty_values:
return None
try:
value = self.Model.objects.get(id=value)
except (ValueError, self.Model.DoesNotExist):
raise forms.ValidationError('%s does not exist'
% self.Model.__class__.__name__.capitalize())
return value
If you use that as a base class and then specialise it with your own models then it becomes a useful based. For example:
# In app/fields.py
from .models import CustomModel
class CustomModelField(ModelField):
Model = CustomModel
Then you can pair that with whatever widget you need at the time:
# in app/forms.py
class MyForm(forms.Form):
hidden_custom_model_field = CustomModelField(widget=forms.HiddenInput())
other_widget_custom_model_field = CustomModelField(widget=MyCustomWidget())
Related
(This is all pseudocode and is not guaranteed to run.)
I am trying to make a "django admin form generator function" that outputs a django form. The current use case is to write reusable code that disallows admins from leaving a field empty, without also marking these fields as non-nullable.
So suppose there exists a model Foo, in which are some nullable fields:
class Foo(Model):
field1 = FloatField(null=True, blank=True, default=0.0)
field2 = FloatField(null=True, blank=True, default=0.0)
field3 = FloatField(null=True, blank=True, default=0.0)
and corresponding FooAdmin and FooForm, such that these fields cannot be made None from the admin.
class FooAdmin(ModelAdmin):
class FooForm(ModelForm):
class Meta(object):
model = Foo
fields = '__all__'
def _ensure_no_blanks(self, field):
value = self.cleaned_data.get(field)
if value is None:
raise forms.ValidationError(_('This field is required.'))
return value
# repeat methods for every field to check
def clean_field1(self):
return self._ensure_no_blanks('field1')
def clean_field2(self):
return self._ensure_no_blanks('field2')
def clean_field3(self):
return self._ensure_no_blanks('field3')
form = FooForm
As you can see, having to write clean_field1, clean_field2, and clean_field_n are repetitive and error-prone, so I write this helper function to generate model admins:
import functools
from django import forms
def form_with_fields(model_class, required_fields):
class CustomForm(forms.ModelForm):
class Meta(object):
model = model_class
fields = '__all__'
def ensure_no_blanks(self, field):
print field
value = self.cleaned_data.get(field)
if value is None:
raise forms.ValidationError('This field is required.')
return value
# make a clean_bar method for every field that I need to check for None
for field_name in required_fields:
handler = functools.partial(CustomForm.ensure_no_blanks, field=field_name)
setattr(CustomForm, 'clean_' + field_name, lambda self: handler(self))
return CustomForm
class CustomAdmin(ModelAdmin):
form = form_with_fields(Foo, ['field1', 'field2', 'field3'])
However, if you run such an admin, and if you do try to save the Foo model through the admin, you will see print field printing field3 three times in the terminal (i.e. all partials are retaining the last-run value).
Other attempts include overriding CustomForm's __getattr__(), and wrapping CustomForm in a type('Form', (CustomForm,), ..., which also exhibit the same behavior.
Is there a dry way to achieve this?
setattr(CustomForm, 'clean_' + field_name, lambda self: handler(self))
The problem is the lambda function. The handler is defined outside of the lambda. It is accessed when the lambda is called, not when it is defined. Since this is after the for-loop has completed, you always get the function that uses the last field name.
See this FAQ entry from the Python docs for a fuller explanation.
In this case, you don't need a lambda at all. Just use handler itself.
setattr(CustomForm, 'clean_' + field_name, handler)
However, as Paulo suggests in the comment, it would be much
def clean(self):
for field in required_fields(self):
self.ensure_no_blanks(field)
You may need to change ensure_no_blanks to use add_error so that the validation errors are added to the correct field.
if value is None:
self.add_error(field, 'This field is required.')
Another option would be to set the required=True for the fields in the __init__ method, then let the form take care of validation.
class CustomForm(forms.ModelForm):
class Meta(object):
model = model_class
fields = '__all__'
def __init__(self, *args, **kwargs):
super(CustomForm, self).__init__(*args, **kwargs)
for field in required_fields:
self.fields[field].required = True
I'm overriding the admin form of a model to modify the choices of a ForeignKey field.
When selecting a choice in the admin form, and saving, I get a ValueError:
Cannot assign "u'6'": "MyModel1.mymodel2" must be a "MyModel2" instance
where 6 is the id of the selected choice.
The new choices is built as ((<choice_1_id>, <choice_1_label>), (<choice_2_id>, <choice_2_label>),...), and I get the same html for the rendered select widget as if I don't modify the choices (apart from the ordering of course).
If I comment self.fields['mymodel2'] = forms.ChoiceField(choices=choices) in MyModel1AdminForm.__init__() I get no error...
Anybody could help?
models.py
class MyModel1(models.Model):
mymodel2 = ForeignKey(MyModel2)
# more fields...
admin.py
class MyModel1AdminForm(forms.ModelForm):
class Meta:
model = MyModel1
def __init__(self, *args, **kwargs):
super(MyModel1AdminForm, self).__init__(*args, **kwargs)
# create choices with ((<choice_1_id>, <choice_1_label>), (<choice_2_id>, <choice_2_label>),...)
self.fields['mymodel2'] = forms.ChoiceField(choices=choices, widget=SelectWithDisabled) # http://djangosnippets.org/snippets/2453/
class MyModel1Admin(admin.ModelAdmin):
form = MyModel1AdminForm
my_site.register(MyModel1, MyModel1Admin)
mymodel2 is the Foreign Key field. You need to supply the queryset if you want to change the choices instead of adding your custom choices:
self.fields['mymodel2'].queryset = MyModel2.objects.all()
If you need to construct the choices manually something like
choices = MyModel2.objects.values_list('pk', 'title')
should work with a standard ChoiceField, where title is the field of the model you want to use as label/verbose name for your choice.
Looking at the snippet you are using values_list won't work so you could fallback to a list comprehension:
[(c.pk, {'label': c.title, 'disabled': False}) for c in MyModel2.objects.all()]
Though you obviously need some more logic to decide whether a choice is enabled or disabled.
I ended up overriding ModelChoiceField:
class MyModelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
level = getattr(obj, obj._mptt_meta.level_attr)
return {'label': obj.name), 'disabled': check_disabled(obj)}
I want my Django custom model field to set an attribute on the model instance.
I'm sure it's not working this way but here is an example:
class MyField(models.Field):
__metaclass__ = models.SubfieldBase
def __init__(self, *args, **kwargs):
super(MyField, self).__init__(*args, **kwargs)
model_instance = ????
setattr(model_instance, "extra_attribute", "It's working!")
class MyModel(models.Model):
my_field = MyField()
model_instance = MyModel.objects.get(pk=123)
print model_instance.extra_attribute # output: "It's working!"
Django's ForeignKey model field is doing a similar thing, so it is possible :P
I think ForeignKey field is using the contribute_to_class method.
You can create a special Proxy class that will replace the field. When getting or setting field value, you can use 'obj' attribute to access model instance. See this example:
class ObjectField(models.PositiveSmallIntegerField):
class ObjectProxy:
def __init__(self, field):
self.field = field
def __get__(self, obj, model):
if obj is None:
return self.field # this is required for initializing model field
value = obj.__dict__[self.field.name] # get actual field value
# ... here you can do something with value and model instance ("obj")
return value
def __set__(self, obj, value):
# same here
obj.__dict__[self.field.name] = value
def contribute_to_class(self, cls, name):
super().contribute_to_class(cls, name)
# set up our proxy instead of usual field
setattr(cls, name, ObjectField.ObjectProxy(self))
You do not have access to the model instance from inside your Field object, sorry. Django's ForeignKey accomplishes the foo_id thing by having separate name and attname fields, but the actual setting of foo_id = 123 is done the same way as all the other model fields, deep in the QuerySet code, without interacting with the field classes.
And conceptually, what you're trying to do is a bad idea - action-at-a-distance. What if adding a particular field could cause bugs in unrelated model functionality, say, if an attribute another field was expecting got overridden? It would be difficult to debug, to say the least. I don't know what your underlying goal is, but it should probably be done in model code, not a field class.
Here's a ModelField that does what you want:
https://gist.github.com/1987190
That's actually pretty old (like maybe pre-1.0, don't remember now), had to dust it off a bit - I'm not sure if it still works. But it's definitely doable, hopefullly this gives you an idea.
init is called when Django processes the Model Class, not the Model Instance. So, you can add the attribute to the Model Class (e.g. by using 'add_to_class' http://www.alrond.com/en/2008/may/03/monkey-patching-in-django/ ). To add the attribute to the instance you should override the init of the instance (but I think this is not your case).
How about
model_instance = SomeExtraModel.objects.get(pk=1456)
replacing 1456 with something that makes sense
I'm trying to learn Django and I've ran into some confusing points. I'm currently having trouble creating a movie using a form. The idea of the form is to give the user any field he'd like to fill out. Any field that the user fills out will be updated in its respective sql table (empty fields will be ignored). But, the form keeps giving me the error "Enter a list of values" when I submit the form. To address this, I thought stuffing the data from the form into a list and then returning that list would solve this.
The first idea was to override the clean() in my ModelForm. However, because the form fails the is_valid() check in my views, the cleaned_data variable in clean() doesn't contain anything. Next, I tried to override the to_python(). However, to_python() doesn't seem to be called.
If I put __metaclass__ = models.SubfieldBase in the respective model, I receive the runtime error
"TypeError: Error when calling the
metaclass bases
metaclass conflict: the metaclass of a derived class must be a
(non-strict) subclass of the
metaclasses of all its bases"
My approach doesn't seem to work. I'm not sure how to get around the 'Enter a list of values" error! Any advice?
Here is the relevant code (updated):
models.py
""" Idea:
A movie consists of many equipments, actors, and lighting techniques. It also has a rank for the particular movie, as well as a title.
A Theater consists of many movies.
A nation consists of many theaters.
"""
from django.db import models
from django.contrib.auth.models import User
class EquipmentModel(models.Model):
equip = models.CharField(max_length=20)
# user = models.ForeignKey(User)
class ActorModel(models.Model):
actor = models.CharField(max_length=20)
# user = models.ForeignKey(User)
class LightModel(models.Model):
light = models.CharField(max_length=20)
# user = models.ForeignKey(User)
class MovieModel(models.Model):
# __metaclass__ = models.SubfieldBase
rank = models.DecimalField(max_digits=5000, decimal_places=3)
title = models.CharField(max_length=20)
equipments = models.ManyToManyField(EquipmentModel, blank=True, null=True)
actors = models.ManyToManyField(ActorModel, blank=True, null=True)
lights = models.ManyToManyField(LightModel, blank=True, null=True)
class TheaterModel(models.Model):
movies = models.ForeignKey(MovieModel)
class NationModel(models.Model):
theaters = models.ForeignKey(TheaterModel)
=====================================
forms.py
"""
These Modelforms tie in the models from models.py
Users will be able to write to any of the fields in MovieModel when creating a movie.
Users may leave any field blank (empty fields should be ignored, ie: no updates to database).
"""
from django import forms
from models import MovieModel
from django.forms.widgets import Textarea
class MovieModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MovieModelForm, self).__init__(*args, **kwargs)
self.fields["actors"].widget = Textarea()
self.fields["equipments"].widget = Textarea()
self.fields["lights"].widget = Textarea()
def clean_actors(self):
data = self.cleaned_data.get('actors')
print 'cleaning actors'
return [data]
class Meta:
model = MovieModel
=============================================
views.py
""" This will display the form used to create a MovieModel """
from django.shortcuts import render_to_response
from django.template import RequestContext
from forms import MovieModelForm
def add_movie(request):
if request.method == "POST":
form = MovieModelForm(request.POST)
if form.is_valid():
new_moviemodel = form.save()
return HttpResponseRedirect('/data/')
else:
form = MovieModelForm()
return render_to_response('add_movie_form.html', {form:form,}, context_instance=RequestContext(request))
The probable problem is that the list of values provided in the text area can not be normalized into a list of Models.
See the ModelMultipleChoiceField documentation.
The field is expecting a list of valid IDs, but is probably receiving a list of text values, which django has no way of converting to the actual model instances. The to_python will be failing within the form field, not within the form itself. Therefore, the values never even reach the form.
Is there something wrong with using the built in ModelMultipleChoiceField? It will provide the easiest approach, but will require your users to scan a list of available actors (I'm using the actors field as the example here).
Before I show an example of how I'd attempt to do what you want, I must ask; how do you want to handle actors that have been entered that don't yet exist in your database? You can either create them if they exist, or you can fail. You need to make a decision on this.
# only showing the actor example, you can use something like this for other fields too
class MovieModelForm(forms.ModelForm):
actors_list = fields.CharField(required=False, widget=forms.Textarea())
class Meta:
model = MovieModel
exclude = ('actors',)
def clean_actors_list(self):
data = self.cleaned_data
actors_list = data.get('actors_list', None)
if actors_list is not None:
for actor_name in actors_list.split(','):
try:
actor = Actor.objects.get(actor=actor_name)
except Actor.DoesNotExist:
if FAIL_ON_NOT_EXIST: # decide if you want this behaviour or to create it
raise forms.ValidationError('Actor %s does not exist' % actor_name)
else: # create it if it doesnt exist
Actor(actor=actor_name).save()
return actors_list
def save(self, commit=True):
mminstance = super(MovieModelForm, self).save(commit=commit)
actors_list = self.cleaned_data.get('actors_list', None)
if actors_list is not None:
for actor_name in actors_list.split(","):
actor = Actor.objects.get(actor=actor_name)
mminstance.actors.add(actor)
mminstance.save()
return mminstance
The above is all untested code, but something approaching this should work if you really want to use a Textarea for a ModelMultipleChoiceField. If you do go down this route, and you discover errors in my code above, please either edit my answer, or provide a comment so I can. Good luck.
Edit:
The other option is to create a field that understands a comma separated list of values, but behaves in a similar way to ModelMultipleChoiceField. Looking at the source code for ModelMultipleChoiceField, it inhertis from ModelChoiceField, which DOES allow you to define which value on the model is used to normalize.
## removed code because it's no longer relevant. See Last Edit ##
Edit:
Wow, I really should have checked the django trac to see if this was already fixed. It is. See the following ticket for information. Essentially, they've done the same thing I have. They've made ModelMutipleChoiceField respect the to_field_name argument. This is only applicable for django 1.3!
The problem is, the regular ModelMultipleChoiceField will see the comma separated string, and fail because it isn't a List or Tuple. So, our job becomes a little more difficult, because we have to change the string to a list or tuple, before the regular clean method can run.
class ModelCommaSeparatedChoiceField(ModelMultipleChoiceField):
widget = Textarea
def clean(self, value):
if value is not None:
value = [item.strip() for item in value.split(",")] # remove padding
return super(ModelCommaSeparatedChoiceField, self).clean(value)
So, now your form should look like this:
class MovieModelForm(forms.ModelForm):
actors = ModelCommaSeparatedChoiceField(
required=False,
queryset=Actor.objects.filter(),
to_field_name='actor')
equipments = ModelCommaSeparatedChoiceField(
required=False,
queryset=Equipment.objects.filter(),
to_field_name='equip')
lights = ModelCommaSeparatedChoiceField(
required=False,
queryset=Light.objects.filter(),
to_field_name='light')
class Meta:
model = MovieModel
to_python AFAIK is a method for fields, not forms.
clean() occurs after individual field cleaning, so your ModelMultipleChoiceFields clean() methods are raising validation errors and thus cleaned_data does not contain anything.
You haven't provided examples for what kind of data is being input, but the answer lies in form field cleaning.
http://docs.djangoproject.com/en/dev/ref/forms/validation/#cleaning-a-specific-field-attribute
You need to write validation specific to that field that either returns the correct data in the format your field is expecting, or raises a ValidationError so your view can re-render the form with error messages.
update: You're probably missing the ModelForm __init__ -- see if that fixes it.
class MovieModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MovieModelForm, self).__init__(*args, **kwargs)
self.fields["actors"].widget = Textarea()
def clean_actors(self):
data = self.cleaned_data.get('actors')
# validate incoming data. Convert the raw incoming string
# to a list of ids this field is expecting.
# if invalid, raise forms.ValidationError("Error MSG")
return data.split(',') # just an example if data was '1,3,4'
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.