drf-spectacular: how to show the primary key in examples section of Swagger - django-rest-framework

I'm trying to show the primary key in the examples section of Swagger, I'm using drf-spectacular and my code looks like:
Serializers.py
class SerializerExample(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('id','name')
Views.py
class BooksBulkUpdate(APIView):
#extend_schema(
request=SerializerExample(many=True),
responses={200:''},
)
def put(self, request, format=None):
with transaction.atomic():
for data in request.data:
book = Book.objects.get(pk=data['id'])
serializer = SerializerExample(book, data=data, partial=True)
if serializer.is_valid():
serializer.save()
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response()
Only the name field is showing:
The only solution that I found was using an inline serializer which is not the ideal solution because if I update my book serializer I'd have to remember to update also this inline serializer. I wonder if there is a better way of doing this.

AFAIK swagger shows input request schema.
For example, you want to add new person and your model is
class Person(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=30)
So you allowed to set only name parameter
Even if you post
{
"id": "someUUID",
"name": "NAME",
}
id will be ignored and Django create it automatically by own logic (because it is read only)
But you can set id field writeable:
class SerializerExample(serializers.ModelSerializer):
id = serializers.UUIDField(write_only=True)
name = serializers.CharField(write_only=True)
class Meta:
model = Person
fields = ('id','name')
write_only=True means that field will be active when you saving new data and receiving id from request json.
In opposite read_only=True will print id field at response (if you trying get data) but ignore it when you saving new data.
So you try to describe API for data adding, and of course that is not allow to set id field in request json.
Not sure if this theory applicable to your case, but hope that will be helpful.

Related

How to serialize multiple query set to json Response in django rest frame work

I am getting a list of committees by user using a get method in which I am sending a user id but I am getting an error Committee object is not serialize I have serializer created but I dont't know how to serializer the queryset of that particular user id result.
below is my views.py file
def get(self, request, user_id):
get_committees = Committee.objects.filter(user=Profile.objects.get(id=user_id))
data = {
"status": "success",
"data":get_committees
}
res = Response(serializer.data, status=status.HTTP_200_OK)
below is my serializer.py
class MyCommitteesSerializer(serializers.ModelSerializer):
def get_queryset(self, user_id):
my_committees =
Committee.objects.filter(user=Profile.objects.get(id=user_id))
return my_committees
from your code in get method
"data":get_committees
this get_committees is a list and you are trying to return a list but you can only return json, and so you are getting the message.
You can do this in a better way
define your serializer as -
class MyCommitteesSerializer(serializers.ModelSerializer):
class Meta:
model = Committee
fields = ""_all__"
and define your views as -
class MyCommitteesView(generics.ListAPIView):
serializer_class = MyCommitteesSerializer
def get_queryset(self):
queryset = Committee.objects.filter(user=Profile.objects.get(id=self.request.user_id))
return queryset
the serializer will take care of serialization and you can customize this according to your needs.
or else you will have to manually convert your get_committes into json format before returning.

DRF Filter PrimaryKeyField Based on Current User

