So I am relatively new to Django, and DRF. I recently started a project to learn more about the platform.
I also found best practice guide line which I am following at the moment. Here is the Django Style Guide
The guild line says that every App should completely decouple itself and use UUID instead of foreign key,
And every business rule should go into services.py (Which I like btw.)
Here comes the problem. I am not sure how I can construct two models together to produce a nested JSON Output.
Ex: I have Post() model and Comments() model. and Comment model uses uuid of Post model as a referential id.
Now in my API.py, I am not sure how I can join these two together in the Serializer.py
FYI the code below is only for demo purposes, may not be executable
Model
class Post(models.Model):
user_id = models.UUIDField(default=uuid.uuid4)
title = models.CharField(max_length=100)
description = models.CharField(max_length=100)
pictures = models.CharField(max_length=100)
lat = models.CharField(max_length=16)
long = models.CharField(max_length=16)
vote = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now_add=True)
class Comment(models.Model):
post_id = models.UUIDField(default=uuid.uuid4)
parent_id = models.ForeignKey("self", on_delete=models.CASCADE)
text = models.CharField(max_length=1000)
up_vote = models.IntegerField()
down_vote = models.IntegerField()
user_id = models.UUIDField(default=uuid.uuid4)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now_add=True)
Service
def get_report(id) -> Dict:
logger.info('Get an Report by Id')
post= Post.objects.get(id=id)
return {
'post'= post
}
def get_comments(id) -> Dict:
logger.info('Get an Report by Id')
comments = Comment.objects.filter(post_id=id)
return {
'comments' = comments
}
API
class ReportGetApi(APIView):
class OutputSerializer(serializers.ModelSerializer):
comments = CommentsSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ('id', 'title', 'description', 'pictures', 'lat', 'long', 'vote', 'comments')
class CommentsSerializer(serializers.ModelSerializer):
class Meta:
model = Comments
fields = ('post_id', 'parent_id', 'text', 'up_vote', 'down_vote', 'user_id', 'created_at', 'modified_at')
def get(self, request):
post = PostService.get_post() #Only one item
comments = PostService.get_comments() #Many Items
serializer = self.OutputSerializer(post, many=True)
return Response(serializer.data)
You actually don't need a services file to do what you're asking. DRF can handle what you want directly with serializers and using a RetrieveAPIView.
So using your models you have above you could have something like the following:
serializers.py
from rest_framework import serializers
from . import models
class CommentSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True)
class Meta:
model = models.Comment
fields = '__all__'
class PostSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True)
comments = serializers.SerializerMethodField()
class Meta:
model = models.Post
fields = '__all__'
def get_comments(self, obj):
comments = models.Comment.objects.filter(post_id=obj.id)
return CommentSerializer(comments, many=True).data
views.py
from rest_framework import generics
from . import serializers, models
class ReportApi(generics.RetrieveAPIView):
serializer_class = serializers.PostSerializer
queryset = models.Post.objects.all()
You would need to specify the URL to pass in the primary key of the Post object you want to retrieve like so:
urls.py
from django.urls import path
from api import views
app_name = 'api'
urlpatterns = [
path('get_post_report/<pk>/', views.ReportApi.as_view()),
]
Then you would access the view using something like http://example.com/api/get_post_report/12345678/.
Note: You must configure the urls.py within your project's
urls file to use 'api/' for including your app's urls for the
'/api/' part of the url above to be a part of the url.
If you don't know how to set up urls refer to the Django Tutorial
This will then give you something like the following:
response.json
{
"id": 1,
"comments": [
{
"id": 1,
"post_id": "00000000-0000-0000-0000-000000000001",
"text": "Comment Text",
"up_vote": 0,
"down_vote": 0,
"user_id": "00000000-0000-0000-0000-000000000001",
"created_at": "2020-05-29T13:14:07.103072Z",
"modified_at": "2020-05-29T13:14:07.103124Z",
"parent_id": null
}
],
"user_id": "00000000-0000-0000-0000-000000000001",
"title": "My Post",
"description": "Post Description",
"pictures": "No Pictures",
"lat": "12",
"long": "12",
"vote": "12",
"created_at": "2020-05-29T13:14:07.102316Z",
"modified_at": "2020-05-29T13:14:07.102356Z"
}
Lastly
I looked at the guide you are referring to and did not see a reference to decouple models with UUID's. While you can do that (and in some cases may be necessary) I would think hard on whether you truly need that much decoupling.
There are reasons to use foreign keys over UUID's such as accessing the related models easier and faster. Decoupling using UUID's means you will need to write more boilerplate code every time you need to access a related model.
It comes down to deciding whether you need decoupling or a better developer experience with (subjectively) cleaner code. Please don't just follow a guide and assume it's law. More experience will help with this.
For example, if you used a foreign key relationship your PostSerializer could look like the following:
example_serializers.py
class PostSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True)
comments = CommentSerializer(many=True)
class Meta:
model = models.Post
fields = '__all__'
See how we got rid of the get_comments(self, obj) method? That was just one bit of boilerplate that we got rid of by making the design decision to use foreign key relationships. Now just imagine a codebase of millions of lines of code and many serializers. Remember, the more code you write the more testing/debugging you need to do as well.
Again, just my opinion, but be sure you actually need to decouple your models before you do it.
Also, I strongly recommend you follow the DRF tutorial. It reviews everything you need to accomplish what I just posted here.
Hope this helps!
You could create one serializer called OutputSerializer and include the comments as its own field? if you change comments = OutputCommentSerializer to comments = CommentsSerializer(many=True, read_only=True) I believe that will depict what you want...(example below). You already defined CommentSerializer, thus you can use it.
class ReportGetApi(APIView):
class OutputSerializer(serializers.ModelSerializer):
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ('id', 'title', 'description', 'pictures', 'lat', 'long', 'vote', 'comments')
class CommentsSerializer(serializers.ModelSerializer):
class Meta:
model = Comments
fields = ('post_id', 'parent_id', 'text', 'up_vote', 'down_vote', 'user_id', 'created_at', 'modified_at')
def get(self, request):
post = PostService.get_post() #Only one item
comments = PostService.get_comments() #Many Items
serializer = self.OutputSerializer(post, many=True)
return Response(serializer.data)
Related
I have created 2 models -
Tags and Startups. Startups has a tags field with a ManytoMany relationship with Tag.
Models.py file -
from django.db import models
from django_extensions.db.fields import AutoSlugField
from django.db.models import CharField, TextField, DateField, EmailField, ManyToManyField
class Tag(models.Model):
name = CharField(max_length=31, unique=True, default="tag-django")
slug = AutoSlugField(max_length=31, unique=True, populate_from=["name"])
def __str__(self):
return self.name
class Startup(models.Model):
name = CharField(max_length=31, db_index=True)
slug = AutoSlugField(max_length=31, unique=True, populate_from=["name"])
description = TextField()
date_founded = DateField(auto_now_add=True)
contact = EmailField()
tags = ManyToManyField(Tag, related_name="tags")
class Meta:
get_latest_by = ["date_founded"]
def __str__(self):
return self.name
My serializers.py file -
from rest_framework.serializers import HyperlinkedModelSerializer, PrimaryKeyRelatedField, ModelSerializer
from .models import Startup, Tag
class TagSerializer(HyperlinkedModelSerializer):
class Meta:
model = Tag
fields = "__all__"
extra_kwargs = {
"url": {
"lookup_field": "slug",
"view_name": "tag-api-detail"
}
}
class StartupSerializer(HyperlinkedModelSerializer):
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Startup
fields = "__all__"
extra_kwargs = {
"url": {
"lookup_field": "slug",
"view_name": "startup-api-detail"
}
}
My viewsets.py file -
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from .serializers import TagSerializer, StartupSerializer
from .models import Tag, Startup
from rest_framework.decorators import action
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_200_OK, HTTP_204_NO_CONTENT
from django.shortcuts import get_object_or_404
class TagViewSet(ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
lookup_field = "slug"
class StartupViewSet(ModelViewSet):
serializer_class = StartupSerializer
queryset = Startup.objects.all()
lookup_field = "slug"
#action(detail=True, methods=["HEAD", "GET", "POST"], url_path="tags")
def tags(self, request, slug=None):
startup = self.get_object()
print(startup)
if request.method in ("HEAD", "GET"):
s_tag = TagSerializer(
startup.tags,
many=True,
context={"request": request}
)
return Response(s_tag.data)
tag_slug = request.data.get("slug")
if not tag_slug:
return Response(
"Slug of Tag must be specified",
status=HTTP_400_BAD_REQUEST
)
tag = get_object_or_404(Tag, slug__iexact=tag_slug)
startup.tags.add(tag)
return Response(HTTP_204_NO_CONTENT)
I can create startups and relate tags through my django admin.
I have a dropdown list with all the created tags in my django admin from where I can relate tags with startups.
I do not understand how can I relate tags with startup when creating startup in react.
I tried posting data in this form -
{
"name": "Test Startup 1",
"description": "First Desc",
"contact": "first#gmail.com",
"tags": [
{
"url": "http://127.0.0.1:8000/api/v1/tag/first-tag/",
"name": "First Tag",
"slug": "first-tag"
}
]
}
I cannot get the tags to relate with startup.
How to handle related fields?
You have multiple options.
If you want to perform association (meaning tags and startup already exists before making the request, kinda what happen with django-admin), you could create a new serializer that have a different field for tags, accepting ids instead of the nested serializer.
If you want to have nested creation/edition, you could checkout WritableNestedSerializer from here. Because the doc says that it does not handle such usecase, because they might be many way to perform this depending on your business logic, but provide ways to perform that yourself here
Another approach would be to have a route with nested ressources (with nested routers for instance) so when you POST a tag in /startup/1/tags/ you create AND associate your tag automatically, like you did.
Now, concerning your endpoint, you need to get the data of your request and pass it to the tag serializer. This serializer will then validate your data, and if it is valid, you can perform tag creation.
To do that, you can do something like:
tag_data = request.data
tag_serializer = TagSerializer(data=request.data)
tag_serializer.is_valid()
tag = tag_serializer.save()
tag.startup_set.add(startup)
Adding the relationship have to be done in two step. You should use a transaction to ensure it is correctly created.
Also, instead of adding this logic in your view, you should override your TagSerializer/StartupSerializer create method to do that.
I've been trying hard but I can't find a solution for this, maybe you can help me:
I have 2 models: consumption and message.
The messages have a field "consumption" so I can know the consumption related.
Now I'm using DRF to get all the consumptions but I need all the messages related with them
This is my serializers.py file:
class ConsumptionSapSerializer(serializers.HyperlinkedModelSerializer):
client = ClientSerializer(many=False, read_only=True, allow_null=True)
call = CallSerializer(many=False, read_only=True, allow_null=True)
course = CourseSerializer(many=False, read_only=True, allow_null=True)
provider = ProviderSerializer(many=False, read_only=True, allow_null=True)
# messages = this is where I failed
class Meta:
model = Consumption
fields = [
"id",
"client",
"course",
"provider",
"user_code",
"user_name",
"access_date",
"call",
"speciality_code",
"expedient",
"action_number",
"group_number",
"start_date",
"end_date",
"billable",
"added_time",
"next_month",
"status",
"incidented",
"messages"
]
class MessageSapSerializer(serializers.ModelSerializer):
user = UserSerializer(allow_null=True)
class Meta:
model = Message
fields = [
"user",
"date",
"content",
"read",
"deleted"
]
I have read here Django REST Framework: adding additional field to ModelSerializer that I can make a method but I don't have the consumption ID to get all the messages related.
Any clue?
There is related_name that you can set in Message.consumption in models.py in order to call consumption.messages.
models.py
class Consumption(models.Model):
# some fields here
class Message(models.Model):
consumption = models.ForeignKey(Consumption, ...., related_name="messages")
P.S. Since you didn't show your models.py, I am just assuming Message.consumption is a ForeignKey.
More explanation on related_name here.
class Schedule(models.Model):
name = models.CharField(max_length=50)
class ScheduleDefinition(models.Model):
schedule = models.ForeignKey(Schedule, on_delete=models.DO_NOTHING)
config = JSONField(default=dict, blank=True)
These are my models. I am trying to create a new ScheduleDefinition(The Schedule already exists and I know the ID I want to use for my foreign_key). I have a predefined Schedule id that I want to use, but it is not working..
Posting this body:
{
"schedule_id": 1,
"config": {
"CCC": "ccc"
}
}
Error I get:
null value in column "schedule_id" violates not-null constraint
What am I doing wrong? When I create new ScheduleDefinition models, the Schedule model will already be created previously. I am never going to be creating new Schedule's when I create new ScheduleDefinition's.
Serializer:
class ScheduleSerializer(serializers.ModelSerializer):
class Meta:
model = Schedule
fields = ['id', 'name']
class ScheduleDefinitionSerializer(serializers.ModelSerializer):
schedule = ScheduleSerializer(read_only=True, many=False)
class Meta:
model = ScheduleDefinition
fields = ['schedule', 'config']
View:
from rest_framework import generics
from .models import Schedule, ScheduleDefinition
from .serializers import ScheduleSerializer, ScheduleDefinitionSerializer
class ScheduleList(generics.ListAPIView):
queryset = Schedule.objects.all()
serializer_class = ScheduleSerializer
class ScheduleDefinitionList(generics.ListCreateAPIView):
queryset = ScheduleDefinition.objects.all()
serializer_class = ScheduleDefinitionSerializer
class ScheduleDefinitionDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = ScheduleDefinition.objects.all()
serializer_class = ScheduleDefinitionSerializer
View error:
File "/app/server/schedules/serializers.py", line 13, in ScheduleDefinitionSerializer
schedule_id = serializers.PrimaryKeyRelatedField(source="schedule")
File "/usr/local/lib/python3.7/dist-packages/rest_framework/relations.py", line 247, in __init__
super().__init__(**kwargs)
File "/usr/local/lib/python3.7/dist-packages/rest_framework/relations.py", line 108, in __init__
'Relational field must provide a `queryset` argument, '
AssertionError: Relational field must provide a `queryset` argument, override `get_queryset`, or set read_only=`True`.
You've specified Schedule as required (not null), but you aren't actually posting any information to it. Currently your serializer is expecting information in the form:
{
"schedule": {
"name": "Foo"
},
"config": {...}
}
schedule_id is being discarded when you post. Furthermore, you've specified that schedule is a read only field, meaning even if you posted a schedule, it would still be discarded.
If you'd like to post to foreign keys, you'll either need to specially handle it by manually writing the create/update logic for a writable nested serializer (which can be a bit of a hassle), or use a different (writable) foreignkey field serializer and serialize your other read-only data another way.
For example, the following setup should work (untested) with the data you're currently trying to POST:
class ScheduleDefinitionSerializer(serializers.ModelSerializer):
schedule = serializers.PrimaryKeyRelatedField(
queryset=Schedule.objects.all()
)
schedule_name = serializers.CharField(read_only=True, source="schedule.name")
class Meta:
model = ScheduleDefinition
fields = ['schedule', 'schedule_name', 'config']
With this your post should work, and you'll still have read-only access to the corresponding schedule's name via the schedule_name field in your list/detail views.
EDIT
My earlier version of the code would not have been compatible with the original desired POST data. The following should work without altering the POST
class ScheduleDefinitionSerializer(serializers.ModelSerializer):
schedule = ScheduleSerializer(many=False, read_only=True)
schedule_id = serializers.PrimaryKeyRelatedField(
source="schedule",
queryset=Schedule.objects.all()
)
class Meta:
model = ScheduleDefinition
fields = ['schedule', 'schedule_id', 'config']
I've been looking and following the Django-Rest Official docs here and tried to base my approach on that, except I was using a OneToOne relationship.
However, when I go to add in the browsable interface, it doesn't pick up the value in the nested text fields, and tells me that it can't be null. I've tried Googling and searching on here, but can't really find anything that works for getting a OneToOne relationship to work like what I want. I'm new to the REST-framework, and am just really confused. Thanks!
Basically, each verb needs to have one past tense object, which has those three fields (for testing, more, or even another layer of nesting, will be added later). I just can't get them to add with the browsable API.
Models.py:
from django.db import models
class Verb(models.Model):
verb = models.TextField()
verbal_noun = models.TextField()
verbal_adjective = models.TextField()
present = models.TextField()
future = models.TextField()
habitual_present = models.TextField()
conditional = models.TextField()
past_habitual = models.TextField()
past_subjunctive = models.TextField()
present_subjunctive = models.TextField()
imperative = models.TextField()
class Past(models.Model):
verb = models.OneToOneField(Verb)
first_singular = models.TextField()
second_singular = models.TextField()
third_singular = models.TextField()
Serializers.py:
from rest_framework import serializers
from conjugations.models import Verb, Past
class PastSerializer(serializers.ModelSerializer):
class Meta:
model = Past
fields = ('first_singular','second_singular','third_singular')
class VerbSerializer(serializers.ModelSerializer):
past = PastSerializer()
class Meta:
model = Verb
fields = ('verb','verbal_noun','verbal_adjective','past','present',
'future','habitual_present','conditional','past_habitual',
'past_subjunctive','present_subjunctive','imperative' )
def create(self, validated_data):
past_data = validated_data.pop('past')
verb = Verb.objects.create(**validated_data)
for past in past_data:
Past.objects.create(verb=verb, **past)
return verb
Raw data input:
{
"verb": "test",
"verbal_noun": "test",
"verbal_adjective": "test",
"past": {
"first_singular": "test1",
"second_singular": "test2",
"third_singular": "test3"
},
"present": "test",
"future": "test",
"habitual_present": "test",
"conditional": "test",
"past_habitual": "test",
"past_subjunctive": "test",
"present_subjunctive": "test",
"imperative": "test"
}
Views.py
from conjugations.models import Verb
from conjugations.serializers import VerbSerializer
from rest_framework import generics, permissions
class VerbList(generics.ListCreateAPIView):
queryset = Verb.objects.all()
serializer_class = VerbSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class VerbDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Verb.objects.all()
serializer_class = VerbSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
Urls.py
from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from conjugations import views
urlpatterns = [
url(r'^verbs/$', views.VerbList.as_view()),
url(r'^verbs/(?P<pk>[0-9]+)/$', views.VerbDetail.as_view()),
]
urlpatterns = format_suffix_patterns(urlpatterns)
Error:
File "/home/Projects/Python/virtualenvs/remnigh/lib/python3.5/site-packages/rest_framework/mixins.py", line 26, in perform_create
serializer.save()
File "/home/Projects/Python/virtualenvs/remnigh/lib/python3.5/site-packages/rest_framework/serializers.py", line 191, in save
self.instance = self.create(validated_data)
File "/home/Projects/Python/reimnigh-test/reimnigh/conjugations/serializers.py", line 19, in create
past_data = validated_data.pop('past')
KeyError: 'past'
My serializers.py was messed up a little. I needed to remove the for loop for past_data since it was a one-to-one and just map that directly. Corrected file below:
from rest_framework import serializers
from conjugations.models import Verb, Past
class PastSerializer(serializers.ModelSerializer):
class Meta:
model = Past
fields = ('first_singular','second_singular','third_singular')
class VerbSerializer(serializers.ModelSerializer):
past = PastSerializer()
class Meta:
model = Verb
fields = ('verb','verbal_noun','verbal_adjective','past','present',
'future','habitual_present','conditional','past_habitual',
'past_subjunctive','present_subjunctive','imperative')
def create(self, validated_data):
past_data = validated_data.pop('past')
verb = Verb.objects.create(**validated_data)
Past.objects.create(verb=verb, **past_data)
return verb
It also helps to make sure your migrations are up to date too. That might have had a little bit to do with it.
Make sure your request is performed as json content type. HTML form don't support nested serializers.
Try changing your past field in the VerbSerializer to this:
past = PastSerializer(source='past_set')
This is syntax you could use if the Past-Verb relationship was a foreign key- I can't say for sure if it will work for you, but the DRF documentation says that for reverse relationships to be nested, a related name must be specified, either as above or in the model definition.
If this doesn't work, could you post the exact error/traceback?
So, I have this 2 classes of servers and datacenters;
class Datacenter(models.Model):
name = models.CharField(max_length=50)
status = models.CharField(max_length=50)
def __unicode__(self):
return self.name
class Servers(models.Model):
datacenter = models.ForeignKey(Datacenter)
hostname = models.CharField(max_length=50)
def __unicode__(self):
return self.hostname
And want to create a view that returns the details of the datacenter plus all the servers that are related, so right now when I do;
http://127.0.0.1:8000/datacenter/1/
I'm getting something like;
{
"id": 1,
"name": "TestDC"
}
But what I'm actually looking to get is something like this;
{
"id": 1,
"name": "TestDC",
"Servers": [
{
"id": 1,
"hostname": "Server1",
},
{
"id": 2,
"hostname": "Server2",
}
]
}
Right now my view is this;
class DatacenterViewSet(viewsets.ModelViewSet):
queryset = datacenter.objects.all()
serializer_class = datacenterSerializer
and my serialiazer;
class DatacenterSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Datacenter
fields = ('id','name')
I also would like to have that server list in an other method like;
http://127.0.0.1:8000/datacenter/1/Servers
Any suggestions?
Nested Servers:
If you want (almost) exactly the output you gave as a sample, then it would be this:
class ServersSerializer(serializers.ModelSerializer):
class Meta:
model = Servers
fields = ('id', 'hostname')
class DatacenterSerializer(serializers.ModelSerializer):
servers = ServersSerializer(source='servers_set')
class Meta:
model = Datacenter
fields = ('id', 'name')
If you want to show all fields for both models, then just drop the 'fields' line.
This could also work without the source keyword argument, but would require the related name to match the 'servers' property name (you could do this by adding related_name='servers' to the datacenter field on the Servers model).
The docs for DRF are pretty good, the bits you care about are serializer relations
Deep URL:
To achieve the nested URL structure, you could simply make an url pattern that matches the above like so:
url(r'^datacenter/(?P<datacenter_id>\d+)/Servers$', 'views.dc_servers',name="dc_servers")
which would call your view with the ID of the Datacenter as the kwarg datacenter_id. You would then use that ID to filter the queryset of your view by datacenter_id.
You'll have to look into how to write that view yourself, here are the views docs to get you started.
A couple of general Django tips: Models should usually have singular names rather than plural and adding a related_name argument is usually a good thing (explicit over implicit).
To show the Servers you can do this on the serializer:
class DatacenterSerializer(serializers.HyperlinkedModelSerializer):
servers = serializers.HyperlinkedRelatedField(
many=True
)
class Meta:
model = Datacenter
fields = ('id','name', 'servers')
If you want to show several fields of the servers, you should create a ServerSerializer with the fields that you want to show and then replace the line:
servers = serializers.HyperlinkedRelatedField(
many=True
)
With:
servers = ServerSerializer(many=True)
For further information have a look at the doc: http://www.django-rest-framework.org/api-guide/relations/
Thanks you both for your answers, finally all I had to do was add the related_name in the model, now it looks like this;
class Datacenter(models.Model):
name = models.CharField(max_length=50)
status = models.CharField(max_length=50)
def __unicode__(self):
return self.name
class Servers(models.Model):
datacenter = models.ForeignKey(Datacenter,related_name="servers")
hostname = models.CharField(max_length=50)
def __unicode__(self):
return self.hostname
Regarding the deep URL, I was checking the documentation and it should be possible to accomplish using a SimpleRouter, but couldn't find any example to see how to implement it; {prefix}/{lookup}/{methodname}/
http://www.django-rest-framework.org/api-guide/routers/