How to create dynamic rest ModelSerializer? - python-3.x

I'm currently working on a big code base and i need to send emails from any potential module, that conduct in Circular Dependencies issues in python
so i tried to use apps.get_model() from django.apps but when serializers are declared the models are not ready.
So i'm trying to create a factory function who build the class at runtime instead of launch time
from rest_framework.serializers import ModelSerializer
def make_serializer(model: str, fields: tuple, options = None, **nested_fields) -> ModelSerializer:
"""Generate a new serializer "On the fly", so the model does not have to be imported at launch time.
"""
model_object = apps.get_model(model)
input_fields = fields
if options is None:
options = {}
class Serializer(ModelSerializer):
class Meta:
model = model_object
fields = input_fields
def create(self, validated_data):
# we won't permit to create data from thoses serializers.
raise NotImplementedError
# configure nested serializers.
for nested_field in nested_fields.values():
for key, nested_serializer_class in nested_field.items():
serializer_instance = nested_serializer_class(**options.get(key, {}))
print(model, key, serializer_instance)
setattr(Serializer, key, serializer_instance)
return Serializer
my tests models looks like
class Band(Model):
name = Charfield(max_length=255)
class Influencer(Model):
entity = Charfield(max_length=255)
class Submission(Model):
influencer = ForeignKey(Influencer, ...)
class Campaign(Model):
band = ForeignKey('band.Band', ...)
submissions = ManyToMany(Submission)
and my testing function is:
def test():
serializer = make_serializer(
model='submission.Campaign',
fields=['pk', 'submissions', 'band'],
options={'submissions': {'many': True}},
nested_fields={
'submissions': make_serializer(
model='submission.Submission',
fields=('influencer',),
nested_fields={
'influencer': make_serializer('influencer.Influencer', ('entity',))
},
),
'band': make_serializer('band.Band', ('name',))
}
)
return serializer
instead of having my fields correly with test()(Campaign.objects.last()).data i only got "pks" and my serialiser looks like:
Serializer():
pk = IntegerField(label='ID', read_only=True)
submissions = PrimaryKeyRelatedField(many=True, queryset=Submission.objects.all())
band = PrimaryKeyRelatedField(allow_null=True, queryset=Band.objects.all(), required=False)
i except and output like:
{
"pk": 1,
"band": {
"name": "BobMarley",
},
"submissions": [
{
"influencer": {"entity": "The influencer's name"}
}
]
}
but i got a ReturnDict containing:
{
"pk": 1,
"band": 523,
"submissions": [6, 7, 8]
}
thanks for your time

well after many headcaches i've found out that i CAN'T setattr on a class after it's declaration, so i use a trick based on a dict
def make_serializer(model: str, fields: tuple, options = None, **nested_fields) -> ModelSerializer:
"""Generate a new serializer "On the fly", so the model does not have to be imported at launch time.
"""
name = f'Serializer_{model}'
model_object = apps.get_model(model)
input_fields = fields
if options is None:
options = {}
def create(self, validated_data):
# we won't permit to create data from thoses serializers.
raise NotImplementedError
class Meta:
model = model_object
fields = input_fields
attrs = {"Meta": Meta}
# configure nested serializers.
for key, nested_serializer_class in nested_fields.items():
attrs[key] = nested_serializer_class(**options.get(key, {}))
attrs['create'] = create
return type(ModelDictSerializer)(name, (ModelDictSerializer,), attrs)
the syntax is something like:
campaign_serializer = make_serializer(
model='submission.Campaign',
fields=['pk', 'submissions', 'band'],
options={'submissions': {'many': True}},
submissions=make_serializer(
model='submission.Submission',
fields=('influencer',),
influencer=make_serializer('influencer.Influencer', ('entity',))
),
band=make_serializer('band.Band', ('name',))
)
and it's working like a charm:
Serializer_submission.Campaign(<Campaign: Campaign object (9665)>):
pk = IntegerField(label='ID', read_only=True)
submissions = Serializer_submission.Submission(many=True):
influencer = Serializer_influencer.Influencer():
entity = CharField(allow_blank=True, max_length=255, required=False)
band = Serializer_band.Band():
name = CharField(max_length=255)
i hope this will help someone else

