DRY way to rename ModelSerializer fields without duplicating model/field definitions? - django

Working with a legacy database with an awful schema and want to rename several ModelSerializer fields without having to redefine fields already defined on the model.
Here's an example model:
class LegacyItem(models.Model):
# Note: ignore things that feel "wrong" here (just an example)
legacyitem_id = models.IntegerField(primary_key=True)
legacyitem_notes = models.CharField(max_length=4000, blank=True, null=True)
directory_id = models.ForeignKey('Directory', models.DO_NOTHING)
created_by_directory_id = models.ForeignKey('Directory', models.DO_NOTHING)
created_date = models.DateField(auto_now_add=True)
modified_by_directory_id = models.ForeignKey('Directory', models.DO_NOTHING)
modified_date = models.DateField(auto_now=True)
Here's a working serializer:
class LegacyItemSerializer(serializers.ModelSerializer):
class Meta:
fields = '__all__'
model = LegacyItem
read_only_fields = [
'created_date',
'modified_date',
]
The goal is to bulk rename model fields for the API so we can abstract away the awful schema. Here's a working serializer showing the sort of renaming we want to do:
class LegacyItemSerializer(serializers.ModelSerializer):
created_by = serializers.PrimaryKeyRelatedField(
read_only=True,
source='created_by_directory_id',
)
customer = serializers.PrimaryKeyRelatedField(
queryset=Directory.objects.all(),
source='directory_id',
)
id = serializers.PrimaryKeyRelatedField(
read_only=True,
source='legacyitem_id'
)
modified_by = serializers.PrimaryKeyRelatedField(
read_only=True,
source='modified_by_directory_id',
)
notes = serializers.DateField(source='legacyitem_notes')
class Meta:
fields = [
'id',
'customer',
'notes',
'created_by',
'created_date',
'modified_by',
'modified_date',
]
model = LegacyItem
read_only_fields = [
'created_date',
'modified_date',
]
All we want to do is rename the fields. We don't want to change the validation and would rather keep most of that validation on the model and the model's fields and not also put it into the serializer. We will have to do this sort of thing for a lot of models and it's very tedious and bad practice (IMO) to be duplicating model validation on the serializer just to rename a field.
Is there a DRY way to do this? Maybe something we can put into LegacyItemSerializer.Meta or some method we can override? When working with Django forms, for example, you could do this easily by tweaking labels/widgets in the ModelForm.Meta. You would never have to redefine ModelForm fields/validation.
In a perfect world, maybe something like:
class LegacyItemSerializer(serializers.ModelSerializer):
class Meta:
model = LegacyItem
model_field_args = {
'legacyitem_id': {'name': 'id'},
'directory_id': {'name': 'customer'},
'legacyitem_notes': {'name': 'notes'},
'created_by_directory_id': {'name': 'created_by'},
'modified_by_directory_id': {'name': 'modified_by'},
}

Related

Adding a custom, non-model attribute to query set in Django?

Newbie to DRF and have a model called posts. And another called user. The post object looks as follows:
class Post(models.Model):
"""
Post model
"""
title = models.CharField(max_length=250)
body = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='forum_posts')
parent_post = models.ForeignKey('self',
on_delete=models.CASCADE,
blank=True,
null=True)
time_stamp = models.DateTimeField(default=timezone.now)
objects = models.Manager()
The serializer for this model is:
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = models.Post
fields = ('id', 'title', 'body', 'parent_post', 'author', 'time_stamp')
extra_kwargs = {'id': {'read_only': True},
'author': {'read_only': True}}
When returning data for this model, I want to add an extra attribute to each object within the query set called "author_username". The username should be the username belonging to the post's author id. I also want to do this without modifying the model to add another attribute such as "author_username" since this'll be redundant (already have an FK for author). So, ideally, the json for an object would look like:
'post_id': 1
'post_title': 'Example post'
'post_body': 'Example post'
'author_id': 1
'parent_post_id': null
'time_stamp': '2022'
'author_username': 'testUser'
How can I go about doing this?
Here's my view:
class PostList(generics.ListCreateAPIView):
permission_classes = [IsAuthenticatedOrReadOnly]
queryset = models.Post.objects.all()
serializer_class = serializers.PostSerializer
The source argument can be passed to a serializer field to access an attribute from a related model
class PostSerializer(serializers.ModelSerializer):
author_username = serializers.CharField(source="author.username", read_only=True)
class Meta:
model = models.Post
...
You should add a select_related call to your view's queryset
class PostList(generics.ListCreateAPIView):
...
queryset = models.Post.objects.select_related('author')
...

Set CharField to RTL in Django Admin site

