Django add extra field to a ModelForm generated from a Model - django

I have to generate a FormSet from a model but I need to insert an "extra value" in to every form.
Specifically, I have a JApplet that generates some Markers and Paths on a image, and POST it on the server.
In my model lines are composed from two Markers. But when I POST it, because I'm using the id generated from the JApplet and not from the database, I will not know from which Markers a Path will be composed.
So I thought to insert a "temporary id" on the Marker on the form, and do the correct arrangements in the view before saving the Path.
I thought about defining a custom form for the markers, but it not seems to be very DRY, and I don't want to came back to this if I change the Marker model.
Here is the form:
class PointForm(forms.ModelForm):
temp_id = forms.IntegerField()
class Meta:
model = Point
def clean(self):
if any(self.errors):
# Don't bother validating the formset unless each form is valid on its own
return
ingresso = self.cleaned_data['ingresso']
ascensore = self.cleaned_data['ascensore']
scala = self.cleaned_data['scala']
if (ingresso and ascensore) or (ingresso and scala) or (ascensore and scala):
raise forms.ValidationError("A stair cannot be a elevator or an access!!!")
return self
def save(commit=True):
# do something with self.cleaned_data['temp_id']
super(PointForm).save(commit=commit)
And the model:
class Point(models.Model):
RFID = models.CharField(max_length=200, blank=True)
x = models.IntegerField()
y = models.IntegerField()
piano = models.ForeignKey(Floor)
ingresso = models.BooleanField()
The error:
ViewDoesNotExist at /admin/
Could not import buildings.views.getFloors. View does not exist in module buildings.views.
Request Method: GET
Request URL: http://127.0.0.1:8000/admin/
Django Version: 1.4.1
Exception Type: ViewDoesNotExist
Exception Value:
Could not import buildings.views.getFloors. View does not exist in module buildings.views.
Exception Location: /usr/local/lib/python2.7/dist-packages/django/core/urlresolvers.py in get_callable, line 101
The error is generated when I try to load the admin page, this page has no references at all with the form.
SOLUTION FOR EXCEPTION
Ok, I'll write here how to find out why Django was doing such a strange thing.
Here it's a correct way to find out what is the problem.
The exception was thrown because I forgot to add forms.py to the from django import forms.

You can add a field to a ModelForm. Unless you add a field named temp_id to your model you do not need to change this form when you change you model.
Example (with a model named Point):
class PointForm (forms.ModelForm):
temp_id = forms.IntegerField()
class Meta:
model = Point
def save(self, commit=True):
# do something with self.cleaned_data['temp_id']
return super(PointForm, self).save(commit=commit)
UPDATE: Forgot self in def save() and changed modelname to Point

To follow up relekang's answer, I had to be reminded to also return the last line as shown, so that the object's get_absolute_url() method could be automatically called on submission of the form:
return super(PointForm, self).save(commit=commit)

Related

Django Rest Framework - Override serializer create method for model with self-referential M2M field

I have a model that looks like this:
class Profile(models.Model):
following = models.ManyToManyField('Profile', related_name='followed')
admin_notes = models.TextField()
And a Django Rest Framework serializer that looks like this:
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Profile
fields = '__all__'
When creating a new Profile I want to interrupt the save/create to set admin_notes programmatically. I know how do to this inside a function-based view, but I am using DRF's viewsets this time and so I want to do this inside the serializer. The self-referential relationship is giving me trouble, though.
I tried this within the ProfileSerializer:
def create(self, **validated_data):
p = Profile(**validated_data)
p.admin_notes = 'Test'
p.save()
This, as well as trying Profile.objects.create instead of just making a new Profile instance in the first line, resulted in the following error:
Exception Value: "<Profile>" needs to have a value for field "id" before this many-to-many relationship can be used.
Trying this:
def create(self, validated_data):
validated_data['admin_notes'] = 'Test'
return super(ProfileSerializer, self).create(**validated_data)
Results in this error:
Exception Value: create() got an unexpected keyword argument 'following'
I understand that Django creates m2m relationships in a two-step-save process, so the error makes sense but still leaves me stumped as to how I can move forward. How can I set the value of admin_notes within the serializer given the way my model is set up?
Try this (assuming that you are passing the ids of the Profile instances to your m2m field):
def create(self, validated_data):
# First, remove following from the validated_data dict...
following_data = validated_data.pop('following', None)
# Set the admin_notes custom value...
validated_data['admin_notes'] = 'Test'
# Create the object instance...
profile = Profile.objects.create(**validated_data)
# Finally, add your many-to-many relationships...
if following_data:
for data in following_data:
profile.followed.add(Profile.objects.get(**data))
return profile

Django "Enter a list of values" form error when rendering a ManyToManyField as a Textarea

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'

Free-form input for ForeignKey Field on a Django ModelForm

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.