I have a view set up to return a list of books to a user, which is retrieved from a simple book model based on the currently logged-in user. However, I also have ReadingSession model which has a foreign key relationship to both the Book, and the User.
When I'm retrieving the books for the user, I'd like to, at the very least, return a list of primary keys that I can use to get the length of in my client.
The following code will get the full set of readingsessions in my BookSerializer:
from rest_framework import serializers
from books.models import Book
class BookSerializer(serializers.ModelSerializer):
readingsession_set = serializers.PrimaryKeyRelatedField(
many=True, read_only=True)
class Meta:
model = Book
fields = ["id", "title", "author", "publisher",
"publish_date", "description", "category",
"language", "small_thumbnail", "thumbnail",
"readingsession_set"]
However, the problem with this is that it will return all of the readingsessions, regardless of whether or not the session belongs to that user.
I'd like to be able to filter that so that it will only return the readingsessions for the current user. Something along the lines of:
readingsession_set = serializers.PrimaryKeyRelatedField(queryset=ReadingSession.objects.filter(user=user), read_only=True)
But I've tried various ways of trying to pass the user (self.request.user) from the APIView but none seem to work. I've tried passing a context, and tried passing extra **kwargs in __init__ but none seem to work.
Is there a way of achieving this? Or am I taking the wrong approach?
Thanks
The user is not present on the serializer's declaration but during its instantiation.
Therefore, you can filter querysets by user within the __init__ method.
from rest_framework import serializers
from bar.models import Foo
class RandomSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
user_foos = Foo.objects.filter(user=self._user)
self.fields['foo_ids'] = serializers.PrimaryKeyRelatedField(
required=False,
many=True,
read_only=False,
queryset=user_foos,
default=user_foos)
#property
def _user(self):
request = self.context.get('request', None)
if request:
return request.user
Don't forget to pass the request object to the serializer in the context (if necessary, e.g., using a simple APIView.
from rest_framework import views
class RandomView(views.APIView):
serializer_class = RandomSerializer
def post(self, request):
serializer = self.serializer_class(
data=request.data, context={'request': request})
# ...
serializer = RandomSerializer(data=request.data, context={'request': request}
You can access the user of the request on the serializer by means of the context.
As mentioned in the documentation, you can always do:
serializer = AccountSerializer(account, context={'request': request})
Thus, you will be able to use self.context['request'].user inside your serializer.
Hope that's what you're after.

Django: customizing the field types needed for create and retrieve serializers

I currently have the following serializer:
serializers.py
class SurfGroupSerializer(serializers.ModelSerializer):
instructor = SurfInstructorSerializer(many=False)
surfers = SurferSerializer(many=True)
class Meta:
model = SurfGroup
fields = ['uuid', 'instructor', 'date', 'starting_time', 'ending_time', 'surfers']
def create(self, validated_data):
return SurfGroup(**validated_data)
And the following viewset create method (viewset inherited from viewsets.ViewSet as we need some bespoke customization, extra signals and actions etc):
viewsets.py
# Surf Group Create View:
def create(self, request, format=None):
serializer = SurfGroupSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
response = responses.standardized_json_response(
message='Surf Group Objects Have Been Successfully Created',
data=serializer.data
)
return Response(data=response, status=status.HTTP_201_CREATED, headers=headers)
For the retrieve action, the serializer works well, and we have a nested instructor object in the response. However, I want to perform a create by passing in the instructor uuid attrbiute like (see content in the POST textarea):
Rather than a whole object...I was wondering how we achieve this? Is it best to just have two Serializers, one for performing the create, and one the retrieval?
def create(self, validated_data):
surf_group = SurfGroup(
instructor__uuid=validated_data['instructor'],
)
surf_group.save()
return surf_group
It is good question.
I work with this situations many times and it looks like one option is to have two serializers as you mean: 1 for list/retrieve and 1 for save.
Another option (for me) is to set serializer field input as UUID and output as another serializer data like this:
class SurfGroupSerializer(serializers.ModelSerializer):
instructor = serializers.UUIDField()
surfers = SurferSerializer(many=True, read_only=True)
class Meta:
model = SurfGroup
fields = ['uuid', 'instructor', 'date', 'starting_time', 'ending_time', 'surfers']
# I use this validate method to transform uuid to object which will
# be bypassed to create method for easly save
def validate_instructor(self, instructor_uuid):
try:
return Instructor.objects.get(uuid=instructor_uuid)
except Instructor.DoesNotExist:
# Remember that you dont need to pass field_key: [errors] to ValidationError
# because validate_FIELD will automatically pass field_key for you !
raise ValidationError(['Instructor with the given uuid does not exist.'])
# Overwrite output data
def to_representation(self, instance):
ret = super().to_representation(instance)
ret['instructor'] = SurfInstructorSerializer(instance=instance.instructor).data
return ret

Django REST framework restrict posting and browsable Api-fields

I use the Django Rest framework together with an JavaScript app. I have some difficulties to get the posting of new data items right with the generic ModelViewSet
Most importantly I want to restrict what a poster can submit
(they should only be allowed to post items that have the user_id of this user (the authenticated user of the session).
I don't know when/where I should check for this? Is this a validation problem?
How I understand the permission classes is that they restrict the method (Post/Get) or check for user groups.
Also my user field in the item model is a foreign key to the user model
so the browsable api suggest in the Html-form a dropdown with the information about other users. (their email adresses and some other fields).
My data items look like this
[{
"id": 792,
"name": "test",
"category": 1,
"value": 5,
"user": "33"
}]
Here is my Serializer and the Viewset:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = ('id',
'name',
'category',
'value',
'user',
)
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
I can't believe this issue with the DRF Create/Update (Post/Put) form isn't more widely discussed.
It's a huge data privacy issue - e.g. One can restrict the List API view to only show items owned by a User via overriding the get_queryset method inside as below:
# views.py
class ItemViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
But as OP notes, when accessing the API Create/Post or Update/Put form for the ItemViewSet, there is seemingly no easy way to restrict the user options to the user itself.
I had a similar issue myself building a survey platform, where I want to restrict choice of survey/question/options etc. to those owned by the user, and prevent users from inadvertently seeing each other's data.
Jocelyn's answer works for the OP's particular situation where we already know that the Item.user must equal request.user, so we override this on the perform_create method.
But Jocelyn's solution is insufficient for situations where you do not know in advance what the relationship between model instances will be (e.g. in my case where a new question objected could be added to any one of a user's surveys).
The solution I came up with was the nuclear option: do away with the Viewset altogether for Create and Update functionality, and use a set of custom views.APIView classes instead, as below (adapted for the case of the OP, only showing Create).
class ItemCreateView(views.APIView):
def post(self, request, format=None):
post_user_id = int(request.data['user'].split('/')[-2])
request_user_id = request.user.id
serializer = ItemSerializer(data=request.data, context={'request': request})
if post_user_id == request_user_id:
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response('Not Allowed: Owner is not User', status=status.HTTP_401_UNAUTHORIZED)
Please note, I'm using a HyperlinkedModelSerializer rather than a plain ModelSerializer, hence the need for .split('/')[-2] to grab the post_user_id
Handling the user field
First set the user field to be readonly:
# serializers.py
class ItemSerializer(serializers.ModelSerializer):
user = serializers.ReadOnlyField()
class Meta:
model = Item
fields = ('id',
'name',
'category',
'value',
'user',
)
Then auto-set the user id on creation:
# views.py
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user.customer)
Handling permissions
Just use standard permissions mechanism to define a custom one :
# permissions.py
from rest_framework import permissions
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return (request.user.is_authenticated() and
(obj.user == request.user.customer))
...and use it in your viewset :
# views.py
from permissions import IsOwner
class ItemViewSet(viewsets.ModelViewSet):
permission_classes = [IsOwner]
...