The frontend of my Django site is in Persian language which is RTL and everything is ok except that the CharField model fields are in LTR direction when edited in the Admin site.
Here's my model:
class Post(models.Model):
STATUS_CHOICES = (('draft', 'Draft'), ('published', 'Published'))
title = models.CharField(max_length=100)
slug = models.SlugField(max_length=100, allow_unicode=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
lead = RichTextField()
body = RichTextUploadingField()
created_on = models.DateTimeField(auto_now_add=True)
published_on = models.DateTimeField(default=timezone.now)
updated_on = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
is_featured = models.BooleanField(default=False, verbose_name='Featured Post')
objects = models.Manager()
published = PublishedManager()
featured = FeaturedManager()
class Meta:
ordering = ('-published_on',)
def __str__(self):
return self.title
I know I can set the site's language to Persian and solve this issue but I don't want to because the Persian translation of Django is dull.
Another solution is to use one of available Rich Text editors (tinymce or ckeditor) but those are overkill for a CharField field.
I also tried custom admin form like this:
class PostAdminForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'slug', 'author', 'lead', 'body', 'status', 'is_featured']
widgets = {'title': forms.TextInput(attrs={'dir': 'rtl'})}
#admin.register(Post, PostAdminForm)
class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'slug', 'created_on', 'published_on', 'status', 'is_featured')
list_filter = ('status', 'created_on', 'published_on', 'is_featured')
search_fields = ('title', 'body')
prepopulated_fields = {'slug': ('title',)}
raw_id_fields = ('author',)
date_hierarchy = 'published_on'
ordering = ('status', 'created_on', 'published_on')
But it gives me this error:
AttributeError: 'ModelFormOptions' object has no attribute 'abstract'
In your admin.py file for your app, you can create a custom form for your model. I don't know your model so I will use general names for this as an example:
from django.contrib import admin
from app_name.models import *
from django import forms
class CustomAdminForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ['mycharfield']
widgets = {'mycharfield':forms.TextInput(attrs={'dir':'rtl'})}
admin.site.register(MyModel,CustomAdminForm)
This should make the mycharfield text input render as being RTL on the admin page for the MyModel form. The line widgets = {'mycharfield':forms.TextInput(attrs={'dir':'rtl'})} will change the text input widget's dir attribute to the rtl value. If you have more than one CharField in your model and want RTL for all of them simply add each field to the fields attribute in the form and do the same thing with the widget attribute for each field.
As the answer provided by Nathan was partially correct, I did my own research and found that the correct way is this:
class CustomPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'slug', 'lead', 'body']
widgets = {'title': forms.TextInput(attrs={'dir': 'rtl', 'class': 'vTextField'})}
and then:
#admin.register(Post)
class PostAdmin(admin.ModelAdmin):
form = CustomPostForm
What surprises me is that the above code removed vTextField class from the input field so I had to add it again.

Django DRF: read_only_fields not working properly