django manytomany validation

Please see the code below. Basically, when the user creates an object of this class, they need to specify the value_type. If value_type==2 (percentage), then percentage_calculated_on (which is a CheckboxSelectMultiple on the form/template side needs to have one or more items checked. The model validation isn't allowing me to validate like I'm trying to -- it basically throws an exception that tells me that the instance needs to have a primary key value before a many-to-many relationship can be used. But I need to first validate the object before saving it. I have tried this validation on the form (modelform) side (using the form's clean method), but the same thing happens there too.
How do I go about achieving this validation?
INHERENT_TYPE_CHOICES = ((1, 'Payable'), (2, 'Deductible'))
VALUE_TYPE_CHOICES = ((1, 'Amount'), (2, 'Percentage'))
class Payable(models.Model):
name = models.CharField()
short_name = models.CharField()
inherent_type = models.PositiveSmallIntegerField(choices=INHERENT_TYPE_CHOICES)
value = models.DecimalField(max_digits=12,decimal_places=2)
value_type = models.PositiveSmallIntegerField(choices=VALUE_TYPE_CHOICES)
percentage_calculated_on = models.ManyToManyField('self', symmetrical=False)
def clean(self):
from django.core.exceptions import ValidationError
if self.value_type == 2 and not self.percentage_calculated_on:
raise ValidationError("If this is a percentage, please specify on what payables/deductibles this percentage should be calculated on.")
I tested out your code in one of my projects' admin app. I was able to perform the validation you required by using a custom ModelForm. See below.
# forms.py
class MyPayableForm(forms.ModelForm):
class Meta:
model = Payable
def clean(self):
super(MyPayableForm, self).clean() # Thanks, #chefsmart
value_type = self.cleaned_data.get('value_type', None)
percentage_calculated_on = self.cleaned_data.get(
'percentage_calculated_on', None)
if value_type == 2 and not percentage_calculated_on:
message = "Please specify on what payables/deductibles ..."
raise forms.ValidationError(message)
return self.cleaned_data
# admin.py
class PayableAdmin(admin.ModelAdmin):
form = MyPayableForm
admin.site.register(Payable, PayableAdmin)
The Admin app uses the SelectMultiple widget (rather than CheckboxSelectMultiple as you do) to represent many to many relationships. I believe this shouldn't matter though.

can't override default admin model form django

I need to add extra validation to my DateField in Admin to make sure the date given is in the future. I have no experience in such a thing, so here's what I've done.
1) I've created custom form field and added validation to it:
class PastDateField(forms.DateField):
def clean(self, value):
"""Validates if only date is in the past
"""
if not value:
raise forms.ValidationError('Plase enter the date')
if value > datetime.now():
raise forms.ValidationError('The date should be in the past, not in future')
return value
2) Then I've added custom model form:
class CustomNewsItemAdminForm(forms.ModelForm):
title = forms.CharField(max_length=100)
body = forms.CharField(widget=forms.Textarea)
date = PastDateField()
region = forms.ModelChoiceField(Region.objects)
3) And here's how I've registered admin:
class NewsItemAdmin(admin.ModelAdmin):
form = CustomNewsItemAdminForm
def queryset(self, request):
return NewsItem.objects.all()
admin.site.register(NewsItem, NewsItemAdmin)
The result of this is that my admin form
1) Shows field I haven't specified in custom admin form
2) Lacks JavaScript calendar for the datetime field
It's pretty obvious to me that I'm doing something wrong, but I've found no examples relevant to my needs as I am a noob. What is the better way to add custom validation to datetime field without messing things up?
EDIT: Thanks a lot to Brian Luft and Daniel Roseman for correct answers! To make this post helpful for someone facing the same problem here is the resulting code:
class CustomNewsItemAdminForm(forms.ModelForm):
class Meta:
model = NewsItem
def clean_date(self):
"""Validates if only date is in the past
"""
date = self.cleaned_data["date"]
if date is None:
raise forms.ValidationError('Plase enter the date')
if date > datetime.now().date():
raise forms.ValidationError('The date should be in the past, not in future')
return self.cleaned_data["date"]
class NewsItemAdmin(admin.ModelAdmin):
form = CustomNewsItemAdminForm
def queryset(self, request):
return NewsItem.objects.all()
admin.site.register(NewsItem, NewsItemAdmin)
Firstly, declaring fields explicitly on a ModelForm - whether in or out of the admin - does not mean that the other fields will not be displayed. You need to define the fields or exclude tuples in the form's inner Meta class. If the other fields are all the default, you can simply declare the one you are overriding.
Secondly, if you want your custom field to use the javascript, you'll need to use the right widget, which is django.contrib.admin.widgets.AdminDateWidget. However, there is a much easier way to do this, which is not define a custom field at all, but instead define a clean_date method on the form itself.