Related

DRF Intermediary Table - POSTing data to the Intermediary Table

I have a many-to-many relationship in my DB design and I am having trouble with the POST aspect. I currently have a table called Loads, Containers, and Container_Loads (this is the intermediary table).
My question is this:
I want to be able to send a POST request into the ContainerLoad intermediary table and just update that table with the values it requires which are: Load ID (PK of the Load table), Container ID (PK of the Container Table) and # of pallets (unique field to the intermediary table). I am able to GET/retrieve the records just fine, but when I try to send a POST request with a payload such as
{
"id":3,
"pallets":"4",
"containerNumberId":5,
"loadNumberId":53
}
(where containerNumberID and loadNumberID are the existing keys in their respective tables), it seems that my code wants to create a whole new Load entry as well (as it asks me for the remaining fields of the Load model), where as I just want to create an entry in the intermediary table without creating a new entry in the Load table.
So for the purpose of my project, a load can be on many containers [imagine that it's split because all of it couldn't fit on one] and a container can belong to many loads.
My models.py looks like this:
class ContainerLoad(models.Model):
id = models.AutoField(primary_key=True)
load_number = models.ForeignKey(Load,on_delete=models.CASCADE)
container_number = models.ForeignKey(Container,on_delete=models.CASCADE)
pallets = models.CharField(blank=True,null=True,default=0,max_length=20)
class Meta:
db_table = 'ContainerLoad'
#load model shortened for brevity
class Load(models.Model):
id = models.AutoField(primary_key=True)
bnsf_container_number = models.ManyToManyField(Container, through='ContainerLoad',through_fields=('load_number','container_number'))
class Meta:
db_table = "Load"
class Container(models.Model):
id = models.AutoField(primary_key=True)
container_number = models.CharField(max_length=15)
in_use = models.BooleanField()
class Meta:
db_table = "Container"
my serializers.py currently looks like this, the commented out section is from me attempting to get the POST to work)
class ContainerLoadSerializer(WritableNestedModelSerializer):
# load_number_id = LoadSerializer(read_only=False)
# container_number_id = ContainerSerializer(read_only=False)
class Meta:
model = ContainerLoad
fields = "__all__"
depth = 2
class LoadSerializer(WritableNestedModelSerializer):
primary_driver = DriverSerializer(read_only=False)
second_driver = DriverSerializer(allow_null=True,read_only=False)
third_driver = DriverSerializer(allow_null=True,read_only=False)
bnsf_container_number = ContainerSerializer(read_only=False)
pickup_location = LocationSerializer(read_only=False)
delivery_location = LocationSerializer(read_only=False)
broker = BrokerSerializer(read_only=False)
booked_by = EmployeeSerializer(read_only=False)
class Meta:
model = Load
fields = '__all__'
depth = 1
class ContainerSerializer(serializers.ModelSerializer):
container_number = serializers.CharField()
in_use = serializers.BooleanField()
class Meta:
model = Container
fields = '__all__'
depth = 1
And finally the views.py
class ContainerLoadViews(APIView):
def get(self, request, id=None):
if id:
container = ContainerLoad.objects.get(id=id)
serializer = ContainerLoadSerializer(container)
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
else:
containers = ContainerLoad.objects.all()
serializer = ContainerLoadSerializer(containers, many=True)
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
def post(self, request):
serializer = ContainerLoadSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
else:
return Response({"status": "Error", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
If you look at the code of the save method of the BaseSerializer class you will see this:
if self.instance is not None:
self.instance = self.update(self.instance, validated_data)
assert self.instance is not None, (
'`update()` did not return an object instance.'
)
else:
self.instance = self.create(validated_data)
assert self.instance is not None, (
'`create()` did not return an object instance.'
)
How you are not passing the instance in the post function:
serializer = ContainerLoadSerializer(data=request.data)
The save is always calling to create. You should do something like.
try:
instance = ContainerLoad.object.get(id=request.data['id'])
except:
instance = None
serializer = ContainerLoadSerializer(instance=instance, data = request.data)
You have depth is set to 2 in your ContainerLoadSerializer Meta class, which is telling the serializer to generate a nested representation of your models.
https://www.django-rest-framework.org/api-guide/serializers/#specifying-nested-serialization
The default ModelSerializer uses primary keys for relationships, but you can also easily generate nested representations using the depth option:
The depth option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation.
If you remove the depth attribute, the serializer should default back to expecting a primary key value, which is your desired behaviour.
Your serializer should look something like this:
class ContainerLoadSerializer(serializers.ModelSerializer):
class Meta:
model = ContainerLoad
fields = "__all__"
The solution to this was that I needed a nested response when reading the data from the ContainerLoad table but a simple write (not nested) function when POSTing the data.
The solution was to use the to_representation and to_internal_value methods (https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior) available within DRF to override the behavior of the serializers. Here is the code that now works for both GET and POST requests and it is no longer asking me for fields related to the Load or Container models when inserting data.
class ContainerSerializer(serializers.ModelSerializer):
container_number = serializers.CharField()
in_use = serializers.BooleanField()
class ContainerFieldSerializer(serializers.Field):
def to_internal_value(self,value):
return Container.objects.get(id=value)
def to_representation(self,instance):
return ContainerSerializer(instance=instance).data
class Meta:
model = Container
fields = '__all__'
depth = 1
I did the same for the Load Serializer.
and then for my ContainerLoad Serializer I just assign the FK fields to the new classes I created:
class ContainerLoadSerializer(serializers.ModelSerializer):
cl_container = ContainerSerializer.ContainerFieldSerializer()
cl_load = LoadSerializer.LoadFieldSerializer()
class Meta:
model = ContainerLoad
fields = "__all__"
depth = 2

DRF use nested serializer to create key object instead of array

Context
Say we take this example from the DRF relations guide.
# models.py
class Album(models.Model):
album_name = models.CharField(max_length=100)
artist = models.CharField(max_length=100)
class Track(models.Model):
album = models.ForeignKey(Album, related_name='tracks', on_delete=models.CASCADE)
order = models.IntegerField()
title = models.CharField(max_length=100)
duration = models.IntegerField()
class Meta:
unique_together = ('album', 'order')
ordering = ['order']
def __str__(self):
return '%d: %s' % (self.order, self.title)
Using a serializer will get us this output
class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.StringRelatedField(many=True)
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
Will get us this output:
{
'album_name': 'Things We Lost In The Fire',
'artist': 'Low',
'tracks': [
'1: Sunflower',
'2: Whitetail',
'3: Dinosaur Act',
...
]
}
Question
How can i use the serializer to get the output like this:
{
'album_name': 'Things We Lost In The Fire',
'artist': 'Low',
'tracks': {
1: {order: 1, title: 'Sunflower'},
2: {order:2, title: 'Whitetail'},
3: {order:3, title: 'Dinosaur Act'},
...
}
}
This way we have an object with tracks instead of a numeric array. So i can do this.props.album.tracks[2].title this instead of this.props.album.tracks.find(track => track.order == 2}).title in javascript.
``I have an use case in where this seems to be more convenient in Reactjs.
What i have tried
I thought about overriding the to_representation method. But i see that this will get me an recursive loop.
class TrackSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
print(self)
return '%s: { %s }' % (instance.order, self.to_representation(instance))
class Meta:
fields = '__all__'
model = Track
Furthermore i have searched and read the docs pretty well. But didn't find any solution for what i think should be a pretty logical solution to have out of the box. Making me think that i am wrong and missing something.
Thanks in advance.
Define a new TrackSerializer and use it in AlbumSerializer class as,
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = ('id', 'order', 'title')
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True)
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['tracks'] = {track['id']: track for track in representation['tracks']}
return representation