I have the following models
class Breed(models.Model)::
name = models.CharField(max_length=200)
class Pet(models.Model):
owner = models.ForeignKey(
"User",
on_delete=models.CASCADE,
)
name = models.CharField(max_length=200)
breed = models.ForeignKey(
"Breed",
on_delete=models.CASCADE,
)
I am trying to add few fileds for representation purpose. I dont want them to be included while create or update
class PetSerializer(serializers.ModelSerializer):
owner_email = serializers.CharField(source='owner.email')
breed_name = serializers.CharField(source='breed.str')
class Meta:
model = Pet
fields = "__all__"
read_only_fields = ["breed_name","owner_email"]
This is not working. I see the owner_email and breed_name in the HTMLform (the DRF api page)
Where as
class PetSerializer(serializers.ModelSerializer):
owner_email = serializers.CharField(source='owner.email',read_only=True)
breed_name = serializers.CharField(source='breed.str',read_only=True)
class Meta:
model = Pet
fields = "__all__"
This is working. I dont see them in the HTMLform
Also i observed, if i use a model field directly in read_only_fields then it works.
class PetSerializer(serializers.ModelSerializer):
class Meta:
model = Pet
fields = "__all__"
read_only_fields = ["name"]
This will make all name not shown in update or create
Why read_only_fields is not working properly
This is very interesting. I looked into the code and found the root cause, specifically this lines in the implementation for ModelSerializer:
for field_name in field_names:
# If the field is explicitly declared on the class then use that.
if field_name in declared_fields:
fields[field_name] = declared_fields[field_name]
continue
....
Here was my script for the investigation
from django.db import models
from rest_framework import serializers
class MyModel(models.Model):
xero_contact_id = models.UUIDField(unique=True)
name = models.CharField(max_length=255, default="Some name")
class Meta:
db_table = "my_model"
class MySerializer(serializers.ModelSerializer):
owner_email = serializers.CharField()
breed_name = serializers.CharField(max_length=255)
class Meta:
model = MyModel
fields = '__all__'
read_only_fields = ["breed_name", "owner_email", "xero_contact_id"]
serializer = MySerializer()
print(repr(serializer))
I added some prints and here is what I saw:
>>> print(repr(serializer))
field_names ['id', 'owner_email', 'breed_name', 'xero_contact_id', 'name']
declared_fields OrderedDict([('owner_email', CharField()), ('breed_name', CharField(max_length=255))])
extra_kwargs {'breed_name': {'read_only': True}, 'owner_email': {'read_only': True}, 'xero_contact_id': {'read_only': True}}
MySerializer():
id = IntegerField(label='ID', read_only=True)
owner_email = CharField()
breed_name = CharField(max_length=255)
xero_contact_id = UUIDField(read_only=True)
name = CharField(max_length=255, required=False)
As you can see, the read_only argument is in the extra_kwargs. The problem is that for all the fields that are only declared in the ModelSerializer itself (visible from declared_fields) and not in the model class, they don't read from the extra_kwargs, they just read what was set in the field itself as visible in the code snippet above fields[field_name] = declared_fields[field_name] then performs a continue. Thus, the option for read_only was ignored.
I fixed it by modifying the implementation of ModelSerializer to also consider the extra_kwargs even for non-model fields
for field_name in field_names:
# If the field is explicitly declared on the class then use that.
if field_name in declared_fields:
field_class = type(declared_fields[field_name])
declared_field_args = declared_fields[field_name].__dict__['_args']
declared_field_kwargs = declared_fields[field_name].__dict__['_kwargs']
extra_field_kwargs = extra_kwargs.get(field_name, {})
# Old implementation doesn't take into account the extra_kwargs
# fields[field_name] = declared_fields[field_name]
# New implementation takes into account the extra_kwargs
fields[field_name] = field_class(*declared_field_args, **declared_field_kwargs, **extra_field_kwargs)
continue
....
Now, read_only was correctly set to the target fields, including non-model fields:
>>> print(repr(serializer))
field_names ['id', 'owner_email', 'breed_name', 'xero_contact_id', 'name']
declared_fields OrderedDict([('owner_email', CharField()), ('breed_name', CharField(max_length=255))])
extra_kwargs {'breed_name': {'read_only': True}, 'owner_email': {'read_only': True}, 'xero_contact_id': {'read_only': True}}
MySerializer():
id = IntegerField(label='ID', read_only=True)
owner_email = CharField(read_only=True)
breed_name = CharField(max_length=255, read_only=True)
xero_contact_id = UUIDField(read_only=True)
name = CharField(max_length=255, required=False)
This doesn't seem to be in the DRF docs. Sounds like a feature we can request to DRF :) So the solution for the meantime is as what #JPG pointed out, use read_only=True explicitly in the extra non-model fields.
The read_only_fields meta option will work for the fields which are not explicitly defined in the Serializer.
So, in your case, you need to add the read_only=True to those explicitly defined fields, as
class PetSerializer(serializers.ModelSerializer):
owner_email = serializers.CharField(source='owner.email', read_only=True)
breed_name = serializers.CharField(source='breed.str', read_only=True)
class Meta:
model = Pet
fields = "__all__"

Serializing a field of the related model in a model DRF

I have a serializer as follows:
class ImageSerializer(serializers.HyperlinkedModelSerializer):
prop_post = serializers.SlugRelatedField(queryset=PropertyPost.objects.all(),
slug_field='pk')
class Meta:
model = Image
fields = (
'url',
'photo',
'prop_post',
)
This works Ok. Now my PropertyPost has a 'owner' field that I need to include in my ImageSerializer.
I was wondering how could I do that. I was thinking it might be like
fields = (
'url',
'photo',
'prop_post',
'prop_post__owner'
)
but it didn't work. Any help is appreciated.
here is my model:
class PropertyPost(models.Model):
owner = models.ForeignKey(
get_user_model(),
related_name='posts4thisowner',
on_delete=models.CASCADE)
class CustomUser(AbstractUser):
pass
class Image(models.Model):
prop_post = models.ForeignKey(
PropertyPost,
related_name='images4thisproperty',
on_delete=models.CASCADE)
photo = models.ImageField(upload_to=upload_update_image, null=True, blank=True)
Easier to use 'source' and you can use your user serializer to populate the owner fields.
Example:
class ImageSerializer(serializers.HyperlinkedModelSerializer):
prop_post = serializers.SlugRelatedField(queryset=PropertyPost.objects.all(),
slug_field='pk')
owner = UserSerializer(source="prop_post.owner")
class Meta:
model = Image
fields = (
'url',
'photo',
'prop_post',
'owner',
)
Use SerializerMethodField() that will achieve the task.
Since you haven't posted your models and how it's related.
I gave you the normal idea of how to achieve this.
It will be better if you can add the models as well.
I will be able to update the answer accordingly.
from rest_framework.serializers import SerializerMethodField
class ImageSerializer(serializers.HyperlinkedModelSerializer):
prop_post = serializers.SlugRelatedField(queryset=PropertyPost.objects.all(),
slug_field='pk')
prop_post_title = SerializerMethodField()
class Meta:
model = Image
fields = [
'url',
'photo',
'prop_post',
'prop_post_title',
]
def get_prop_post_title(self, obj):
try:
return obj.prop_post.title
except:
return None

