The scenario is quite straight-forward:
I have a model with some fields that are required. Let's say one of them is a TextField which can't be blank.
I also have a ModelSerializer (Django Rest Framework) that represents that model.
When an empty string is used to set that field through the serializer the error returned comes from the model itself (This field can't be blank).
I would like to override the error messages only in the serializer level, without the need to explicitly re-specifying every field in the serializer (which I believe is against the DRY principle), having to write a validate_ method for each field and raise my own ValidationError or having to change the error messages in the Model level (because sometimes the context of the error message matters to my use-case and the error message should be given accordingly).
In other words, is there a way to override error messages in the serializer level as easy as it is for a ModelForm:
class MyModelForm(ModelForm):
class Meta:
model = MyModel
error_messages = {"field1": {"required": _("For some reason this is a custom error message overriding the model's default")}}
EDIT: I see that this question still receives some views, so it is important to note that there's another approach, much cleaner than the original answer I posted here.
You can just use the extra_kwargs attribute of the serializer's Meta class, like so:
class UserSerializer(ModelSerializer):
class Meta:
model = User
extra_kwargs = {"username": {"error_messages": {"required": "Give yourself a username"}}}
Original answer:
Using #mariodev 's answer I created a new class in my project that does that:
from rest_framework.serializers import ModelSerializer, ModelSerializerOptions
class CustomErrorMessagesModelSerializerOptions(ModelSerializerOptions):
"""
Meta class options for CustomErrorMessagesModelSerializerOptions
"""
def __init__(self, meta):
super(CustomErrorMessagesModelSerializerOptions, self).__init__(meta)
self.error_messages = getattr(meta, 'error_messages', {})
class CustomErrorMessagesModelSerializer(ModelSerializer):
_options_class = CustomErrorMessagesModelSerializerOptions
def __init__(self, *args, **kwargs):
super(CustomErrorMessagesModelSerializer, self).__init__(*args, **kwargs)
# Run through all error messages provided in the Meta class and update
for field_name, err_dict in self.opts.error_messages.iteritems():
self.fields[field_name].error_messages.update(err_dict)
The first one gives the possibility to add a new Meta class attribute to the serializer as with the ModelForm.
The second one inherits from ModelSerializer and uses #mariodev's technique to update the error messages.
All is left to do, is just inherit it, and do something like that:
class UserSerializer(CustomErrorMessagesModelSerializer):
class Meta:
model = User
error_messages = {"username": {"required": "Give yourself a username"}}
I tried to create a simple Serializer rather than a ModelSerializer. Probably because of that the accepted answer with extra_kwargs by Gabriel Amram didn't work for me. Another top answer by #mariodev did work but I was looking for a more elegant solution and found one. Turns out that the Field class accepts error_messages as a parameter, which is a dictionary that overrides the default error messages. Here is the reference to the docs. It's the same format as described in the accepted answers. Here is an example:
from rest_framework import serializers
class MySerializer(serializers.Serializer):
client_id = serializers.IntegerField(required=True, error_messages={'required': 'Custom error message'})
In your serializer:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
def __init__(self, *args, **kwargs):
super(UserSerializer, self).__init__(*args, **kwargs)
self.fields['username'].error_messages['required'] = u'My custom required msg'
Please notice that some error messages consist of %s placeholders like:
'invalid': _("'%s' value must be either True or False."),
for BooleanField.
So you need to go over default_error_messages part in each field type in the DRF's fields.py, to use it properly.
unique seemed to be ignored from error_messages, so I had to take a different approach.
email = serializers.EmailField(validators=[
UniqueValidator(
queryset=models.Client.objects.all(),
message="My custom error",
)]
)
It's simpler (yet less flexible, less reusable) than #gabriel-amram's, but far less hacky than #mariodev's.
Another approach for UniqueValidator (for using with ModelSerializer):
def __init__(self, *args, **kwargs):
super(UserSerializer, self).__init__(*args, **kwargs)
# Find UniqueValidator and set custom message
for validator in self.fields['email'].validators:
if isinstance(validator, validators.UniqueValidator):
validator.message = _('This email already exist on this site')
I just spent an hour ripping my hair out over this, so figured I'd post an update here in case anyone else finds it useful.
I'm using djangorestframework version 3.10.3, and for whatever reason, it seems that drf no longer uses the 'required' key in the error_messages dict to allow customization of an error message for a missing value. Instead it uses 'blank'.
class SampleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = SampleModel
fields = (
'description',
)
extra_kwargs = {
'description': {'error_messages': {'blank': "Please provide a description"}},
}
DRF3.0 expects us to explicitly define the validators for fields if we wish to override the default model validators. This can be done by passing extra_kwargs and explicitly defining the validators for whichever field
you seem necessary. Also you can even specify your own custom validator which can be reused again for different fields or even other serializers
http://www.django-rest-framework.org/api-guide/serializers/#validation
http://www.django-rest-framework.org/api-guide/validators/#validation-in-rest-framework
# my_app/validators.py
def validate_required(value):
# whatever validation logic you need
if value == '' or value is None:
raise serializers.ValidationError('This field is required.')
# my_app/serializers.py
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
extra_kwargs = {"field1": {"validators": [validators.validate_required,]}}
Just a note since I played with this for awhile, if you're using something like a URLField that just adds a URLValidator, it doesn't seem to use the error_messages, so I did something similar to #Hugo's answer:
class Meta:
extra_kwargs = {"url_field": {"validators": [validators.URLValidator(message="My error message")]}}
You can create separate function in serializers.py and call it from serializer class
def checkFields(fields):
for field in fields:
fields[field].error_messages['blank']=fields[field].error_messages['required'] = 'Please enter %s'%field
This is the code that inherits the model's error message.
There is also a module, so download it if you want.
If there's a problem, leave it in the comments.
https://pypi.org/project/django-rest-inherits-error-messages/#files
from rest_framework import serializers
from rest_framework.relations import HyperlinkedRelatedField
from rest_framework.utils.field_mapping import get_nested_relation_kwargs
class InheritsModelSerializer(serializers.ModelSerializer):
def build_field(self, field_name, info, model_class, nested_depth):
'''
inherits the error_messages of the model
'''
result: tuple = super().build_field(field_name, info, model_class, nested_depth)
field = model_class._meta.get_field(field_name)
error_messages = field.error_messages
if error_messages:
result[1]['error_messages'] = field.error_messages
return result
Related
Is it possible to change what fields are displayed in a ModelForm, dynamically?
I am trying to show only a small number of fields in a ModelForm when the user adds a new instance (of the Model) from the frontend (using an add form) but larger number of fields when the user edits an instance (using an edit form).
The Form class looks something like this:
class SchoolForm(ModelForm):
class Meta:
model = School
#want to change the fields below dynamically depending on whether its an edit form or add form on the frontend
fields = ['name', 'area', 'capacity', 'num_of_teachers']
widgets = {
'area': CheckboxSelectMultiple
}
labels = {
'name': "Name of the School",
'num_of_teachers': "Total number of teachers",
}
Trying to avoid having two separate classes for add and edit since that doesnt seem DRYish. I found some SO posts with the same question for the admin page where we could override get_form() function but that does not apply here.
Also, this answer suggests using different classes as the normal way and using dynamic forms as an alternative. Perhaps dynamics forms is the way forward here but not entirely sure (I also have overridden __init__() and save() methods on the SchoolForm class).
I'm not suere if is a correct way, but i use some method in class to add fields or delete-it. I used like this:
class someForm(forms.ModelForm):
class Meta:
model = Foo
exclude = {"fieldn0","fieldn1"}
def __init__(self, *args, **kwargs):
super(someForm, self).__init__(*args, **kwargs)
self.fields['foofield1'].widget.attrs.update({'class': 'form-control'})
if self.instance.yourMethod() == "FooReturn":
self.fields['city'].widget.attrs.update({'class': 'form-control'})
else:
if 'city' in self.fields: del self.fields['city']
Hope it helps.
I want to add a link to a single resource representation which is an URL to itself, self. Like (taken from documentation):
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing')
{
'album_name': 'The Eraser',
'artist': 'Thom Yorke',
'self': 'http://www.example.com/api/album/2/',
}
How should this be done?
If you inherit serializers.HyperlinkedModelSerializer all you need to do is pass a url field to fields. See the docs here:
http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/
Alright, this solved my problem but if you have a better solution please post an answer:
from django.urls import reverse
from rest_framework import serializers
self_url = serializers.SerializerMethodField('get_self')
def get_self(self, obj):
request = self.context['request']
return reverse('album-detail', kwargs={'id': obj.id}, request=request)
here is my solution,
in your view methods create serilizer object like this:
album = AlbumSerializer(data=data, {"request":request})
in your serilizer class override to_representation method (you can read about this method on DRF docs
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
def to_representation(self, obj):
data = super().to_representation(obj)
request = self.context["request"]
return data
According to this issue, you can just add 'url' in the list of fields.
Here is a little more context than you got in the other answers so far. The key is the context argument passed to the serializer constructor and the 'url' in fields.
http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/
In your viewset:
class AlbumViewSet(viewsets.ViewSet):
def list(self, request):
queryset = Album.objects.all()
serializer = AlbumSerializer(queryset, many=True,
context={'request': request})
return Response(serializer.data)
In the serializer:
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing', 'url')
As stated above the HyperlinkedModelSerializer will convert all your related fields and remove the ID of the resource as well. I use one of the following solutions, depending on the situation.
Solution 1: import api settings and add the url field:
For more details see URL_FIELD_NAME.
from rest_framework.settings import api_settings
class AlbumSerializer(SelfFieldMixin, serializers.ModelSerializer):
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing', api_settings.URL_FIELD_NAME)
Solution 2: a simple mixin, which only works if default fields are used:
class SelfFieldMixin:
"""
Adds the self link without converting all relations to HyperlinkedRelatedField
"""
def get_default_field_names(self, declared_fields, model_info):
"""
Return the default list of field names that will be used if the
`Meta.fields` option is not specified.
"""
default_fields = super().get_default_field_names(declared_fields, model_info)
return [self.url_field_name, *default_fields]
And it can be used like
class AlbumSerializer(SelfFieldMixin, serializers.ModelSerializer):
class Meta:
model = Album
fields = '__all__'
NOTE: It requires Python 3 due to the super() call, the a mixin must be placed before any of the serializer classes!
P.S.: To achieve the required response in the question one must also set the URL_FIELD_NAME to 'self'.
Edit: get_default_field_names must return a list object for Meta.exclude to work on ModelSerializers.
You can use the HyperlinkedIdentityField like so:
class ThingSerializer(ModelSerializer):
class Meta:
model = Thing
fields = ['self_link', ...]
self_link = HyperlinkedIdentityField(view_name='thing-detail')
You need to have your routes named appropriately but the Default routers do so automatically (documented here).
As others have pointed out the HyperlinkedModelSerializer also works. This is because it uses this field automatically. See here.
Given a serializer with a reference to a custom serializer:
class IndustryIdeaSerializer(serializers.ModelSerializer):
sub_industry = IndustrySerializer(many=False, read_only=True)
class Meta:
model = myModels.IdeaIndustry
fields = (
'id'
, 'sub_industry'
)
I am unable to save changes to this class when I post JSON like { sub_industry: 12 } or { sub_industry_id: 12 }
It does return the right JSON for displaying the data, and I wouldn't change it from that perspective. However changing it to:
class IndustryIdeaSerializer(serializers.ModelSerializer):
class Meta:
model = myModels.IdeaIndustry
fields = (
'id'
, 'sub_industry'
)
Gives me the save action (can persist with the simple JSON) I want BUT not the read action (doesn't return all the data associated with that foreign key)!
First am I missing something obvious? Is there a pattern to deal with behavior I am after - namely read and return the deep tree, but persist with just the Id's
This is for DRF 3.0. I just whipped this up this afternoon, I will follow up if I encounter any unforeseen problems (likewise, let me know if you spot anything wrong! I am fairly new to DRF)
class EnhancedPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
'''
This custom field extends the PrimaryKeyRelatedField
It overrides to_representation (which generates the data to be
serialized) to use a given serializer.
This allows other serializers to show nested data about a related
field, while still allowing the client to set relations by simply
passing an id.
To initialize, pass the queryset and serializer arguments.
The serializer argument should be a Serializer class.
If the serializer provides Meta.model (such as a ModelSerializer),
and you wish to use the queryset provided by that serializer, you may
omit the queryset argument.
e.g.
# without queryset
child_object = EnhancedPrimaryKeyRelatedField(
serializer=ChildObjectSerializer
)
# with queryset
child_object = EnhancedPrimaryKeyRelatedField(
queryset=models.ChildObject.objects.all(),
serializer=SomeSpecializedSerializer
)
'''
def __init__(self, *args, **kwargs):
assert 'serializer' in kwargs
self.serializer = kwargs['serializer']
del kwargs['serializer']
if 'queryset' not in kwargs:
# Catch any programmer errors
assert 'Meta' in self.serializer.__dict__
assert 'model' in self.serializer.Meta.__dict__
kwargs['queryset'] = self.serializer.Meta.model.objects.all()
super(serializers.PrimaryKeyRelatedField, self).__init__(*args, **kwargs)
def to_representation(self, data):
if hasattr(data.pk, 'all'): # are we dealing with a collection?
return self.serializer(data.pk.all(), many=True).data
elif hasattr(data, 'pk') and data.pk:
return self.serializer(self.queryset.get(pk=data.pk)).data
else:
return data.pk
There's nothing built in that handles this explicitly, but it's now come up a couple of times recently (e.g. here so perhaps we need to make it easier.
The work-around is to subclass PrimaryKeyRelatedField, which will handle setting the relation and override to_native to provide the full serialisation you're after.
I hope that helps.
This seems like a bug but I just want to make sure I'm consuming the API properly.
It seems that support for django's modelform isn't supported on neo4django. Here's what I have:
Simple class:
from neo4django.db import models
class Person(models.NodeModel):
name = models.StringProperty()
The modelform:
class PersonForm(forms.ModelForm):
class Meta:
model = Person
Will trigger exception:
'super' object has no attribute 'editable'
I posted details as an issue:
https://github.com/scholrly/neo4django/issues/135
Because when Django goes to lookup field information using the model's _meta information, it finds a BoundProperty instead of a StringProperty or Property (which has a member called 'editable', but BoundProperty doesn't).
Is there a workaround, or is this an actual bug? Any ideas on how to fix the bug? I'm not familiar with the library codebase.
Thanks!
Below is a reasonable (and quick) workaround for anyone using neo4j with Django.
This solution requires that field names on the form have the exact same name as the attributes of the model.
Inherit the form from this class and set the model under the form class Meta class:
class NeoModelForm(forms.Form):
def __init__(self, *args, **kwargs):
super(NeoModelForm, self).__init__(*args, **kwargs)
self._meta = getattr(self, 'Meta', None)
if not self._meta:
raise Exception('Missing Meta class on %s' % str(self.__class__.__name__))
if not hasattr(self._meta, 'model'):
raise Exception('Missing model on Meta class of %s' % str(self.__class__.__name__))
def save(self, commit=True):
if not self.is_valid():
raise Exception('Failed to validate')
instance = self._meta.model(**self.cleaned_data)
if commit:
instance.save()
return instance
Now you can create a form class like this:
class PersonForm(NeoModelForm):
name = forms.CharField(widget=forms.TextInput())
class Meta:
model = Person
And still be able to save a model instance from a valid form:
form = formclass(request.POST)
if form.is_valid():
obj = form.save()
Plus the commit argument will give you the same solution as django's modelform class- but I didn't bother to implement to save_m2m functionality (which doesn't seem relevant for neo4j as a backend).
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'