marshmallow-mongoengine:Output dump value missing 'None' field

My project uses flask+mongoengine+marshmallow,When I used marshmallow to serialize the model, the returned value lacked field, and the missing field value was None.When using Django to serialize fields, the None value is still output
model
class Author(db.Document):
name = db.StringField()
gender = db.IntField()
books = db.ListField(db.ReferenceField('Book'))
def __repr__(self):
return '<Author(name={self.name!r})>'.format(self=self)
class Book(db.Document):
title = db.StringField()
serializers
class AuthorSchema(ModelSchema):
class Meta:
model = Author
class BookSchema(ModelSchema):
class Meta:
model = Book
author_schema = AuthorSchema()
When I do this:
author = Author(name="test1")
>>> author.save()
<Author(name='test1')>
>>> author_schema.dump(author)
MarshalResult(data={'id': '5c80a029fe985e42fb4e6299', 'name': 'test1'}, errors={})
>>>
not return the books field
I hope to return
{
"name":"test1",
"books": None
}
what should I do?
When I looked at the source code of the marshmallow-mongoengine library, I found the solution model_skip_values=() in the tests file.
def test_disable_skip_none_field(self):
class Doc(me.Document):
field_empty = me.StringField()
list_empty = me.ListField(me.StringField())
class DocSchema(ModelSchema):
class Meta:
model = Doc
model_skip_values = ()
doc = Doc()
data, errors = DocSchema().dump(doc)
assert not errors
assert data == {'field_empty': None, 'list_empty': []}