DRF nested serializers - Filtering data on child serializers

I am trying to use nested serializer. How do I use the root serializer to filter data on the grandchild serializer?
School and Program have a many to many relationship So that any school can subscribe to any program. Each school has classes and those classes are part of a program, that's why PClass has foreign keys to both School and program.
When I call my api .../api/school/1 I want to get all the programs that school subscribes to and which classes are available in each program (in that school)
class School(TimeStampedModel, SoftDeletableModel):
name = models.CharField(max_length=40)
slug = models.SlugField(max_length=40, default='', blank=True)
class Program(TimeStampedModel, SoftDeletableModel):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50,default='',blank=True, unique=True)
description = models.CharField(max_length=100, blank=True)
school = models.ForeignKey(School, blank=True, null=True, related_name="programs")
class PClass(TimeStampedModel, SoftDeletableModel):
name = models.CharField(max_length=50)
slug = models.SlugField(max_length=50,default='',blank=True)
description = models.CharField(max_length=100)
program = models.ForeignKey(Program, related_name="classes")
school = models.ForeignKey(School, related_name="classes")
and the following serializers:
class SchoolSerializer( serializers.ModelSerializer):
programs = ProgramSerializer(source='get_programas',many=True,read_only=True)
class Meta:
model = School
fields = '__all__'
lookup_field = 'slug'
extra_kwargs = {
'url': {'lookup_field': 'slug'}
}
class PClassSerializer(serializers.ModelSerializer):
class Meta:
model = Class
fields = ('name','slug')
class ProgramSerializer(serializers.ModelSerializer):
school = serializers.SlugRelatedField(queryset=School.objects.all(),
slug_field='name',
required=False)
classes = PClassSerializer(many=True,read_only=True)
class Meta:
model = Program
exclude = ('id',)
lookup_field = 'slug'
extra_kwargs = {
'url': {'lookup_field': 'slug'}
}
is this possible? or is it a problem with the way I set up my models?
There's 2 ways I know how to do this. The first is you're pretty close already
EDIT: Noticed you're using related names. I've updated the answer for that
class SchoolSerializer( serializers.ModelSerializer):
programas = ProgramSerializer(source='programs',many=True,read_only=True)
For more complex filtering the best way is to use a SerializerMethodField Field. Here's an example.
You'll probably want to also do some pre-fetches in your view to get the queryset to minimize the # of queries.
class SchoolSerializer(serializers.ModelSerializer):
programas = SerializerMethodField(source='get_programas',many=True,read_only=True)
class Meta:
model = Unidade
fields = '__all__'
lookup_field = 'slug'
extra_kwargs = {
'url': {'lookup_field': 'slug'}
}
def get_programas(self, obj):
# You can do more complex filtering stuff here.
return ProgramaSerializer(obj.programs.all(), many=True, read_only=True).data
To get the PClasses you'll just need to filter your queryset with
program.classes.filter(school=program.school)
Full example for ProgramSerializer is
class ProgramSerializer(serializers.ModelSerializer):
classes = SerializerMethodField(source='get_classes', many=True, read_only=True)
class Meta:
model = Program
def get_classes(self, obj):
return PClassSerializer(obj.classes.filter(school=obj.school), many=True, read_only=True).data
EDIT 10 or so:
Since you have changed the program -> School from foreignkey to ManyToMany, this changes everything.
For the schoolserializer, you need to use a SerializerMethodField. This way you can pass in extra context to your nested serializer.
class SchoolSerializer(serializers.ModelSerializer):
classes = SerializerMethodField(source='get_programs')
class Meta:
model = School
def get_programs(self, obj):
return ProgramSerializer(obj.program_set.all(), many=True, read_only=True, context={ "school": obj }).data
class ProgramSerializer(serializers.ModelSerializer):
classes = SerializerMethodField(source='get_classes', many=True, read_only=True)
class Meta:
model = Program
def get_classes(self, obj):
return PClassSerializer(obj.classes.filter(school=self.context["school"]), many=True, read_only=True).data