From dd0bb97082326f880339c63b52124c569b91e738 Mon Sep 17 00:00:00 2001 From: Lucas Berg <55436804+BergLucas@users.noreply.github.com> Date: Wed, 16 Aug 2023 17:24:12 +0200 Subject: [PATCH 1/5] Add support for source with attributes in extra_kwargs --- rest_framework/serializers.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 77c181b6c..274017fdb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1117,9 +1117,36 @@ class ModelSerializer(Serializer): if source == '*': source = field_name + # Get the right model and info for source with attributes + source_attrs = source.split('.') + source_info = info + source_model = model + + if len(source_attrs) > 1: + attr_info = info + attr_model = model + + for attr in source_attrs[:-1]: + if attr not in attr_info.relations: + break + + attr_model = attr_info.relations[attr].related_model + attr_info = model_meta.get_field_info(attr_model) + else: + attr = source_attrs[-1] + if ( + attr in attr_info.fields_and_pk + or attr in attr_info.relations + or hasattr(attr_model, attr) + or attr == self.url_field_name + ): + source = attr + source_info = attr_info + source_model = attr_model + # Determine the serializer field class and keyword arguments. field_class, field_kwargs = self.build_field( - source, info, model, depth + source, source_info, source_model, depth ) # Include any kwargs defined in `Meta.extra_kwargs` From 705e277896d3dd1d642c7556b6621c38e62517f7 Mon Sep 17 00:00:00 2001 From: Lucas Berg <55436804+BergLucas@users.noreply.github.com> Date: Wed, 23 Aug 2023 14:39:28 +0200 Subject: [PATCH 2/5] Add test case for source with attributes in extra_kwargs --- tests/test_model_serializer.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 20d0319fc..366211cf7 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -735,6 +735,27 @@ class TestRelationalFieldMappings(TestCase): """) self.assertEqual(repr(TestSerializer()), expected) + def test_source_with_attributes(self): + class TestSerializer(serializers.ModelSerializer): + class Meta: + model = RelationalModel + fields = ('foreign_key_name', 'one_to_one_name') + extra_kwargs = { + 'foreign_key_name': { + 'source': 'foreign_key.name', + }, + 'one_to_one_name': { + 'source': 'one_to_one.name', + } + } + + expected = dedent(""" + TestSerializer(): + foreign_key_name = CharField(max_length=100, source='foreign_key.name') + one_to_one_name = CharField(max_length=100, source='one_to_one.name') + """) + self.assertEqual(repr(TestSerializer()), expected) + class DisplayValueTargetModel(models.Model): name = models.CharField(max_length=100) From c1ccc370971673e41d63be4d425c3f88b4cacdeb Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Wed, 13 Sep 2023 02:14:54 +0200 Subject: [PATCH 3/5] Add a real world example --- tests/test_model_serializer.py | 38 ++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 366211cf7..1fde17c98 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -13,6 +13,7 @@ import tempfile import django import pytest +from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured from django.core.serializers.json import DjangoJSONEncoder from django.core.validators import ( @@ -736,25 +737,40 @@ class TestRelationalFieldMappings(TestCase): self.assertEqual(repr(TestSerializer()), expected) def test_source_with_attributes(self): - class TestSerializer(serializers.ModelSerializer): + class UserProfile(models.Model): + age = models.IntegerField() + birthdate = models.DateField() + user = models.ForeignKey(User, on_delete=models.CASCADE) + + class UserProfileSerializer(serializers.ModelSerializer): class Meta: - model = RelationalModel - fields = ('foreign_key_name', 'one_to_one_name') + model = UserProfile + fields = ('username', 'email', 'first_name', 'last_name', 'age', 'birthdate') extra_kwargs = { - 'foreign_key_name': { - 'source': 'foreign_key.name', + 'username': { + 'source': 'user.username', }, - 'one_to_one_name': { - 'source': 'one_to_one.name', + 'email': { + 'source': 'user.email', + }, + 'first_name': { + 'source': 'user.first_name', + }, + 'last_name': { + 'source': 'user.last_name', } } expected = dedent(""" - TestSerializer(): - foreign_key_name = CharField(max_length=100, source='foreign_key.name') - one_to_one_name = CharField(max_length=100, source='one_to_one.name') + UserProfileSerializer(): + username = CharField(help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, source='user.username', validators=[, ]) + email = EmailField(allow_blank=True, label='Email address', max_length=254, required=False, source='user.email') + first_name = CharField(allow_blank=True, max_length=150, required=False, source='user.first_name') + last_name = CharField(allow_blank=True, max_length=150, required=False, source='user.last_name') + age = IntegerField() + birthdate = DateField() """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(repr(UserProfileSerializer()), expected) class DisplayValueTargetModel(models.Model): From a5b239e6403a5379779e920b3ebb2e9b559f254d Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Thu, 14 Sep 2023 18:26:04 +0200 Subject: [PATCH 4/5] Fix test for Django 3.0 --- tests/test_model_serializer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 1fde17c98..6178c85fe 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -761,11 +761,15 @@ class TestRelationalFieldMappings(TestCase): } } - expected = dedent(""" + # In Django 3.0, the maximum length of first_name is 30, whereas it is 150 + # in later versions, so we can't hard-code the value in the expected variable. + max_length = User.first_name.field.max_length + + expected = dedent(f""" UserProfileSerializer(): username = CharField(help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, source='user.username', validators=[, ]) email = EmailField(allow_blank=True, label='Email address', max_length=254, required=False, source='user.email') - first_name = CharField(allow_blank=True, max_length=150, required=False, source='user.first_name') + first_name = CharField(allow_blank=True, max_length={max_length}, required=False, source='user.first_name') last_name = CharField(allow_blank=True, max_length=150, required=False, source='user.last_name') age = IntegerField() birthdate = DateField() From 2fc08b2971e2e52c5315615c445c8be74fc37de7 Mon Sep 17 00:00:00 2001 From: Berg Lucas <55436804+BergLucas@users.noreply.github.com> Date: Fri, 6 Oct 2023 16:29:10 +0200 Subject: [PATCH 5/5] Add some documentation about source in extra_kwargs --- docs/api-guide/serializers.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 0f355c76d..9e2e53d97 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -580,6 +580,22 @@ This option is a dictionary, mapping field names to a dictionary of keyword argu Please keep in mind that, if the field has already been explicitly declared on the serializer class, then the `extra_kwargs` option will be ignored. +It is also possible to create new serializer fields from any related model fields using the `extra_kwargs` option. For example: + + class UserProfile(models.Model): + birthdate = models.DateField() + user = models.ForeignKey(User, on_delete=models.CASCADE) + + class UserProfileSerializer(serializers.ModelSerializer): + class Meta: + model = UserProfile + fields = ['date_of_birth', 'first_name', 'last_name'] + extra_kwargs = { + 'date_of_birth': {'source': 'birthdate'}, + 'first_name': {'source': 'user.first_name'}, + 'last_name': {'source': 'user.last_name'} + } + ## Relational fields When serializing model instances, there are a number of different ways you might choose to represent relationships. The default representation for `ModelSerializer` is to use the primary keys of the related instances.