I'm developing a web system using Django 1.11 and the current version of Django Rest Framework which is 3.8.2.
I'm having a problem while serving a JSON of my models in this project. I need to pass a JSON that contains a specific attribute.
I'm gonna exemplify my serializers.py to make an easier understanding of my problem.
class LikertSerializerModel(serializers.ModelSerializer):
class Meta:
model = Likert
fields = ('id', 'escala')
class RespostaSerializerModel(serializers.ModelSerializer):
likerts = LikertSerializerModel(many = True, read_only = True)
class Meta:
model = Resposta
fields = ('id', 'resposta','tipo', 'foto', 'pergunta', 'qtd_escolhida', 'classificacao_escala', 'data', 'likerts')
class PerguntaSerializerModel(serializers.ModelSerializer):
respostas = RespostaSerializerModel(read_only=True ,many=True)
class Meta:
model = Pergunta
fields = ('id', 'pergunta', 'tipo_questao', 'questionario', 'respondida', 'data_inicial', 'data_final',
'obrigatoria', 'outros', 'ordem', 'qtd_max_caracteres', 'respostas')
class QuestionarioSerializerModel(serializers.ModelSerializer):
entrevistadores = UsuarioSerializer(many = True, read_only = True)
sub_administrador = UsuarioSerializer(read_only= True)
perguntas = PerguntaSerializerModel(many = True, read_only = True)
class Meta:
model = Questionario
fields = ('id', 'titulo', 'descricao', 'data', 'duracao', 'localizacao', 'sub_administrador', 'entrevistadores', 'perguntas')
THE PROBLEM: See, in the PerguntaSerializerModel I have this field "respostas". And the thing is, I only want to pass to that Pergunta Object, Resposta Objects that have a "tipo" attribute with a value of "alt" and are linked with that Pergunta Object.
PS: If you need an explanation of how those classes are linked, here it goes (we can ignore the Likert Object as it is useless to the problem):
A Resposta Object is linked by a ForeignKey to a single Pergunta Object, and a Pergunta Object is linked by a ForeignKey to a single Questionario Object.
A Questionario Object can have one or more Pergunta Objects and a Pergunta Object can have one or more Resposta Objects
When you do a GET in the rest URL the final JSON will start with the Questionario Object, and will cascade until the last Pergunta Object.
Please help me to find an answer, I wasn't able to find one in the REST documentation because I don't know what I need to look for.
Best regards.
What you need here is a SerializerMethodField(). You can read through the docs for more info on it.
Create a method on the serializer PerguntaSerializerModel that returns a filtered queryset of Resposta objects on the basis of attribute tipo.
Something like this should work fine:
class PerguntaSerializerModel(serializers.ModelSerializer):
respostas = serializers.SerializerMethodField()
class Meta:
model = Pergunta
fields = ('id', 'pergunta', 'tipo_questao', 'questionario', 'respondida', 'data_inicial', 'data_final',
'obrigatoria', 'outros', 'ordem', 'qtd_max_caracteres', 'respostas')
def get_respostas(self, obj):
reposta_qs = obj.filter(reposta__tipo='alt')
resposta_serailizer = RespostaSerializerModel(reposta_qs, read_only=True ,many=True)
return resposta_serailizer.data
Let me know if this helps !
Related
I have 3 models: Maker, Item and MakerItem that creates the relation between the items and their makers:
class Maker(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
class Item(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
class MakerItem(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
item_id = models.ForeignKey(Item, on_delete=models.CASCADE)
maker_id = models.ForeignKey(Maker, on_delete=models.CASCADE)
the items can have a random amount of makers.
I want to create both the Item and the MakerItem objects at the same time with a single set of data,
for example if a Maker with id = "abcd" already exists, and I go to /item and send a POST request with the following data:
{
"name": "item1",
"makers": [
{
"maker_id": "abcd"
}
]
}
I want the serializer to create the Item object and the MakerItem object.
I have achieved this, with the following setup:
views.py
class ItemListCreate(ListCreateAPIView):
queryset = Item.objects.all()
serializer_class = ItemSerializer
serializers.py
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True)
class Meta:
model = Item
fields = ['id', 'name', 'makers']
def create(self, validated_data):
maker_item_data = validated_data.pop('makers')
item_instance = Item.objects.create(**validated_data)
for each in maker_item_data:
MakerItem.objects.create(
item_id=check_instance,
maker_id=each['maker_id']
)
return item_instance
but when Django tries to return the created object, it always gives me the error:
AttributeError at /item/
Got AttributeError when attempting to get a value for field `makers` on serializer `ItemSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Item` instance.
Original exception text was: 'Item' object has no attribute 'makers'.
What am I doing wrong?
Thanks
EDIT: To clarify, the objects get created and populate the database correctly, but when the browsable API that DRF provides tries to display the created object, it gives me the error above.
Change:
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True)
To:
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True, source="makeritem_set")
Hope this works!
For clarity, you're attempting to serialise the reverse relationship between MakerItem and Item for this serialiser.
This means that the attribute on your object is automatically set by Django as fieldname_set but you can override this behaviour by setting the related_name kwarg on the field and then makemigrations and migrate it.
In your case you would need to do:
maker_id = models.ForeignKey(Maker, on_delete=models.CASCADE, related_name="maker_items")
And then update the field in the Meta to match the new field name, this way you don't have to manually specify source. Because actually the attribute "makers" is misleading, due to the fact its actually the MakerItem, not the Maker itself.
See https://docs.djangoproject.com/en/3.2/ref/models/relations/ for further details about this behaviour.
Let's say we have three Models : ModelA, related to ModelB and ModelC.
ModelA is defined as :
class ModelA(models.Model):
field_b = models.ForeignKey(ModelB, on_delete=models.CASCADE)
field_c = models.ForeignKey(ModelC, on_delete=models.CASCADE)
other_field = models.CharField(max_length=30)
class Meta:
constraints = [
models.UniqueConstraint(fields=['field_b', 'field_c'], name='unique_modelA')
]
How to generate a ModelASerializer instance from instances of ModelB
and ModelC ?
Then, will there be a serialized representation of
field_b and field_c into ModelASerializer ?
The UniqueConstraint will
it be checked when calling .is_valid() onto ModelASerializer
instance ?
I tried the following :
class ModelASerializer(serializers.ModelSerializer):
field_b = ModelBSerializer(read_only=True)
field_c = ModelCSerializer(read_only=True)
other_field = serializers.CharField(required=False)
class Meta:
model = ModelA
fields = ('field_b', 'field_c', 'other_field',)
validators = [
UniqueTogetherValidator(
queryset=ModelA.objects.all(),
fields=['field_b', 'field_c'],
message='A Model A instance for this couple of ModelB and ModelC instances already exists.'
)
]
def create(self, validated_data):
"""
Create and return a new `ModelA` instance, given the validated data.
"""
return ModelA.objects.create(**validated_data)
def update(self, instance, validated_data):
"""
Update and return an existing `ModelA` instance, given the validated data.
"""
instance.other_field= validated_data.get('other_field', instance.other_field)
instance.save()
return instance
But I cannot find any way to create the serializer :
model_b = ModelB()
model_c = ModelC()
model_b.save()
model_c.save()
other_field = "Dummy content"
First try
model_a_serializer = ModelASerializer(model_b, model_c, other_field)
The serializer is looking for an ID field and can't find it
Anyway, no data field being provided, we can't call .is_valid() onto the serializer, and thus, can't check the integrity constraint
Second try
model_b_serializer = ModelBSerializer(model_b)
model_c_serializer = ModelCSerializer(model_c)
data = {'model_b':model_b_serializer , 'model_c':model_c_serializer , 'other_field':other_field}
model_a_serializer = ModelASerializer(data=data)
if model_a_serializer.is_valid():
model_a_serializer.save()
Here, the serializer tries to recreate the ModelB and ModelC instances when is_valid() is called... And I don't want that.
Any ideas? Thank you very much by advance.
Not sure if I understood your question properly but...
How to generate a ModelASerializer instance from instances of ModelB and ModelC ?
If you want to generate a ModelA instance from ModelB and ModelCthen you should remove read_only=True argument and you have to explictly specify the relationship in create and update method.
class ModelBSerializer(serializers.ModelSerializer):
modelA_set = ModelASerializer() # this field name can be changed by adding related_name attribute in ModelA in models.py
class Meta:
model = ModelB
fields = ('modelA_set' , 'other_fields')
def create(self,validate_data):
modelA_data = validate_data.pop('modelA_data') # whatever field name you will use to send data for modelA
b = ModelB.objects.create(**validate_data)
for data in modelA_data:
modelA_instance = ModelA.objects.create(field_b=b ,**data)
return b
modelA_set is an automatically generated field name when we are trying to serialize ModelA from ModelB serializer . It can be changed by passing related_name='some_name' argument like this:
field_b = models.ForeignKey(ModelB, on_delete=models.CASCADE , related_name="field_a")
Then, will there be a serialized representation of field_b and field_c into ModelASerializer ?
Yes there will be.
Again correct me if I misunderstood your question.
I'm trying to query a specific column from my table. I've tried doing it with this
team_lenDeserialized = RolesInTeam.objects.values_list('user_id', flat=True).filter(academic_year_id=9).filter(deleted=0)
team_lenDict = RolesInTeamSerializer(team_lenDeserialized, many=True)
team_len = orderedDictToJSON(team_lenDict.data)
After that I run it through a function that converts it to JSON
def orderedDictToJSON(orderedDict):
return json.loads(json.dumps(orderedDict))
then I go and manipulate it further. However if I try to serialize and convert the team_lenDeserialized I get an error that states
AttributeError: Got AttributeError when attempting to get a value for field `user_id` on
serializer RolesInTeamSerializer`.
The serializer field might be named incorrectly and not match any
attribute or key on the `int` instance.
Original exception text was: 'int' object has no attribute 'user_id'.
This is my model for that table
class RolesInTeam(models.Model):
user_id = models.IntegerField()
team_id = models.IntegerField()
role_id = models.IntegerField()
deleted = models.IntegerField()
academic_year_id = models.IntegerField()
class Meta:
managed = False
db_table = 'roles_in_team'
and my serializer
class RolesInTeamSerializer(serializers.ModelSerializer):
class Meta:
model = RolesInTeam
fields = ['id', 'user_id', 'team_id', 'role_id', 'deleted', 'academic_year_id']
I have no clue what's happening or why it's not working.
You can only serialize models instances with a ModelSerializer, and values_list() returns a queryset of tuples, so when you try to use the serializer over the queryset, you get the error.
If you make a regular query (team_lenDeserialized = RolesInTeam.objects.filter(academic_year_id=9).filter(deleted=0)), you would be able to serialize team_lenDeserialized.
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
I have this code
# Models
class NestedSample(models.Model):
something = models.CharField(max_length=255)
class Sample(models.Model):
thing = models.BooleanField()
nested = models.ForeignKey(NestedSample)
# Serializers
class NestedSampleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = api_models.NestedSample
class SampleSerializer(serializers.HyperlinkedModelSerializer):
nested = NestedSampleSerializer() # HERE filter delete=false
nested2 = NestedSample2Serializer() # HERE filter deletefalse
class Meta:
model = api_models.Sample
In my view I am overrding the queryset for delete=False but it is not applying to nested serializers.
delete=False in queryset will only filter Sample. To filter queryset in nested serializer you can use serializers.ListSerializer like:
class FilterDeleteListSerializer(serializers.ListSerializer):
def to_representation(self, data):
data = data.filter(delete=False)
return super(FilterDeleteListSerializer, self).to_representation(data)
class NestedSampleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = api_models.NestedSample
list_serializer_class = FilterDeleteListSerializer
class NestedSample2Serializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = api_models.NestedSample2
list_serializer_class = FilterDeleteListSerializer
class SampleSerializer(serializers.HyperlinkedModelSerializer):
nested = NestedSampleSerializer() # HERE filter delete=false
nested2 = NestedSample2Serializer() # HERE filter deletefalse
class Meta:
model = api_models.Sample
Learn more here
I didn't exactly understand your question, but from what I figured you've got a boolean field in your Model which is set to True if you delete the object instead of actually deleting it from the database (SQL DELETE).
Now coming to your question, if you just want to filter the nested serializer then you could use the SerializerMethodField. You need to specify the method to call as an argument or add a method with the name 'get_' followed by the field name. In this method you can filter the queryset serialize it and return the data of that queryset.
class UserSerializer(serializers.ModelSerializer):
delete_filtered_items = serializers.SerializerMethodField()
class Meta:
model = User
def get_delete_filtered_items(self, obj):
items = Item.objects.filter(user=obj,deleted=False)
serializer = ItemsSerializer(instance=items, many=True)
return serializer.data
The above solution should work for your requirements, but if what you've implemented is similar to a soft delete then it would seem cleaner and more moduler to create a custom model manager.