DRF docs - getting serializers dynamically - django-rest-framework

I have a viewset which uses different serializers for different HTTP methods (and possibly even for different users). DRF docs says this:
To be compatible with this behaviour methods (such as get_serializer or
get_serializer_class etc.) which inspect self.request or, particularly,
self.request.user may need to be adjusted to handle this case.
Can anyone provide me an example of the "adjustment to handle this case"?
Say I have a viewset which is using serializer A for get method for admins, serializer B for get method for all other users and serializer C for all other methods. How to specify this so that the documentation understands this? I am using the builtin one.
My urls.py:
router = DefaultRouter()
router.register('test', SampleViewset, base_name="test")
urlpatterns = [
path('admin/', admin.site.urls),
path('docs/', include_docs_urls(title='My API title', public=True)),
path('', include(router.urls)),
]

Minimal example
models.py
class SampleModel(models.Model):
name = models.CharField(max_length=120)
age = models.IntegerField()
address = models.CharField(max_length=100)
serializers.py
class SampleSerializer(serializers.ModelSerializer):
class Meta:
fields = '__all__'
model = SampleModel
class SampleSerializerCreate(serializers.ModelSerializer):
age = serializers.IntegerField(max_value=20)
class Meta:
fields = '__all__'
model = SampleModel
views.py
class SampleViewset(ModelViewSet):
serializer_class = SampleSerializer
queryset = SampleModel.objects.all()
def get_serializer_class(self):
if self.action=='create':
return SampleSerializerCreate
return SampleSerializer
Here I used a logic that, while creation of SampleModel, the age attribute should not exceed a max_value of 20.
Update-1
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'sample', SampleViewset)
urlpatterns = [
path('docs/', include_docs_urls(title='My API title', public=True, patterns=router.urls)),
] + router.urls

Related

How can I change HyperLinkedModelSerializer's default <pk> lookup_url_kwarg?

I want to use HyperLinkedModelSerializer in order to add a url field for my Book model. Here is the solution you'd typically find in the average tutorial:
# serializers.py
class BookSerializer(HyperLinkedModelSerializer):
class Meta:
model = Book
fields = ("title", "url",)
# views.py
class BookView(RetrieveAPIView):
serializer_class = BookSerializer
# urls.py
urlpatterns = [
path("<pk>/", BookDetailView.as_view(), name="book-detail"),
]
And that works all right. But now I need to change the URL conf in order to match the book id, not with <pk>, but with <fiction_id>. So I figured I'd just change it!
# urls.py
urlpatterns = [
path("<fiction_id>/", BookDetailView.as_view(), name="book-detail"),
]
Now comes the crash:
django.core.exceptions.ImproperlyConfigured: Could not resolve URL for hyperlinked relationship using view name "fiction-detail". You may have failed to include the related model in your API, or incorrectly configured the `lookup_field` attribute on this field.
I've tried fiddling with the lookup_field, lookup_url_kwargs in my view:
# views.py
class BookView(RetrieveAPIView):
serializer_class = BookSerializer
lookup_field = "pk"
lookup_url_kwargs = "fiction_id"
I've tried reminding the serializer to actually look for the (default) "pk":
# serializers.py
class BookSerializer(HyperLinkedModelSerializer):
class Meta:
model = Book
fields = ("title", "url",)
extra_kwargs = {
"url": {"lookup_field": "pk"},
}
I've tried combinations of these, to no avail. It looks like you can't use anything but <pk> if you want to take advantage of HyperLinkedModelSerializer's url field. The documentation doesn't seem to offer a way to change that behaviour:
By default hyperlinks are expected to correspond to a view name that matches the style '{model_name}-detail', and looks up the instance by a pk keyword argument.
How can I change this behaviour, or is it bound to become too messy?
You should check HyperlinkedModelSerializer implementation and see that it uses a serializer_related_field defaulting to HyperlinkedRelatedField
class HyperlinkedModelSerializer(ModelSerializer):
"""
A type of `ModelSerializer` that uses hyperlinked relationships instead
of primary key relationships. Specifically:
* A 'url' field is included instead of the 'id' field.
* Relationships to other instances are hyperlinks, instead of primary keys.
"""
serializer_related_field = HyperlinkedRelatedField
...
And then HyperlinkedRelatedField has a class attribute lookup_field defaulting to pk
class HyperlinkedRelatedField(RelatedField):
lookup_field = 'pk'
...
What you can do is to use a custom HyperlinkedRelatedField with your own lookup_field
from rest_framework.relations import HyperlinkedRelatedField
from rest_framework.serializers import HyperlinkedModelSerializer
class BookHyperlinkedRelatedField(HyperlinkedRelatedField):
lookup_field = 'fiction_id'
class BookSerializer(HyperLinkedModelSerializer):
serializer_related_field = BookHyperlinkedRelatedField
class Meta:
model = Book
fields = ("title", "url",)
In order to do this, you need to give the url field's new name for the lookup in the matched pattern by passing it through the extra_kwargs dictionary:
# serializers.py
class BookSerializer(HyperLinkedModelSerializer):
class Meta:
model = Book
fields = ("title", "url",)
extra_kwargs = {
"url": {"lookup_url_kwarg": "fiction_id"},
}
Also remember to modify the corresponding view:
# views.py
class BookView(RetrieveAPIView):
serializer_class = BookSerializer
lookup_url_kwarg = "fiction_id"
Do not write lookup_url_kwargs in plural.
You don't need to meddle with the lookup_field at any level as long as the lookup will be done on the model's primary key.