get() returned more than one Sub_Topic -- it returned 3

I have started a project using Django. Where I used add multiple sub-topics under one main topic by taking staticid. When I am giving same staticid to multiple sub-topics, I am getting the error below (get() returned more than one Sub_Topic -- it returned 3!).
Model:
class Sub_Topic(models.Model):
IMPORTANCE_SCORE = (
('LOW','Low'),
('NORMAL', 'Normal'),
('HIGH','High'),
)
staticid = models.ForeignKey(SID,on_delete=models.CASCADE, blank=True, default=None, null=True)
sub_topic = models.CharField(max_length=250)
Num_Of_Sub_subTopics = models.PositiveIntegerField(default=0)
Num_Of_Questions = models.PositiveIntegerField(default=0)
importance = models.CharField(max_length=6, choices= IMPORTANCE_SCORE, default='LOW')
complexity = models.PositiveIntegerField(default=0)
prerequisite = models.CharField(max_length=250)
def __str__(self):
return self.sub_topic
View:
class Sub_TopicDetailView(generics.RetrieveUpdateDestroyAPIView):
"""
GET sub_topic/:id/
PUT sub_topic/:id/
DELETE sub_topic/:id/
"""
queryset = Sub_Topic.objects.all()
serializer_class = Sub_TopicSerializer
def get(self, request, *args, **kwargs):
try:
a_sub_topic = self.queryset.get(staticid=kwargs["staticid"])
return Response(Sub_TopicSerializer(a_sub_topic).data)
except Sub_Topic.DoesNotExist:
return Response(
data={
"message": "Sub_Topic with id: {} does not exist".format(kwargs["staticid"])
},
status=status.HTTP_404_NOT_FOUND
)
#validate_request_data
def put(self, request, *args, **kwargs):
try:
a_sub_topic = self.queryset.get(staticid=kwargs["staticid"])
serializer = Sub_TopicSerializer()
updated_sub_topic = serializer.update(a_sub_topic, request.data)
return Response(Sub_TopicSerializer(updated_sub_topic).data)
except Sub_Topic.DoesNotExist:
return Response(
data={
"message": "Sub_Topic with id: {} does not exist".format(kwargs["staticid"])
},
status=status.HTTP_404_NOT_FOUND
)
Error:
get() returned more than one Sub_Topic -- it returned 3!
How do I overcome this?
If you have a main topic (say, "donuts"), and many subtopics within that ("plain donuts", "chocolate donuts", "vanilla donuts", ...), you cannot reference a subtopic by just saying "donuts", you have to be more specific.
Your sub-topic views should accept a sub-topic ID, not the main topic ID. Try changing this:
a_sub_topic = self.queryset.get(staticid=kwargs["staticid"])
# 'staticid' is the foreign key of the main topic: it is
# the same for many sub-topics!
to this:
a_sub_topic = self.queryset.get(id=kwargs["id"])
# 'id' is the primary key field generated automatically by Django:
# it's unique for every sub-topic
If instead you want to display all sub-topics for a given topic, then you should use filter() instead of get():
sub_topics = self.queryset.filter(staticid=kwargs["staticid"])

