How to serialize a Wagtail Orderable model? - python

I came across a challenge which I also intent to blog about. I found a solution but, since I'm going to write about it, I wonder if there is a better way to serialize an Orderable model.
Context: My LessonPage(Page) model has a LessonPageDocuments(Orderable) model that will allow users to add multiple documents to a particular LessonPage:
class LessonPageDocuments(Orderable):
page = ParentalKey(LessonPage, on_delete=models.CASCADE,
related_name='documents')
document = models.ForeignKey(
'wagtaildocs.Document', on_delete=models.CASCADE, related_name='+'
)
panels = [
DocumentChooserPanel('document'),
]
Now, due to this projects needs and business requirements, we're creating a custom REST API instead of using Wagtail's API.
And the way I found to serialize the documents field was the following:
class LessonDetailsSerializer(serializers.ModelSerializer):
content = RichTextSerializer()
documents = serializers.SerializerMethodField()
def to_representation(self, instance):
ret = super().to_representation(instance)
video_url = ret['video_url']
ret['video_url'] = get_embed(video_url).html if video_url else ''
return ret
def get_documents(self, lesson):
"""Return serialized document fields and file URL"""
request = self.context.get('request')
doc_list = []
for doc_cluster in lesson.documents.all():
doc_list.append({
"url": request.build_absolute_uri(doc_cluster.document.file.url),
"title": doc_cluster.document.title,
"id": doc_cluster.document.pk,
})
return doc_list
class Meta:
model = LessonPage
fields = ['id', 'title', 'slug', 'description',
'video_url', 'content', 'documents']
Is there a better approach to serialize this field?
Thank you so much in advance!

I think the only way (that I know of) to do it better is to make it reusable:
wagtail_api/serializers.py
from wagtail.documents.api.v2.serializers import DocumentDownloadUrlField
from wagtail.documents.models import Document
class DocumentSerializer(serializers.ModelSerializer):
_detail_url = serializers.HyperlinkedIdentityField(view_name='api:wagtail:documents:detail')
download_url = DocumentDownloadUrlField()
class Meta:
model = Document
fields = (
'_detail_url',
'id', 'title', 'download_url',
)
my_app/serializers.py
class SomePageDocumentSerializer(serializers.ModelSerializer):
description = RichTextAPIField()
document = DocumentSerializer()
class Meta:
model = SomePage
fields = ('description', 'document')
There is also probably a way to overwrite wagtails DocumentSerializer
from ee_wagtail.apps.wagtail_api.serializers

Related

Filtering results of querying a model based on ids of related ManyToMany another Model ids

I have 2 models
class Tag(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Question(models.Model):
ques_id = models.IntegerField(default=0)
name = models.CharField(max_length=255)
Tag_name = models.ManyToManyField(Tag)
class Meta:
ordering = ['ques_id']
def __str__(self):
return self.name
searlizers.py
class TagSerializers(serializers.ModelSerializer):
class Meta:
model = Tag
fields = '__all__'
class QuestionSerializers(serializers.ModelSerializer):
class Meta:
model = Question
fields = '__all__'
This is my searilzers class.
I want the response like
{
"id": 1,
"name": "QUES 1",
"tags": [{
"id": 1,
"name": "Abcd"
}]
}
what will be query to get Fetch 10 questions, given some input tag ids
e.g Tag_id = 1 or 2 or 3.
You need to add tags field as another serializer data to your QuestionSerializer.
Your QuestionSerializer code should look like that:
class QuestionSerializers(serializers.ModelSerializer):
Tag_name = TagSerializer(many=True)
class Meta:
model = Question
fields = '__all__'
If you want exactly tags name in response, you can specify SerializerMethodField like that:
class QuestionSerializer(serializers.ModelSerializer):
tags = serializers.SerializerMethodField()
get_tags(self, instance):
return TagSerializer(instance.Tag_name, many=True).data
class Meta:
model = Question
fields = ('ques_id', 'name', 'tags')
First: I would suggest that you refactor your Question Model, since it has a ques_id, and I think it is considered a duplicate (since Django already creates an id field by default)
Then You need to change your ManyToManyField's name to tags, and makemigrations, then migrate
class Question(models.Model):
name = models.CharField(max_length=255)
tags = models.ManyToManyField(Tag)
class Meta:
ordering = ['id']
def __str__(self):
return self.name
# run make migrations
python manage.py makemigrations <<<YOUR QUESTIONS APP NAME>>>
# It will prompt you to check if you change the many to many relationship say yes
Did you rename question.Tag_name to question.tags (a ManyToManyField)? [y/N] y
# Then Run migrate
python manage.py migrate
Second: Update your QuestionSerializers to make the tags field serialize the relation
class QuestionSerializers(serializers.ModelSerializer):
tags = TagSerializers(many=True)
class Meta:
model = Question
fields = '__all__'
This way you make your code cleaner. And you are good to go.
Answer Updated (Filtering, and Pagination)
Now if you wanted to filter questions based on provided tag ids.
You need to use PageNumberPagination for your view, and for filtering use DjangoFilterBackend.
I recommend you to make them the default of DRF settings.
Make sure you have django-filter installed.
# In your settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
),
'PAGE_SIZE': 10
}
Now create your custom filter
# Inside filters.py
import re
import django_filters
from questions.models import Question
class QuestionsFilterSet(django_filters.FilterSet):
tags = django_filters.CharFilter(field_name='tags__id', method='tags_ids_in')
def tags_ids_in(self, qs, name, value):
ids_list = list(map(lambda id: int(id), filter(lambda x: x.isdigit(), re.split(r'\s*,\s*', value))))
return qs.filter(**{f"{name}__in": ids_list}).distinct()
class Meta:
model = Question
fields = ('tags',)
Now use ListAPIView to list your questions
class QuestionsListAPIView(ListAPIView):
queryset = Question.objects.all()
serializer_class = QuestionSerializers
filter_class = QuestionsFilterSet
Now if you hit
http://<PATH_TO_VIEW>?tags=1,2,3
You will receive all Questions that have tag id 1, 2, or 3.
and all of them will be paginated 10 results at a time.
You just need to write a field tags like this in your QuestionSerializers
class QuestionSerializers(serializers.ModelSerializer):
tags = TagSerializers(source='Tag_name', many=True)
class Meta:
model = Question
fields = ('id', 'name', 'tags')
It will give you response like this
{
"id": 1,
"name": "QUES 1",
"tags": [{
"id": 1,
"name": "Abcd"
}]
}
Your query to fetch on the basis of tags will be like this
Question.objects.filter(Tag_name__in=[1, 2, 4])