How to restrict fields when creating post request in DRF?

I am making a POST api using DRF. In that api, I need only few fields(name, size, customer_name, customer_address), but don't require this fields(status, ordered_time) because these fields I want to save these fields in run time as status='open' and ordered_time=DateTimeField.now()
views.py
class PizzaOrderCustomerView(APIView):
def post(self, request):
orders = request.data.get('orders')
# Create an article from the above data
serializer = ArticleSerializer(data=orders)
if serializer.is_valid(raise_exception=True):
article_saved = serializer.save()
return Response({"success": "Article '{}' created successfully".format(article_saved.name)})
models.py
class PizzaOrder(models.Model):
name = models.CharField(max_length=120)
size = models.CharField(max_length=10, choices=SIZE_CHOICE, default='MEDIUM')
customer_name = models.CharField(max_length=120)
customer_address = models.TextField()
ordered_time = models.DateTimeField(default=timezone.now, editable=False)
status = models.CharField(max_length=20, default='open', editable=False)
serializers.py
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = PizzaOrder
# fields = '__all__'
read_only_fields = ('status',)
But when I try to create an order, it needed status and ordered_time also. But it should save at the time of creating order automatically.
Suggest a good way to do it.
from rest_framework import viewsets, mixins
class PizzaViewsets(viewsets.ViewSet, mixins.CreateModelMixin):
model = PizzaOrder
serializer_class = OrderSerializer
queryset = model.objects.all(
serializer, it is always good practise to mention all fields instead of
all
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = PizzaOrder
fields = ('status','ordered_time','name', 'size', 'customer_name', 'customer_address',)
read_only_fields = ('status','ordered_time',)

Multiple endpoints for a single model in REST framework

I have a REST framework app for a multi-page form:
class InformationRequest(models.Model):
# user information
first_name = models.CharField(max_length=60)
last_name = models.CharField(max_length=60)
# contact details
phone = models.CharField(max_length=60)
email = models.CharField(max_length=60)
I'm trying to create endpoints for each of the two blocks of data within the model:
UserInformationSerializer(serializers.Serializer):
first_name = serializers.CharField(max_length=60)
last_name = serializers.CharField(max_length=60)
ContactDetailsSerializer(serializers.Serializer):
phone = serializers.CharField(max_length=60)
email = serializers.CharField(max_length=60)
I'd like the endpoints to look like:
requests/1/user-informtion
requests/1/contact-details
But I'm unsure of how to structure the view to achieve this. Currently I'm using a model viewset:
class InformationRequestViewSet(viewsets.ModelViewSet):
queryset = InformationRequest.objects.all()
serializer_class = ??
Is it possible to have two serializers for one model?
It's certainly possible to have 2 (or any number of) serializers for a model. And you are on the right path. What you want is different urls mapping to different views. So in your case, it can be something like the following:
Note that I turned each of your serializers into a ModelSerializer.
path-to/serializers.py
class UserInformationSerializer(serializers.ModelSerializer):
class Meta:
model = InformationRequest
fields = ('first_name', 'last_name')
class ContactDetailsSerializer(serializers.ModelSerializer):
class Meta:
model = InformationRequest
fields = ('phone', 'email')
Next, we have 2 different urls that point to 2 different views:
path-to/urls.py
urlpatterns = [
url(r'^requests/(?P<pk>\d+)/user-information/$', views.UserInformationDetail.as_view()),
url(r'^requests/(?P<pk>\d+)/contact-details/$', views.ContactInformationDetail.as_view()),
# ... other urls
]
And finally, the views themselves (I'm using generic RetrieveAPIView for convenience)
path-to/views.py
class UserInformationDetail(generics.RetrieveAPIView):
queryset = InformationRequest.objects.all()
serializer_class = UserInformationSerializer
class ContactInformationDetail(generics.RetrieveAPIView):
queryset = InformationRequest.objects.all()
serializer_class = ContactDetailsSerializer

django rest framework many to many serialize and apiview

I am creating APIView for book list and add, delete.
Is this serialization the right way?
I want to show a list of books, but I do not know what to put in '?'.
member/models.py
class MyUser(AbstractBaseUser, PermissionsMixin):
username = models.EmailField(unique=True, max_length=256)
nickname = models.CharField(max_length=256)
is_staff = models.BooleanField(default=False)
created_date = models.DateTimeField(auto_now_add=True)
mybook = models.ManyToManyField(Book)
book/models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=256)
author = models.CharField(max_length=200)
def __str__(self):
return self.title
book/seralizers.py
from rest_framework import serializers
from book.models import Book
class MyBookSerializer(serializers.ModelSerializer):
mybook = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Book
fields = (
'title',
'author',
)
apiview.py
class MyBook(APIView):
def get(self, request):
mybook = Book.objects.filter(myuser=?)
serializer = MyBookSerializer(mybook)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request):
pass
def delete(self, request):
pass
If you want create a CRUD for model, depends on a request.user you can simply use ModelViewSet:
from rest_framework import viewsets
class MyBookViewSet(viewsets.ModelViewSet):
serializer_class = MyBookSerializer
def get_queryset(self, request):
return Book.objects.filter(myuser=self.request.user)
That's all for view. You only need to specify router for a viewset.
urls.py:
from rest_framework import routers
router = routers.DefaultRouter()
router.register('books', MyBookViewSet)
urlpatterns = []
urlpatterns += router.urls
Include this usl file in a root urls and you are done.
You also need to specify related_name for m2m field if you want to use myuser name:
mybook = models.ManyToManyField(Book, related_name='myuser')
And you don't need this field in a serializer:
mybook = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
viewsets docs
routers docs

