My problem is that when trying to run is_valid() on a big chunk of data in a POST-request, where the model has a foreign key, it will fetch the foreign key table for each incoming item it needs to validate.
This thread describes this as well but ended up finding no answer:
Django REST Framework Serialization POST is slow
This is what the debug toolbar shows:
My question is therefore, is there any way to run some kind of select_related on the validation? I've tried turning off validation but the toolbar still tells me that queries are being made.
These are my models:
class ActiveApartment(models.Model):
adress = models.CharField(default="", max_length=2000, primary_key=True)
company = models.ForeignKey(Company, on_delete=models.CASCADE)
class Company(models.Model):
name = models.CharField(blank=True, null=True, max_length=150)
These are my serializers:
I have tried not using the explicit PrimaryKeyRelatedField as well, having validators as [] doesn't seem to stop the validation either for some reason.
class ActiveApartmentSerializer(serializers.ModelSerializer):
company = serializers.PrimaryKeyRelatedField(queryset=Company.objects.all())
class Meta:
model = ActiveApartment
list_serializer_class = ActiveApartmentListSerializer
fields = '__all__'
extra_kwargs = {
'company': {'validators': []},
}
class ActiveApartmentListSerializer(serializers.ListSerializer):
def create(self, validated_data):
data = [ActiveApartment(**item) for item in validated_data]
# Ignore conflcits is the only "original" part of this create method
return ActiveApartment.objects.bulk_create(data, ignore_conflicts=True)
def update(self, instance, validated_data):
pass
This is my view:
def post(self, request, format=None):
# Example of incoming data in request.data
dummydata = [{"company": 12, "adress": "randomdata1"}, {"company": 12, "adress": "randomdata2"}]
serializer = ActiveApartmentSerializer(data=request.data, many=True)
# This will run a query to check the validity of their foreign keys for each item in dummydata
if new_apartment_serializer.is_valid():
print("valid")
Any help would be appreciated (I would prefer not to use viewsets)
Have you tried to define company as IntegerField in the serializer, pass in the view's context the company IDs and add a validation method in field level?
class ActiveApartmentSerializer(serializers.ModelSerializer):
company = serializers.IntegerField(required=True)
...
def __init__(self, *args, **kwargs):
self.company_ids = kwargs.pop('company_ids', None)
super().__init__(*args, **kwargs)
def validate_company(self, company):
if company not in self.company_ids:
raise serializers.ValidationError('...')
return company
The way I solved it was I took the direction athanasp pointed me in and tweaked it a bit as his solution was close but not working entirely.
I:
Created a simple nested serializer
Used the init-method to make the query
Each item checks the list of companies in its own validate method
class CitySerializer(serializers.ModelSerializer):
class Meta:
model = City
fields = ('name',)
class ListingSerializer(serializers.ModelSerializer):
# city = serializers.IntegerField(required=True, source="city_id") other alternative
city = CitySerializer()
def __init__(self, *args, **kwargs):
self.cities = City.objects.all()
super().__init__(*args, **kwargs)
def validate_city(self, value):
try:
city = next(item for item in self.cities if item.name == value['name'])
except StopIteration:
raise ValidationError('No city')
return city
And this is how the data looks like that should be added:
city":{"name":"stockhsdolm"}
Note that this method more or less works using the IntegerField() suggested by athan as well, the difference is I wanted to use a string when I post the item and not the primary key (e.g 1) which is used by default.
By using something like
city = serializers.IntegerField(required=True, source="city_id") works as well, just tweak the validate method, e.g fetching all the Id's from the model using values_list('id', flat=True) and then simply checking that list for each item.
Related
I'm developing a Web App for a uni project and it's about airports and carriers...
Problem
The problem is when I try to feed to the database I'm doing in a parallel way because in the json file I'm using as the reference there is a lot of entries about carriers and airports.
When you update(PUT and PATCH) one by one in a sequential way it is working as it should but when I try PUT/PATCH parallel requests the instance in the DB is being overwritten every time.
Each airport has a list of carriers and when I tried to add more carriers to that list via PUT or PATCH in a parallel way it overwrites the instance previous carriers list
My models are:
class Carrier(models.Model):
code = models.CharField(max_length=10, primary_key=True)
name = models.TextField()
def __str__(self):
return self.name
class Airport(models.Model):
code = models.CharField(max_length=10, primary_key=True)
name = models.TextField()
carriers = models.ManyToManyField(Carrier, related_name='airports')
def __str__(self):
return self.name
Serializers:
class AirportListSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='airport-detail')
class Meta:
model = models.Airport
fields = ('code', 'name', 'url')
class AirportDetailSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='airport-detail')
class Meta:
model = models.Airport
fields = ('code', 'name', 'url', 'carriers')
And I'm having problems with my update method I had to override it because for appending new data to the carriers array of an instance of an airport:
def update(self, request, *args, **kwargs):
instance = self.get_object()
serializer = serializers.AirportDetailSerializer(
instance=instance,
data=request.data,
context={'request': request}
)
if serializer.is_valid(raise_exception=True):
# Getting the user inputed carriers after it was validated by the serializer
carriers = set(dict(request.data)['carriers'])
# Adding new carriers to the current airport list of carriers without deleting the old ones
for carrier in serializer.validated_data['carriers']:
print(carrier)
carriers.add(carrier)
print('Carriers %s' % carriers)
# Saving alterations to the db
serializer.save(carriers=carriers)
# Overriding the original data for more features
data = serializer.data
# Creating the carrier links
data['carriers'] = ['http://%s/api/carriers/%s/' % (request.get_host(), carrier) for carrier in data['carriers']]
return Response(data)
Example of entry in DRF Browsable API:
Every time you are calling serializer.save(carriers=carriers), you are saving the Airport instance with only the carriers from your particular PUT/PATCH request...rather than adding carriers to your Airport instance.
You should look at the docs for writable nested serializers and use a separate CarrierSerializer.
On a related note, your update logic is better suited as part of the AirportSerializer, not your view. This makes it more reusable. That section of the DRF docs is the perfect example for you.
Something like this pseudo-code adapted from the Albums/Tracks example:
class AirportSerializer(serializers.ModelSerializer):
carriers = CarrierSerializer(many=True)
class Meta:
model = Airport
fields = ......
def update(self, instance, validated_data):
carriers_data = validated_data.pop('carriers')
for carrier in carriers_data:
Carrier.objects.update_or_create(airport=instance, defaults=carrier_data)
return instance
I'm using Django 2.x and Django REST Framework.
I have a model with contact as a foreign key
class AmountGiven(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
contact = models.ForeignKey(Contact, on_delete=models.PROTECT)
amount = models.FloatField(help_text='Amount given to the contact')
given_date = models.DateField(default=timezone.now)
created = models.DateTimeField(auto_now=True)
and serializer like
class AmountGivenSerializer(serializers.ModelSerializer):
mode_of_payment = serializers.PrimaryKeyRelatedField(queryset=ModeOfPayment.objects.all())
contact_detail = ContactSerializer(source='contact', read_only=True)
contact = serializers.PrimaryKeyRelatedField(queryset=Contact.objects.all())
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'contact_detail', 'amount', 'given_date', 'created'
)
contact field is required while creating a new record. But I do not want contact to be modified once it is created.
But when I send only amount with PUT method it says
{
"contact": [
"This field is required."
]
}
And when I use PATCH method, it works fine but if passing some other value for contact, it is updating contact as well.
I want to make contact field not-required while updating existing record. And even if it is passed, use the earlier one instead of setting the new data.
Trial 2
I tried overriding the contact field in the request to the previously stored value so that in case if changed contact is passed or no contact is passed, it will save earlier one.
So, in the viewset add the function
def update(self, request, *args, **kwargs):
obj = self.get_object()
request.data['contact'] = obj.contact_id
return super().update(request, *args, **kwargs)
But this is giving error as
This QueryDict instance is immutable
Use __init__ method of serializer to make it read when object is being updated:
class AmountGivenSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
"""If object is being updated don't allow contact to be changed."""
super().__init__(*args, **kwargs)
if self.instance is not None:
self.fields.get('parent').read_only = True
# self.fields.pop('parent') # or remove the field
mode_of_payment = serializers.PrimaryKeyRelatedField(queryset=ModeOfPayment.objects.all())
contact_detail = ContactSerializer(source='contact', read_only=True)
contact = serializers.PrimaryKeyRelatedField(queryset=Contact.objects.all())
class Meta:
model = AmountGiven
depth = 1
fields = (
'id', 'contact', 'contact_detail', 'amount', 'given_date', 'created'
)
Using self.context['view'].action is not recommended as it will not work when using the serializer out of DRF, eg. in normal Django views. It's best to use self.instance as it will work in every situation.
If your viewset is a ModelViewSet, you can overwrite the perform_update hook (because ModelViewSet inherits from GenericAPIView (take a look at "Save and deletion hooks"). You can access the old contact using the serializer's instance field:
class MyViewSet(viewsets.ModelViewSet):
# ... other stuff
def perform_update(self, serializer):
serializer.save(contact=serializer.instance.contact)
So you will have to provide a contact, but no matter which contact you provide, it will always use the old saved contact when updating.
I've surfed most of the afternoon and have been at this particular quandry for a while.
I am trying to figure out how to essentially present a foreign key as a dropdown choice if the user has driven that type of car. For example purposes and to keep this as easy as possible...
Let's say I have aCars, Manufacturers and a UserProfile model.
I have a model for Cars as so...
class Cars(models.Model):
car_name = models.CharField(max_length=80)
class = models.ForeignKey(Manufacturer,null=True,on_delete=models.DO_NOTHING,related_name='car_manufacturer')
I have a model for Manufacturers as so...
class Manufacturers(models.Model):
manu_name = models.CharField(max_length=80)
Then I have a UserProfile model....
class Userprofile(models.Model):
user = models.OneToOneField(User,on_delete=models.CASCADE)
user_name = models.CharField(max_length=80)
car_owned = models.ForeignKey(Car,null=True,on_delete=models.DO_NOTHING,related_name='car_owned')
All good so far...
I have a view where I am listing all of the Manufacturers and this works fine as well. It shows all of the manufacturers that I would expect in the form view below.
class ManufacturerForm(forms.Form):
dropdown = forms.ModelChoiceField(queryset=Manufacturer.objects.all())
def __init__(self, *args, **kwargs):
super(ManufacturerForm, self).__init__(*args, **kwargs)
self.fields['dropdown'].widget.attrs['class'] = 'choices1'
self.fields['dropdown'].empty_label = ''
I'm using the FORMVIEW below to display the form...
class ManufacturerView(LoginRequiredMixin,FormView):
form_class = ManufacturerForm
template_name = 'Directory/HTMLNAME.html'
def get_form_kwargs(self):
kwargs = super(ManufacturerView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
manufacturer = form.cleaned_data['dropdown']
return HttpResponseRedirect(reverse("NAME:manufacturer",kwargs={'pk':manufacturer.pk}))
This all works fine. However, I can't figure out how to limit the manufacturer dropdown to only the cars the user has driven. I'm trying to essentially limit the dropdown display to only the manufacturers that are pertinent to the cars the user has owned based on their profile. I've researched reverse look ups and have also tried something similar to what is outlined below to solve my problem...
class ManufacturerForm(forms.Form):
dropdown = forms.ModelChoiceField(queryset=Manufacturer.objects.filter(car_manufacturer=1)
def __init__(self, *args, **kwargs):
super(ManufacturerForm, self).__init__(*args, **kwargs)
self.fields['dropdown'].widget.attrs['class'] = 'choices1'
self.fields['dropdown'].empty_label = ''
But this obviously only gives me record 1 for the Manufacturer model. I am trying to figure out how to display only the records that are relevant to an individual user based on their car_owned data. I can list all of the manufacturers and then just display the records that are relevant in a ListView, but I am trying to limit the dropdown to only where there are relevant records in a ListView. Thanks in advance for any thoughts.
You missed just couple of points:
1) Pass UserProfile as kwargs['user']:
kwargs['user'] = UserProfile.objects.get(user=self.request.user)
2) Add user parameter in form's __init__ signature and override dropdown.queryset there:
class ManufacturerForm(forms.Form):
dropdown = forms.ModelChoiceField(queryset=Manufacturer.objects.all())
def __init__(self, user, *args, **kwargs):
super(ManufacturerForm, self).__init__(*args, **kwargs)
self.fields['dropdown'].widget.attrs['class'] = 'choices1'
self.fields['dropdown'].empty_label = ''
self.fields['dropdown'].queryset = Manufacturer.objects.filter(car_manufacturer__car_owned=user)
Also I would like to recommend to rewrite your Car - User relationship to ManyToMany. If I understand correctly your message, User can have multiple cars:
limit the dropdown display to only the manufacturers that are
pertinent to the cars the user has owned
Also if I understand correctly, you want to track cars that user used to have (but doesn't have anymore).
If you rewrite Car - User relationship, then you won't probably have any reason to keep UserProfile model only to hold additional username. If so, your models.py should look like this:
class Manufacturer(models.Model):
name = models.CharField(max_length=80)
class Car(models.Model):
name = models.CharField(max_length=80)
klass = models.ForeignKey(Manufacturer, null=True, on_delete=models.DO_NOTHING, related_name='car_manufacturer')
owners = models.ManyToManyField(User, through='Ownership')
class Ownership(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
car = models.ForeignKey(Car, on_delete=models.CASCADE)
active = models.BooleanField(default=True) # True if user owns the car at the moment
I think it all hinges on your order and relationships between models, perhaps try adding in a ManyToMany relation between Manufacturer and Car, so one manufacturer can make many cars:
class Manufacturer(models.Model):
name = models.CharField(max_length=80)
car = models.ManyToManyField(Car)
Then it may be a case of doing something such as:
qs = Manufacturer.objects.all(car=user.car_owned)
dropdown = forms.ModelChoiceField(qs)
And in your views.py file:
form = ManufacturerForm(request.POST, user=request.user)
(You may need to look up if the above is valid, as I'm not sure if Forms can have the request object passed in).
Disclaimer: I see you're using a class based view...so I may need to tweak the above.
TL;DR: DRF drops the inner serialized object when validating the outermost serializer.
I'm using django 2.0, django-rest-framework 3.7.7, python 3.
I want to build a REST endpoint that performs a search in the database, using some parameters received in a POST (I want to avoid GET calls, which could be cached). The parameters should act like ORs (that's why I set all fields as not required), and I'm solving that using django Q queries when extracting the queryset.
I have the following django models in app/models.py:
class Town(models.Model):
name = models.CharField(max_length=200)
province = models.CharField(max_length=2, blank=True, null=True)
zip = models.CharField(max_length=5)
country = models.CharField(max_length=100)
class Person(models.Model):
name = models.CharField(max_length=100)
birth_place = models.ForeignKey(Town, on_delete=models.SET_NULL,
null=True, blank=True,
related_name="birth_place_rev")
residence = models.ForeignKey(Town, on_delete=models.SET_NULL,
null=True, blank=True,
related_name="residence_rev")
And I wrote the following serializers in app/serializers.py:
class TownSerializer(serializers.ModelSerializer):
class Meta:
model = models.Town
fields = ("id", "name", "province", "zip", "country")
def __init__(self, *args, **kwargs):
super(TownSerializer, self).__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].required = False
class PersonSerializer(serializers.ModelSerializer):
birth_place = TownSerializer(read_only=True)
residence = TownSerializer(read_only=True)
class Meta:
model = models.Person
fields = ("id", "name", "birth_place", "residence")
def __init__(self, *args, **kwargs):
super(PersonSerializer, self).__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].required = False
Then I wrote a view to provide the REST interface, in api/views.py:
class PersonSearchList(views.APIView):
model_class = Person
serializer_class = PersonSerializer
permission_classes = (permissions.AllowAny,)
def post(self, request, format=None):
serializer = self.serializer_class(data=request.data)
print("initial_data", serializer.initial_data) ########
if serializer.is_valid():
self.data = serializer.validated_data
print(self.data) ########
queryset = self.get_queryset()
serialized_objects = self.serializer_class(queryset, many=True)
return Response(serialized_objects.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get_queryset(self, *args, **kwargs):
orig_queryset = self.model_class.objects.all()
query_payload = self.data
# .. perform filtering using the query_payload data.
return queryset
And when I try to perform a query using e.g. curl:
$ curl -s -X POST -H "Content-Type: application/json" --data '{"birth_place": {"name": "Berlin", "country": "Germany"}}' http://127.0.0.1:8000/persons/ |python -m json.tool
[]
even though a Person object with birth_place set accordingly has just been created.
The two print statements that I placed in the post method of the view return:
initial_data: {'birth_place': {'name': 'Berlin', 'country': 'Germany'}}
after is_valid: OrderedDict()
So it seems like DRF drops the nested relation when validates.
How should I specify to parse and validate also the nested relation? Any suggestion is appreciated.
PS: Am I forcing a wrong design by making the request with a POST? I thought that since a search is not idempotent, and it may contain sensitive data (name, surname, birth date, etc) of a person.
I need an action which is safe (a search doesn't change the data) but not idempotent (a search in two different times can be different).
Initially I started using a generics.ListAPIView, but list() works only with GET. If there's a way to make it accept POST requests it would work like a charm.
As #Jon Clements♦ mentioned in comments, this would solve your problem
class PersonSerializer(serializers.ModelSerializer):
birth_place = TownSerializer()
residence = TownSerializer()
class Meta:
model = Person
fields = ("id", "name", "birth_place", "residence")
def __init__(self, *args, **kwargs):
super(PersonSerializer, self).__init__(*args, **kwargs)
for field in self.fields:
self.fields[field].required = False
I'm trying to figure it out why when i submit my form, my tags are not saved in my db. Pretty new with the django-rest-framework and Django-taggit too, i think i'm doing something wrong :)
First, before making my API with the rest-framework, i was using a generic view (CreateView and UpdateView) to register/validate my event. It was working fine but i decided to go further and try to build an API since i'm using Angularjs now.
Now my model event is created but without my tag and i have some errors. I put some code and i'll describe my errors after.
events/models.py
class Event(models.Model):
[...]
title = models.CharField(max_length=245, blank=False)
description = models.TextField(max_length=750, null=True, blank=True)
start = models.DateTimeField()
end = models.DateTimeField()
created_at = models.DateTimeField(editable=False)
updated_at = models.DateTimeField(editable=False)
slug = AutoSlugField(populate_from='title', unique=True, editable=False)
expert = models.BooleanField(choices=MODE_EXPERT, default=0)
home = models.BooleanField(choices=HOME, default=0)
nb_participant = models.PositiveSmallIntegerField(default=1)
price = models.PositiveSmallIntegerField(default=0)
cancelled = models.BooleanField(default=0)
user = models.ForeignKey(User, editable=False, related_name='author')
address = models.ForeignKey('Address', editable=False, related_name='events')
participants = models.ManyToManyField(User, related_name='participants', blank=True, editable=False,
through='Participants')
theme_category = models.ForeignKey('EventThemeCategory', unique=True, editable=False)
tags = TaggableManager(blank=True)
class Meta:
db_table = 'event'
def save(self, *args, **kwargs):
if not self.pk:
self.created_at = timezone.now()
self.updated_at = timezone.now()
super(Event, self).save(*args, **kwargs)
[...]
i'm using the serializers.HyperlinkedModelSerializer.
api/serializer.py
from taggit.models import Tag
class TagListSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Tag
fields = ('url', 'id', 'name')
class EventSerializer(serializers.HyperlinkedModelSerializer):
address = AddressSerializer()
user = UserSerializer(required=False)
tags = TagListSerializer(blank=True)
class Meta:
model = Event
fields = ('url', 'id', 'title', 'description', 'start', 'end', 'created_at', 'updated_at', 'slug', 'expert','home', 'nb_participant', 'price', 'address', 'user', 'theme_category', 'tags')
depth = 1
api/views/tags_views.py
from rest_framework import generics
from api.serializers import TagListSerializer
from taggit.models import Tag
class TagsListAPIView(generics.ListCreateAPIView):
queryset = Tag.objects.all()
model = Tag
serializer_class = TagListSerializer
class TagsDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Tag.objects.all()
model = Tag
serializer_class = TagListSerializer
api/views/events_views.py
class EventListAPIView(generics.ListCreateAPIView):
queryset = Event.objects.all()
model = Event
serializer_class = EventSerializer
paginate_by = 100
def pre_save(self, obj):
"""
Set the object's owner, based on the incoming request.
"""
obj.user = self.request.user
return super(EventListAPIView, self).pre_save(obj)
api/urls.py
url(r'^events/(?P<slug>[0-9a-zA-Z_-]+)/$', EventDetailAPIView.as_view(), name='event-detail'),
So first when i call /api/events/name-of-my-event the API send me the good resource with my tags on it. The GET method is working fine.
I was thinking that rest-framework follow the query set. So if i can get the resource with with all my tags why when i use POST my tags are not register ?
Actually i have two problems with the POST method:
first one if i send a tag which i have already created, he send me an error saying that the tag must be unique. I understand that, i don't want to create a new one, i just want it to be linked with my object. I don't have this problem when i use the generic view (it's done by magic :) and all is working fine)
Secondly, when i try to create a new tag, my new event is saved but without my tags.
You can see the response received by angularjs for my tag... He send me the name of the tag but without id, url (hyperlinked). When i checked my db the tag has not been created.
I think i have to make a custom get_queryset(self) in my tags_views but i'm not sure.
I'll will continue to investigate. If someone have already to that and have some advise, i'll be very API. Thanks.
meet the same question. But I just want to save the tag list directly by TaggableManager (without TagListSerializer and TagsListAPIView). My solution is:
class MyModel(models.Model):
...
tags = TaggableManager(blank=True)
def get_tags_display(self):
return self.tags.values_list('name', flat=True)
class MyModelSerializer(serializers.HyperlinkedModelSerializer):
...
tags = serializers.Field(source='get_tags_display') # more about: http://www.django-rest-framework.org/api-guide/fields#generic-fields
...
class MyModelViewSet(viewsets.ModelViewSet):
...
def post_save(self, *args, **kwargs):
if 'tags' in self.request.DATA:
self.object.tags.set(*self.request.DATA['tags']) # type(self.object.tags) == <taggit.managers._TaggableManager>
return super(MyModelViewSet, self).post_save(*args, **kwargs)
The post data of tags data will be ['tagA', 'tagB',...], the TaggableManager will handle it. Thx.
For DRF>3.1, you just need to override create and update in your ModelSerializer class:
class StringListField(serializers.ListField): # get from http://www.django-rest-framework.org/api-guide/fields/#listfield
child = serializers.CharField()
def to_representation(self, data):
return ' '.join(data.values_list('name', flat=True)) # you change the representation style here.
class MyModelSerializer(serializers.ModelSerializer):
tags = StringListField()
class Meta:
model = models.MyModel
def create(self, validated_data):
tags = validated_data.pop('tags')
instance = super(MyModelSerializer, self).create(validated_data)
instance.tags.set(*tags)
return instance
def update(self, instance, validated_data):
# looks same as create method
I used to follow the following ways to serialize taggit objects but currently django-taggit provide a built in serializer https://github.com/jazzband/django-taggit/blob/master/taggit/serializers.py and it was vendor from the package I mentioned previously.
"""
Django-taggit serializer support
Originally vendored from https://github.com/glemmaPaul/django-taggit-serializer
"""
import json
# Third party
from django.utils.translation import gettext_lazy
from rest_framework import serializers
class TagList(list):
def __init__(self, *args, **kwargs):
pretty_print = kwargs.pop("pretty_print", True)
super().__init__(*args, **kwargs)
self.pretty_print = pretty_print
def __add__(self, rhs):
return TagList(super().__add__(rhs))
def __getitem__(self, item):
result = super().__getitem__(item)
try:
return TagList(result)
except TypeError:
return result
def __str__(self):
if self.pretty_print:
return json.dumps(self, sort_keys=True, indent=4, separators=(",", ": "))
else:
return json.dumps(self)
class TagListSerializerField(serializers.Field):
child = serializers.CharField()
default_error_messages = {
"not_a_list": gettext_lazy(
'Expected a list of items but got type "{input_type}".'
),
"invalid_json": gettext_lazy(
"Invalid json list. A tag list submitted in string"
" form must be valid json."
),
"not_a_str": gettext_lazy("All list items must be of string type."),
}
order_by = None
def __init__(self, **kwargs):
pretty_print = kwargs.pop("pretty_print", True)
style = kwargs.pop("style", {})
kwargs["style"] = {"base_template": "textarea.html"}
kwargs["style"].update(style)
super().__init__(**kwargs)
self.pretty_print = pretty_print
def to_internal_value(self, value):
if isinstance(value, str):
if not value:
value = "[]"
try:
value = json.loads(value)
except ValueError:
self.fail("invalid_json")
if not isinstance(value, list):
self.fail("not_a_list", input_type=type(value).__name__)
for s in value:
if not isinstance(s, str):
self.fail("not_a_str")
self.child.run_validation(s)
return value
def to_representation(self, value):
if not isinstance(value, TagList):
if not isinstance(value, list):
if self.order_by:
tags = value.all().order_by(*self.order_by)
else:
tags = value.all()
value = [tag.name for tag in tags]
value = TagList(value, pretty_print=self.pretty_print)
return value
class TaggitSerializer(serializers.Serializer):
def create(self, validated_data):
to_be_tagged, validated_data = self._pop_tags(validated_data)
tag_object = super().create(validated_data)
return self._save_tags(tag_object, to_be_tagged)
def update(self, instance, validated_data):
to_be_tagged, validated_data = self._pop_tags(validated_data)
tag_object = super().update(instance, validated_data)
return self._save_tags(tag_object, to_be_tagged)
def _save_tags(self, tag_object, tags):
for key in tags.keys():
tag_values = tags.get(key)
getattr(tag_object, key).set(tag_values)
return tag_object
def _pop_tags(self, validated_data):
to_be_tagged = {}
for key in self.fields.keys():
field = self.fields[key]
if isinstance(field, TagListSerializerField):
if key in validated_data:
to_be_tagged[key] = validated_data.pop(key)
return (to_be_tagged, validated_data)
http://blog.pedesen.de/2013/07/06/Using-django-rest-framework-with-tagged-items-django-taggit/
With the release of the Django Rest Framework 3.0, the code for the TagListSerializer has changed slightly. The serializers.WritableField was depreciated in favour for serializers.Field for the creation of custom serializer fields such as this. Below is the corrected code for Django Rest Framework 3.0.
class TagListSerializer(serializers.Field):
def to_internal_value(self, data):
if type(data) is not list:
raise ParseError("expected a list of data")
return data
def to_representation(self, obj):
if type(obj) is not list:
return [tag.name for tag in obj.all()]
return obj
I now use the bulit in taggit serializer which was taken from https://github.com/glemmaPaul/django-taggit-serializer library.
I had a bunch of errors but i found a way to resolve my problem. Maybe not the best as i'm pretty new with all of this but for now it works.
I'll try to describe all my errors maybe it'll help someone.
First my angularjs send a json which match exatly the queryset
So for example with my model events below, angularjs send to the API:
Now let's begin with all my errors:
"A tag with this name already exist"
When i re-use a tag i have this error. Don't know why because with a classic validation without the API, all is working fine.
With a new tag nothing is saved too.
When i try to use a new tag on my event event model nothing is saved on the database. Angularjs received a response with the tag name but with an id of null (see the pitcure on my original question)
"AttributeError: 'RelationsList' object has no attribute 'add'"
Now i'm trying to think that to register my tags i need to have an instance of event already created. Thanks to that i will be able to add my tag on it like it's describe in the doc.
apple.tags.add("red", "green", "fruit")
So i decided to add a post_save in my events_views.py:
class EventListAPIView(generics.ListCreateAPIView):
queryset = Event.objects.all()
model = Event
serializer_class = EventSerializer
paginate_by = 100
def pre_save(self, obj):
"""
Set the object's owner, based on the incoming request.
"""
obj.user = self.request.user
return super(EventListAPIView, self).pre_save(obj)
def post_save(self, obj, created=False):
print 'tags', self.request.DATA
obj.tags.add(self.request.DATA['tags'])
return super(EventListAPIView, self).post_save(obj)
But now as is said i have this error AttributeError: 'RelationsList' object has no attribute 'add'.
Actually, it's obvious since obj.tags is a list of object and not the TaggableManager anymore.
So i decided to start over and send my tags not in 'tags' but in another custom property 'tagged' to avoid conflit with the TaggableManager.
"TypeError: unhashable type: 'list'"
New error :) I found the solution with this django-taggit-unhashable-type-list
def post_save(self, obj, created=False):
map(obj.tags.add, self.request.DATA['tagged'])
return super(EventListAPIView, self).post_save(obj)
"TypeError: unhashable type: 'dict'"
Now, i figured it out that the tags i sent are not well formatted. I changed it (on the angularjs side) to send an array like this ['jazz','rock'] instead of [object, object]. Stupid mistake from a beginner.
Now the magic happen, response received by angularjs is good:
Sorry for my english. I know it may not be the best solution and i will try to update it when i'll find another solution.