How to nest two serializers with same model

I have two serializers with same model. I want to nest them.
Unfortunately this approach does not work:
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['name', 'word_count']
class BetterBookSerializer(serializers.ModelSerializer):
book = BookSerializer(many=False)
class Meta:
model = Book
fields = ('id', 'book')
Expected result:
{
"id": 123,
"book": {
"name": "book_name",
"word_count": 123
}
}
Use source=* instead of many=True as
class BetterBookSerializer(serializers.ModelSerializer):
book = BookSerializer(source='*')
class Meta:
model = Book
fields = ('id', 'book')
From the doc,
The value source='*' has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations, or for fields which require access to the complete object in order to determine the output representation.
You can achieve the desired output like this:
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['name', 'word_count']
class BetterBookSerializer(serializers.ModelSerializer):
book = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Book
fields = ('id', 'book')
def get_book(self, obj):
return BookSerializer(obj).data
Small Update:
Although my approach to solve your problem works just fine, the answer from #JPG mentioning source='*' option is a good way to go. In that way you can easily use the nested serializer when creating new object.

Modifying value on serialization - Django Rest Framework

I have a model which contains sensitive data, let's say a social security number, I would like to transform that data on serialization to display only the last four digits.
I have the full social security number stored: 123-45-6789.
I want my serializer output to contain: ***-**-6789
My model:
class Employee (models.Model):
name = models.CharField(max_length=64,null=True,blank=True)
ssn = models.CharField(max_length=16,null=True,blank=True)
My serializer:
class EmployeeSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
class Meta:
model = Employee
fields = ('id','ssn')
read_only_fields = ['id']
You can use SerializerMethodField:
class EmployeeSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
ssn = SerializerMethodField()
class Meta:
model = Employee
fields = ('id','ssn')
read_only_fields = ['id']
def get_ssn(self, obj):
return '***-**-{}'.format(obj.ssn.split('-')[-1]
If you don't need to update the ssn, just shadow the field with a SerializerMethodField and define get_ssn(self, obj) on the serializer.
Otherwise, the most straightforward way is probably to just override .to_representation():
def to_representation(self, obj):
data = super(EmployeeSerializer, self).to_representation(obj)
data['ssn'] = self.mask_ssn(data['ssn'])
return data
Please add special case handling ('ssn' in data) as necessary.
Elaborating on #dhke’s answer, if you want to be able to reuse this logic to modify serialization across multiple serializers, you can write your own field and use that as a field in your serializer, such as:
from rest_framework import serializers
from rest_framework.fields import CharField
from utils import mask_ssn
class SsnField(CharField):
def to_representation(self, obj):
val = super().to_representation(obj)
return mask_ssn(val) if val else val
class EmployeeSerializer(serializers.ModelSerializer):
ssn = SsnField()
class Meta:
model = Employee
fields = ('id', 'ssn')
read_only_fields = ['id']
You can also extend other fields like rest_framework.fields.ImageField to customize how image URLs are serialized (which can be nice if you’re using an image CDN on top of your images that lets you apply transformations to the images).

Django Rest Framework - Interrelated serializers

Given the following models for which two have a ManyToMany relationship with the first, how can I construct a serializer for all three models?
This is where the importance of ordering in Python is turning to be an issue.
class Trail(models.Model):
'''
Main model for this application. Contains all information for a particular trail
'''
trail_name = models.CharField(max_length=150)
active = models.BooleanField(default=True)
date_uploaded = models.DateTimeField(default=now())
owner = models.ForeignKey(Account, default=1)
class Meta:
ordering = ('trail_name', )
class Activity(models.Model):
'''
A many to many join table to map an activity name to a trail. A trail can have many
activities
'''
name = models.CharField(max_length=40, unique=True)
trails = models.ManyToManyField(Trail)
class Meta:
ordering = ('name', )
class Surface(models.Model):
'''
A many to many join table to map a surface type to many trails. A trail can have many
surface types.
'''
type = models.CharField(max_length=50, db_column='surface_type', unique=True)
trails = models.ManyToManyField(Trail)
class Meta:
ordering = ('type', )
I have the following serializers in serializers.py:
class TrailSerializer(serializers.ModelSerializer):
owner = AccountSerializer()
activities = ActivitySerializer()
class Meta:
model = Trail
fields = ('trail_name', 'active', 'date_uploaded', 'owner', 'activities', )
class ActivitySerializer(serializers.ModelSerializer):
trails = TrailSerializer()
class Meta:
model = Activity
fields = ('trails', 'name', )
class SurfaceSerializer(serializers.ModelSerializer):
trails = TrailSerializer()
class Meta:
model = Surface
fields = ('trails', 'type', )
My question is, short of creating another file to contain ActivitySerializer and SurfaceSerializer, how can I ensure that TrailSerializer works as expected in order to include Activity and Surface references during serialization?
Use Marshmallow (serialization library inspired in part by DRF serializers). It solves this problem, by allowing nested schemas to be reference as strings. See Marshmallow: Two Way Nesting
class AuthorSchema(Schema):
# Make sure to use the 'only' or 'exclude' params to avoid infinite recursion
books = fields.Nested('BookSchema', many=True, exclude=('author', ))
class Meta:
fields = ('id', 'name', 'books')
class BookSchema(Schema):
author = fields.Nested(AuthorSchema, only=('id', 'name'))
class Meta:
fields = ('id', 'title', 'author')
You can use directly or use via django-rest-marshmellow by Tom Christie, which allows use of Marshmallow, while maintaining the same API as REST framework's Serializer class.
I'm not aware of a way to achieve the same result using just DRFs serializers.
See also: Why Marshmallow?

Django Tastypie - Filtering ToManyField resource with URL parameter

I am working on implementing an API for my Django (v1.5) application using Tastypie. I would like to be able to filter/limit the related resources I get when the parent resource.
Here are my (simplified) models:
# myapp/models.py
class User(models.Model):
number = models.IntegerField()
device_id = models.CharField(verbose_name="Device ID", max_length=255)
timezone = models.CharField(max_length=255, blank=True)
def data(self, limit=0):
result = Data.objects.filter(patient_id = self.id).order_by('-datetime').values('datetime', 'value')
if limit != 0:
result = result[:limit]
return result
class Data(models.Model):
user = models.ForeignKey(User)
datetime = models.DateTimeField()
value = models.IntegerField()
My resources:
# api/resources.py
class DataResource(ModelResource):
class Meta:
queryset = Data.objects.all()
resource_name = 'cgm'
fields = ['value', 'datetime']
serializer = Serializer(formats=['json', 'xml'])
filtering = {
'datetime': ('gte', 'lte'),
}
include_resource_uri = False
def dehydrate(self, bundle):
bundle.data['timestamp'] = calendar.timegm(bundle.data['datetime'].utctimetuple())
return bundle
class UserResource(ModelResource):
data = fields.ToManyField(DataResource, attribute=lambda bundle: Data.objects.filter(patient_id=bundle.obj.id), full=True, related_name='data', null=True)
class Meta:
queryset = User.objects.all().order_by('number')
resource_name = 'user'
fields = ['number', 'timezone', 'device_id'],
serializer = Serializer(formats=['json', 'xml'])
filtering = {
'data': ALL_WITH_RELATIONS,
}
I would like to be able to filter the Data resources by 'datetime' inside the User resource using an URL parameter, e.g.:
127.0.0.1:8000/api/v1/user/1/?format=json&datetime__gte=2013-11-14%2012:00:00
or
127.0.0.1:8000/api/v1/user/1/?format=json&data__datetime__gte=2013-11-14%2012:00:00
to get the User's number, timezone, device id and Data list filtered with the given datetime.
I don't want to have to query the Data resources separately to filter them, I want the whole thing bundled within the User resource.
Is there a way to implement a filter applied to the nested resource using the framework?
Thanks for your time, I'll appreciate any suggestion!
You can extend your attribute argument you've passed to the data field with a full-scale function and reuse the DataResource:
def filter_data_items(bundle):
res = DataResource()
new_bundle = Bundle(request=bundle.request)
objs = res.obj_get_list(new_bundle)
return objs.filter(parent_id=bundle.obj.pk)
res.obj_get_list handles building and applying filters as defined per your DataResource. You just need to filter it futher on parent_id.
Reference.

Categories