instance expected, got OrderedDict Django Rest Framework writable nested serializers

I am creating a survey kind of app, so i have three models Form, Questiosn, Choices[for multiple choice questions]
I followed this tutorial http://www.django-rest-framework.org/api-guide/relations/#nested-relationships
It works fine for 1 level nested relations, but for 2 levels it gives
TypeError: 'Choice' instance expected, got OrderedDict([(u'title', u'option1')])
class ChoiceSerializer(serializers.ModelSerializer):
class Meta:
model = Choice
fields = ['title']
class QuestionSerializer(serializers.ModelSerializer):
choices = ChoiceSerializer(many=True, required=False)
class Meta:
model = Question
fields = ['title', 'type', 'required','order','choices']
def create(self, validated_data):
choices_data = validated_data.pop("choices")
question = Question.objects.create(**validated_data)
for choice_data in choices_data:
Choice.objects.create(question=question, **choice_data)
return question
class FormSerializer(serializers.ModelSerializer):
questions = QuestionSerializer(many=True)
class Meta:
model = Form
fields = ['title', 'description', 'created', 'active', 'hash','questions']
read_only_fields = ['active','hash']
def create(self, validated_data):
questions_data = validated_data.pop('questions')
form = Form.objects.create(**validated_data)
for question_data in questions_data:
Question.objects.create(form=form, **question_data)
return form
EDIT
Solved using the manual way, In FormSerializer override the create method,
#transaction.atomic
def create(self, validated_data):
try:
with transaction.atomic():
questions_data = validated_data.pop('questions')
form = Form.objects.create(**validated_data)
for question_data in questions_data:
question = Question.objects.create(form=form,
title=question_data['title'],
type=question_data['type'],
required=question_data['required'])
if question.type == Question.RADIO or question.type == Question.CHECKBOX:
choices_data = question_data.pop('choices')
for choice_data in choices_data:
choice = Choice.objects.create(question=question, title=choice_data['title'])
return form
except Exception, e:
raise serializers.ValidationError("Cannot Save Form %s" % e)
I also struggled with this and I believe the proper way to handle this is:
class ChoiceSerializer(serializers.ModelSerializer):
class Meta:
model = Choice
fields = ['title']
class QuestionSerializer(serializers.ModelSerializer):
choices = ChoiceSerializer(many=True, required=False)
class Meta:
model = Question
fields = ['title', 'type', 'required','order','choices']
class FormSerializer(serializers.ModelSerializer):
questions = QuestionSerializer(many=True)
class Meta:
model = Form
fields = ['title', 'description', 'created', 'active',
'hash','questions']
read_only_fields = ['active','hash']
def create(self, validated_data):
questions_data = validated_data.pop('questions')
form = Form.objects.create(**validated_data)
for question_data in questions_data:
choices_data = question_data.pop('choices')
Question.objects.create(form=form, **question_data)
for choice_data in choices_data:
choice = Choice.objects.create(question=question, **choice_data)
return form
An easy way to screw this up is to not pop choices before creating the Question object. When you do that, you'll get an instance expected, got OrderedDict( 500 error.
Note also that you do not need to define create() on the QuestionSerializer. All child processing is done at the top level.

Resources