Model Serializer : choose which fields to display and add custom fields

Let's say I have this simple model :
class BlogPost(models.Model):
author = models.ForeignKey(MyUser)
body = models.TextField()
title = models.CharField(max_length=64)
urlid = models.CharField(max_length=32)
private_data = models.CharField(max_length=64)
private_data contains data that I do not want to expose to the API (!). I'm using a ModelSerializer :
class BlogPostSerializer(serializers.ModelSerializer):
class Meta:
model = BlogPost
def __init__(self, *args, **kwargs):
# Don't pass the 'request' arg up to the superclass
request = kwargs.pop('request', None)
# Instatiate the superclass normally
super(ModelSerializer, self).__init__(*args, **kwargs)
self.request = request
def absolute_url(self, blogpost):
return blogpost.get_absolute_url(self.request)
The absolute_url method needs the request to determine the domain name (dev or prod for example) and if it was made in http or https.
I want to specify which fields in the model are going to get returned by the serializer (not expose private_data for example). Simple enough:
class BlogPostSerializer(serializers.ModelSerializer):
class Meta:
model = BlogPost
fields = ('author', 'body', 'title', 'urlid',)
# The same jazz after that
All right, it works. Now I also want to return absoluteUrl:
class BlogPostSerializer(serializers.ModelSerializer):
absoluteUrl = serializers.SerializerMethodField('absolute_url')
class Meta:
model = BlogPost
fields = ('author', 'body', 'title', 'urlid',)
# The same jazz after that
Well, without surprises, this returns only the fields I specified, without the absoluteUrl. How can I return only certain fields of the model AND the absoluteUrl, calculated from the serializer?
If I don't specify fields I do get the absoluteUrl, but with all the model's fields (including private_data). If I add 'absoluteUrl' to fields I get an error because blogpost.absoluteUrl doesn't exist (no surprises there). I don't think I could use this method http://django-rest-framework.org/api-guide/serializers.html#specifying-fields-explicitly because I need the request to obtain the absoluteUrl (or can I specify arguments to the model's method ?)
If I don't specify fields I do get the absoluteUrl, but with all the model's fields (including private_data). If I add 'absoluteUrl' to fields I get an error because blogpost.absoluteUrl doesn't exist (no surprises there).
You should just be adding 'absoluteUrl' to the fields tuple, and it should work just fine - so what error are you seeing?
The absolute_url method needs the request to determine the domain name (dev or prod for example) and if it was made in http or https.
Note that you can also pass through context to the serializer without modfiying the __init__, just pass a context={'request': request} when instantiating the serializer. The default set of generic views do this for you, so you can access self.context['request'] in any of the serializer methods. (Note that this is how hyperlinked relationships are able to return fully qualified URLs)

Resources