Is it possible to specify in which order fields will appear in a serialised model?
To make sure there is no confusion, while searching answers for this I have found lots of suggestions for ordering objects in a list view but this is not what I am looking for.
I really mean for a given model, I'd like their fields, once serialized to appear in a specific order. I have a fairly complex serialized object containing a lot of nested serializers, which appear first. I'd prefer instead key identifying fields such as name and slug to show up first, for readability.
Apologies in advance if this question is a duplicate, but I didn't find any relevant responses.
Solution
Based on #Toni-Sredanović solution I have implemented the following solution
def promote_fields(model: models.Model, *fields):
promoted_fields = list(fields)
other_fields = [field.name for field in model._meta.fields if field.name not in promoted_fields]
return promoted_fields + other_fields
class MySerializer(serializers.ModelSerializer):
...
class Meta:
model = MyModel
fields = promote_fields(model, 'id', 'field1', 'field2')
For that you can specify which fields you want to show and their order in class Meta:
class Meta:
fields = ('id', 'name', 'slug', 'field_1', 'field_2', ..., )
Here is a full example:
class TeamWithGamesSerializer(serializers.ModelSerializer):
"""
Team ModelSerializer with home and away games.
Home and away games are nested lists serialized with GameWithTeamNamesSerializer.
League is object serialized with LeagueSerializer instead of pk integer.
Current players is a nested list serialized with PlayerSerializer.
"""
league = LeagueSerializer(many=False, read_only=True)
home_games = GameWithTeamNamesSerializer(many=True, read_only=True)
away_games = GameWithTeamNamesSerializer(many=True, read_only=True)
current_players = PlayerSerializer(many=True, read_only=True)
class Meta:
model = Team
fields = ('id', 'name', 'head_coach', 'league', 'current_players', 'home_games', 'away_games', 'gender')
And the result:
{
"id": 1,
"name": "Glendale Desert Dogs",
"head_coach": "Coach Desert Dog",
"league": {
"id": 1,
"name": "Test league 1"
},
"current_players": [
{
"id": "rodriem02",
"first_name": "Emanuel",
"last_name": "Rodriguez",
"current_team": 1
},
{
"id": "ruthba01",
"first_name": "Babe",
"last_name": "Ruth",
"current_team": 1
}
],
"home_games": [
{
"id": 6,
"team_home": {
"id": 1,
"name": "Glendale Desert Dogs"
},
"team_away": {
"id": 2,
"name": "Mesa Solar Sox"
},
"status": "canceled",
"date": "2019-10-01"
},
{
"id": 7,
"team_home": {
"id": 1,
"name": "Glendale Desert Dogs"
},
"team_away": {
"id": 2,
"name": "Mesa Solar Sox"
},
"status": "",
"date": "2019-10-04"
}
],
"away_games": [
{
"id": 3,
"team_home": {
"id": 2,
"name": "Mesa Solar Sox"
},
"team_away": {
"id": 1,
"name": "Glendale Desert Dogs"
},
"status": "canceled",
"date": "2019-10-02"
}
],
"gender": "M"
}
If you would just use fields = '__all__' default ordering would be used which is:
object id
fields specified in the serializer
fields specified in the model
Best i can think of right now regarding your comment about generating fields is getting the fields in model, not really sure how to access what you've defined in the serializer so you would still need to write that manually.
Here is how you could do it with my example (this would make the name and gender appear on top):
class Meta:
model = Team
fields = ('name', 'gender')\
+ tuple([field.name for field in model._meta.fields if field.name not in ('name', 'gender')])\
+ ('league', 'home_games', 'away_games', 'current_players')
Related
I am working on an API, with Django, Django Rest Framework, and trying to achieve these(ad described)
First Serializer
class DeviceConfigSerializer(serializers.ModelSerializer):
config = serializers.JSONField(initial={})
context = serializers.JSONField(initial={})
templates = FilterTemplatesByOrganization(many=True)
class Meta:
model = Config
fields = ['backend', 'status', 'templates', 'context', 'config']
extra_kwargs = {'status': {'read_only': True}}
Now I have two nested serializer containing the above serializer for the LIST and DETAIL endpoints:-
Second Serializer
class DeviceListSerializer(FilterSerializerByOrgManaged, serializers.ModelSerializer):
config = DeviceConfigSerializer(write_only=True, required=False)
class Meta(BaseMeta):
model = Device
fields = ['id','name','organization','mac_address','key','last_ip','management_ip',
'model', 'os', 'system', 'notes', 'config', 'created', 'modified',]
Third Serializer
class DeviceDetailSerializer(BaseSerializer):
config = DeviceConfigSerializer(allow_null=True)
class Meta(BaseMeta):
model = Device
fields = ['id','name','organization','mac_address','key','last_ip','management_ip',
'model','os','system','notes','config','created','modified',]
Now, I am using the same DeviceConfigSerializer serializer for List, and Detail endpoint, but for the list endpoint I have set the nested serializer as write_only=True, But What I am trying to do with the list endpoint that is DeviceListSerializer serilaizer is that out of all the fields from the DeviceConfigSerializer, I want the status, and backend fields to be both read & write and others fields as write_only.
Presently with this configuration I am getting the response from the DeviceListSerializer as this:-
{
"id": "12",
"name": "tests",
"organization": "-------",
"mac_address": "-------",
"key": "------",
"last_ip": null,
"management_ip": null,
"model": "",
"os": "",
"system": "",
"notes": "",
"created": "2021-04-26T10:41:25.399160+02:00",
"modified": "2021-04-26T10:41:25.399160+02:00"
}
What I am trying to achieve is:-
{
"id": "12",
"name": "tests",
"organization": "----",
"mac_address": "-----",
"key": "----",
"last_ip": null,
"management_ip": null,
"model": "",
"os": "",
"system": "",
"notes": "",
"config": {
"status": "...",
"backend": "...",
,}
"created": "2021-04-26T10:41:25.399160+02:00",
"modified": "2021-04-26T10:41:25.399160+02:00"
}
PS: I tried by introducing an extra serializer for this two fields and nest it to the DeviceListSerializer, but I don't want to introduce an extra serializer for this two fields, and looking forward if this could be achieved with the same nested serializer.
Every Device instance contains config.
In short:-
I am trying to use the same DeviceConfigSerializer, for both DeviceListSerilaizer, and DeviceDetailSerializer. But for the DeviceListSerializer I want the status, & backend field from the DeviceConfigSerializerto be both read & write, which is presenty set to write only for the DeviceListSerializer.
This is not possible, so I had to introduce two new serializers for my need.
PrimaryKeyRelatedField may be used to represent the target of the relationship using its primary key
For example, the following serializer:
class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']
Would serialize to a representation like this:
{
'album_name': 'Undun',
'artist': 'The Roots',
'tracks': [
89,
90,
91,
...
]
}
You can use this type of approach in your serializes. Reference https://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield
I'm trying to optimize the queries for my moderation system, build with Django and DRF.
I'm currently stuck with the duplicates retrieval: currently, I have something like
class AdminSerializer(ModelSerializer):
duplicates = SerializerMethodField()
def get_duplicates(self, item):
if item.allowed:
qs = []
else:
qs = Item.objects.filter(
allowed=True,
related_stuff__language=item.related_stuff.language
).annotate(
similarity=TrigramSimilarity('name', item.name)
).filter(similarity__gt=0.2).order_by('-similarity')[:10]
return AdminMinimalSerializer(qs, many=True).data
which works fine, but does at least one additional query for each item to display. In addition, if there are duplicates, I'll do additional queries to fill the AdminMinimalSerializer, which contains fields and related objects of the duplicated item. I can probably reduce the overhead by using a prefetch_related inside the serializer, but that doesn't prevent me from making several queries per item (assuming I have only one related item to prefetch in AdminMinimalSerializer, I'd still have ~2N + 1 queries: 1 for the items, N for the duplicates, N for the related items of the duplicates).
I've already looked at Subquery, but I can't retrieve an object, only an id, and this is not enough in my case. I tried to use it in both a Prefetch object and a .annotate.
I also tried something like Item.filter(allowed=False).prefetch(Prefetch("related_stuff__language__related_stuff_set__items", queryset=Items.filter..., to_attr="duplicates")), but the duplicates property is added to "related_stuff__language__related_stuff_set", so I can't really use it...
I'll welcome any idea ;)
Edit: the real code lives here. Toy example below:
# models.py
from django.db.models import Model, CharField, ForeignKey, CASCADE, BooleanField
class Book(Model):
title = CharField(max_length=250)
serie = ForeignKey(Serie, on_delete=CASCADE, related_name="books")
allowed = BooleanField(default=False)
class Serie(Model):
title = CharField(max_length=250)
language = ForeignKey(Language, on_delete=CASCADE, related_name="series")
class Language(Model):
name = CharField(max_length=100)
# serializers.py
from django.contrib.postgres.search import TrigramSimilarity
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from .models import Book, Language, Serie
class BookAdminSerializer(ModelSerializer):
class Meta:
model = Book
fields = ("id", "title", "serie", "duplicates", )
serie = SerieAdminAuxSerializer()
duplicates = SerializerMethodField()
def get_duplicates(self, book):
"""Retrieve duplicates for book"""
if book.allowed:
qs = []
else:
qs = (
Book.objects.filter(
allowed=True, serie__language=book.serie.language)
.annotate(similarity=TrigramSimilarity("title", book.title))
.filter(similarity__gt=0.2)
.order_by("-similarity")[:10]
)
return BookAdminMinimalSerializer(qs, many=True).data
class BookAdminMinimalSerializer(ModelSerializer):
class Meta:
model = Book
fields = ("id", "title", "serie")
serie = SerieAdminAuxSerializer()
class SerieAdminAuxSerializer(ModelSerializer):
class Meta:
model = Serie
fields = ("id", "language", "title")
language = LanguageSerializer()
class LanguageSerializer(ModelSerializer):
class Meta:
model = Language
fields = ('id', 'name')
I'm trying to find a way to prefetch related objects and duplicates so that I can get rid of the get_duplicates method in BookSerializer, with the N+1 queries it causes, and have only a duplicates field in my BookSerializer.
Regarding data, here would be an expected output:
[
{
"id": 2,
"title": "test2",
"serie": {
"id": 2,
"language": {
"id": 1,
"name": "English"
},
"title": "series title"
},
"duplicates": [
{
"id": 1,
"title": "test",
"serie": {
"id": 1,
"language": {
"id": 1,
"name": "English"
},
"title": "first series title"
}
}
]
},
{
"id": 3,
"title": "random",
"serie": {
"id": 3,
"language": {
"id": 1,
"name": "English"
},
"title": "random series title"
},
"duplicates": []
}
]
Suppose for below ModelSerializer class
class UserSongSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField()
song_likes = serializers.ReadOnlyField() # This is model's property field
song_shares = serializers.ReadOnlyField()
song_plays = serializers.ReadOnlyField()
song_price = serializers.ReadOnlyField()
genre = GenreSerializer(many=True,required=False,context={'key':5})
language = LanguageSerializer(many=True, required=False)
Passing specific context kwarg like below
genre = GenreSerializer(many=True,required=False,context={'fields':['name']})
Since I want to retrieve only name field in Genre model class in some specific cases, I overrided GenreSerializer class's get_fields_name method so that I can mention specific fields only when required via context
class GenreSerializer(serializers.ModelSerializer):
def get_field_names(self, *args, **kwargs):
"""
Overriding ModelSerializer get_field_names method for getting only specific fields in serializer when mentioned in SerializerClass arguments
"""
field_names = self.context.get('fields', None)
if field_names:
return field_names
return super(GenreSerializer, self).get_field_names(*args, **kwargs)
class Meta:
model = Genre
fields = '__all__'
However, I am unable to get any 'fields' (getting None) key inside overrided get_fields_name method. I know of other ways as well like using StringRelatedField but that would change the output representation to
"genre":[
"Pop",
"Rock"
]
Whereas, I want to stick to my original representation
"genre": [
{
"id": 3,
"name": "Pop",
"created_date": "2018-09-05T17:05:59.705422+05:30",
"updated_date": "2018-09-20T14:43:02.062107+05:30",
"status": false
},
{
"id": 4,
"name": "Rock",
"created_date": "2018-09-05T17:06:06.889047+05:30",
"updated_date": "2018-09-17T16:45:22.684044+05:30",
"status": true
},
{
"id": 5,
"name": "Classical",
"created_date": "2018-09-05T17:06:14.216260+05:30",
"updated_date": "2018-09-05T17:06:14.275082+05:30",
"status": true
}
]
UPDATE - What I want is like this
"genre": [
{
"name": "Pop"
},
{
"name": "Rock"
},
{
"name": "Classical"
}
]
Contexts are meant to be set to the root serializer only.
Whenever UserSongSerializer will be instantiated it'll override the nested genre context.
If you are using generic views, you'll want to override the view's get_serializer_context and add your own context there. It's documented at the bottom of the methods section
PS: context are "shared" to serializers, fields, validators.
PPS: Don't alter context after it's been set you it's going to be sort of undefined behavior.
I'm having an issue in Django RestFramework in testing.
I have the following test:
def test_update_coupon(self):
response = self.make_coupon_request(
kind="put",
version="v1",
id=2,
data=self.valid_coupon_data
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
Where make_coupon_request has a return of:
return self.client.put(
reverse("coupon",
kwargs={
"version": kwargs["version"],
"pk": kwargs["id"]
}
),
data=json.dumps(kwargs["data"]),
content_type='application/json'
)
and valid_coupon_data where the problem is occurring is:
self.valid_coupon_data = {
"company": Company.objects.get(id=1),
"name": "Coupon Updated",
"added": "2018-11-30",
"code": "TESTCODE"
}
edit - An example Company that would be in this structure is:
{
"id": 1,
"name": "test",
"added": "2018-11-30"
},
So the total structure would look like:
self.valid_coupon_data = {
"company": {
"id": 1,
"name": "test",
"added": "2018-11-30"
},
"name": "Coupon Updated",
"added": "2018-11-30",
"code": "TESTCODE"
}
The error I am getting is in make_coupon_request that json.dumps cannot serialize valid_coupon_data:
"TypeError: Object of type Company is not JSON serializable"
I have a serializer for Company:
class CompanySerializer(serializers.ModelSerializer):
coupons = CouponSerializer(many=True, read_only=True)
class Meta:
model = Company
fields = ("name", "added", "coupons")
And for coupon:
class CouponSerializer(serializers.ModelSerializer):
class Meta:
model = Coupon
fields = ("company", "name", "added", "code")
Basically I know that somehow I need to use a serializer in order to make my test work, as json.dumps isn't accepting the raw Company object... but I am not sure how nor do I quite understand why.
Here are my 2 models for reference:
class Company(models.Model):
name = models.CharField(max_length=255, null=False)
class Meta:
verbose_name_plural = "Companies"
class Coupon(models.Model):
company = models.ForeignKey(
Company, on_delete=models.CASCADE, related_name='coupons')
name = models.CharField(max_length=100)
added = models.DateField(auto_now_add=True)
code = models.CharField(max_length=255, null=False)
The problem may lies in this statement,
self.valid_coupon_data = {
"company": Company.objects.get(id=1), # here
"name": "Coupon Updated",
"added": "2018-11-30",
"code": "TESTCODE"
}
It's clear that you are trying to send a json data and unfortunately it contains a non-serializable data of Company type.
According to the CouponSerializer you'd provided, the below json is enough to create/update the Coupon instance.
{
"company": 1, # provide only integer value
"name": "Coupon Updated",
"added": "2018-11-30",
"code": "TESTCODE"
}
The problem is that you are passing a python object via a json in your test, this section
self.valid_coupon_data = {
"company": Company.objects.get(id=1), # This is the error point! you are passing python object not a json.
"name": "Coupon Updated",
"added": "2018-11-30",
"code": "TESTCODE"
}
And passing just integer value like other answers will not work either. you should pass json of company object too. like this:
company = Company.objects.get(id=1)
self.valid_coupon_data = {
"company": {
"id": company.id,
"name": company.name,
"added": company.added
},
"name": "Coupon Updated",
"added": "2018-11-30",
"code": "TESTCODE"
}
Note
By the way if you are using django rest, then the way you are returning data in your views is not correct. use to_representation or serializer.data methods. it's not that well to use json.dump when you have a powerfull library like django-rest-framework. You can also return json as is with Response library of django-rest. like return Response(jsonData)
If you want more clear answer, please provide make_coupon_request method codes. to see what's going on in there.
So I have a Film model that holds a list of Actors model in a many to many field:
class Person(models.Model):
full = models.TextField()
short = models.TextField()
num = models.CharField(max_length=5)
class Film(models.Model):
name = models.TextField()
year = models.SmallIntegerField(blank=True)
actors = models.ManyToManyField('Person')
I'm trying to load some initial data from json fixtures, however the problem I have is loading the many to many actors field.
For example I get the error:
DeserializationError: [u"'Anna-Varney' value must be an integer."]
with these fixtures:
{
"pk": 1,
"model": "data.Film",
"fields": {
"actors": [
"Anna-Varney"
],
"name": "Like a Corpse Standing in Desperation (2005) (V)",
"year": "2005"
}
while my actors fixture looks like this:
{
"pk": 1,
"model": "data.Person",
"fields": {
"full": "Anna-Varney",
"num": "I",
"short": "Anna-Varney"
}
}
So the many to many fields must use the pk integer, but the problem is that the data isn't sorted and for a long list of actors I don't think its practical to manually look up the pk of each one. I've been looking for solutions and it seems I have to use natural keys, but I'm not exactly sure how to apply those for my models.
EDIT: I've changed my models to be:
class PersonManager(models.Manager):
def get_by_natural_key(self, full):
return self.get(full=full)
class Person(models.Model):
objects = PersonManager()
full = models.TextField()
short = models.TextField()
num = models.CharField(max_length=5)
def natural_key(self):
return self.full
But I'm still getting the same error
There's a problem with both the input and the natural_key method.
Documentation: Serializing Django objects - natural keys states:
A natural key is a tuple of values that can be used to uniquely
identify an object instance without using the primary key value.
The Person natural_key method should return a tuple
def natural_key(self):
return (self.full,)
The serialised input should also contain tuples/lists for the natural keys.
{
"pk": 1,
"model": "data.film",
"fields": {
"actors": [
[
"Matt Damon"
],
[
"Jodie Foster"
]
],
"name": "Elysium",
"year": 2013
}
}