Passing argument from view to Custom RelatedField serializer

How can I pass an argument to a serializers.RelatedField class from views.py. I need to pass language_id to query Language.objects model within that RelatedField.
I am not sure if I took a right approach to this issue. What I want to achieve is to present information about genres associated to a movie from database model about depending on the language. The MovieGenre model has genre ID field which I want to replace with actual Genre name.
My serialiser.py
class GenreField(serializers.RelatedField):
def to_representation(self, value, language_id=1):
genre_name = GenresVideo.objects.get(genre_id=value, language_id=language_id)
return genre_name.name
class MovieGenresSerializer(serializers.ModelSerializer):
genre_id = GenreField(read_only=True)
class Meta:
model = MoviesGenres
As you see, here I query Language.objects with default value but I would like to pass it from views (language_id).
My views.py:
class MovieGenresTestViewSet(viewsets.ModelViewSet):
lookup_field = 'movie'
queryset = MoviesGenres.objects.all()
serializer_class = MovieGenresSerializer
def list(self, request, language_pk):
queryset = MoviesGenres.objects.all()
serializer = MovieGenresSerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, movie, language_pk):
queryset = MoviesGenres.objects.filter(movie=movie)
serializer = MovieGenresSerializer(queryset, many=True)
return Response(serializer.data)
And my urls.py:
router.register(r'lang', LanguagesViewSet, base_name='lang')
mov_gen = routers.NestedSimpleRouter(router, r'lang', lookup='language')
mov_gen.register(r'mg', MovieGenresTestViewSet, base_name='mg')
url(r'^api/', include(genre_spec.urls))
My models.py
class Languages(models.Model):
name = models.CharField(unique=True, max_length=255)
short_name = models.CharField(unique=True, max_length=4, blank=True, null=True)
active = models.BooleanField(default="")
class Meta:
managed = False
db_table = 'languages'
ordering = ('id',)
class GenresVideo(models.Model):
genre_id = models.IntegerField()
language = models.ForeignKey('Languages')
name = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = False
db_table = 'genres_video'
unique_together = (('genre_id', 'language'),)
ordering = ('genre_id',)
class MoviesGenres(models.Model):
movie = models.ForeignKey(Movies)
genre_id = models.IntegerField()
class Meta:
managed = False
db_table = 'movies_genres'
unique_together = (('movie', 'genre_id'),)
Through the urls routes, I can get a correct response from API including the language_id. I just need to pass it to the view somehow.
Thanks a lot for help!
I'll try to answer to your first question, with the easiest implementation possible: SerializerMethodField. Because we will get the language id via the context passed to the serializer, we should either generate the context for the serializer, or let the framework do that for us.
Now to the problem at hand: you aren't filtering the queryset (MoviesGenres) by language per se. Thus, we can avoid overwriting the list and retrieve methods. Nevertheless, the router mechanism will inject in kwargs for the view method the language_pk parameter - that's the parameter that we will retrieve from within the serializer context:
class MovieGenresSerializer(serializers.ModelSerializer):
genre = searializers.SerializerMethodField()
class Meta:
model = MoviesGenres
def get_genre(self, instance):
# get the language id from the view kwargs
language_id = self.context['view'].kwargs['language_pk']
# get the genre
try:
genre_name = GenresVideo.objects.get(genre_id=instance.genre_id, language_id=language_id).name
except GenresVideo.DoesNotExist:
genre_name = None
# return the formatted output
return genre_name
class MovieGenresTestViewSet(viewsets.ModelViewSet):
lookup_field = 'movie'
queryset = MoviesGenres.objects.all()
serializer_class = MovieGenresSerializer

Resources