django rest ViewAPI parameters not displayed in OpenAPI dynamic schema - django-rest-framework

I want to make a swagger document of my project, so I tried to generate a dynamic schema for my APIs, using django rest automatic dynamic generated schema, as said in its document.
This is my urls.py:
from django.urls import path, include
from rest_framework.schemas import get_schema_view
schema_view = get_schema_view(title="Example API")
urlpatterns = [
path("schema/", schema_view, name="schema"),
...
]
And this is one of my view serializers:
class BookSerializer(serializers.ModelSerializer):
summary_publisher_name = serializers.ReadOnlyField(source="published_by.name")
authors = AuthorSerializer(many=True)
chapters = ChapterSerializer(many=True, read_only=True)
class Meta:
model = Book
fields = ["title", "summary_publisher_name", "authors", "chapters"]
...
The problem is that in generated schema, there is none of these API parameters.
How can I add them to schema? Is there any automation way, or I need to add all fields for any of my views, by myself?

Related

Using FileField/ImageField with Swagger UI and drf-spectacular

I have a Django REST Framework project which uses a ModelViewSet to create APIs for a model containing a FileField.
I've shared a full example of a Django project demonstrating this issue here. But to summarise the key components:
models.py
from django.db import models
class Profile(models.Model):
image = models.FileField(upload_to='uploads/%Y/%m/%d/')
views.py
from rest_framework import (
viewsets,
serializers,
parsers,
)
from sample import models
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = models.Profile
fields = ['id', 'image']
read_only_fields = ['id']
class ProfileViewSet(viewsets.ModelViewSet):
serializer_class = ProfileSerializer
queryset = models.Profile.objects.all()
urls.py
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
)
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
from rest_framework.routers import DefaultRouter
from sample import views
router = DefaultRouter()
router.register('profile', views.ProfileViewSet)
urlpatterns = [
path('admin/', admin.site.urls),
path('api/schema/', SpectacularAPIView.as_view(), name='api-schema'),
path(
'api/docs/',
SpectacularSwaggerView.as_view(url_name='api-schema'),
name='api-docs',
),
path('', include(router.urls)),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
settings.py (REST_FRAMEWORK configuration only):
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
requirements.txt
Django==3.2.3
djangorestframework==3.12.4
drf-spectacular==0.16.0
I am generating browsable Swagger-UI docs using drf-spectacular to document the API.
The problem is that the FileField input in Swagger-UI is a string input, and doesn't give the option to set a file:
I would like to have a file input where I can choose a file which will be sent to the API.
My question is: How can I configure DRF or drf-spectacular to show this field?
After some digging through the drf-spectacular docs and GitHub issues, I found that the FileField can be set to binary by adding the following to in settings.py:
SPECTACULAR_SETTINGS = {
'COMPONENT_SPLIT_REQUEST': True
}
Also in Swagger UI, make sure you change the content-type from application/json to multipart/form-data and click Try it out. The upload button will apppear.

How to write a generic CRUD service on django using rest framework?

I am new to django and faced to several problems trying to write a simple service.
What am I trying to do?
I intend to write a generic crud service for my models using rest-framework library.
I don't want to write serializers and views to all my models and trying to optimize code and learn some useful stuff.
My model
Let's imagine I have an abstract BaseBusinessObject
class BaseBusinessObject(models.Model):
CreatedAt = models.DateField()
UpdatedAt = models.DateField()
class Meta:
abstract = True
I also have a plenty of concrete classes, which are inherited from base one:
class Product(BaseBusinessObject):
Description: models.TextField()
Type: models.CharField()
....
class Company(BaseBusinessObject):
Title: models.CharField()
....
class Person(BaseBusinessObject):
Name: models.CharField()
and so on
What I want
I already figured out, that with rest-framework I can create serializers and views, then register router for url .../Product, .../Company, .../Person. But what if I have 1000 classes? This is boring
A. How can I dynamically specified url's for child objects? I don't want to hardcode methods, I am looking for solution...something like this:
.../api/Entities/ClassName
B. How can I then use my dynamically created urls in django-rest-framework?
router.register('persons', PersonViewSet)
How can write it in more generic way?
router.register('<ClassName>', <GenericViewSet>)
C. Using DRF I can create my viewset for each concrete class in my model:
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonSerializer
But as I said I have a lot of classes. How can I write it in more generic way?
I tried to create a view set for abstract class, but there are some trouble when querying an abstract object.
Is it possible to create such service for an abstract class and then all its child simply(or not simply) inherit CRUD methods?
Maybe I should try to write a factory for serializers and viewsets?
What possible solutions could I implement for solving my problem?
After 2 days of walking around I finally find my solution. May be someone else will face the some problem, so I trying to explain what I had already done.
First, I create a "base" application inside my django project and add it to settings.py. Then I create an abstract class in models.py:
class BaseCrudEntity(models.Model):
pass
class Meta:
abstract = True
I want to write a generic service for CRUD operations for all "business" classes.
The problem is that I don't want to write serializers and views for them - I want to create them "on fly", dynamically. I decided to use django rest framework as well, because I am not intended to create a bycicle again.
I decided to inherit all my "business" classes from that abstract one and write a service for all possible "families"
So I have to create a fabric which is responsible for VeiwSet creation.
Here is my view.py:
class BaseCrudViewSetFabric():
#classmethod
def CreateViewSet(self, _context):
classname = _context.__name__ + 'ViewSet'
return type(classname, (viewsets.ModelViewSet,), {
'queryset':_context.objects.all(),
'serializer_class':BaseCrudSerializerFabric.CreateSrializer(_context)
})
pass
here _context - variable which describes concrete class of my model.
as you can see this function creates a concrete ViewSet based on my context. Inside it a Serializers fabric is called.
Here the code of my serializers.py:
class BaseCrudSerializerFabric():
#classmethod
def CreateSrializer(self, _context):
classname = _context.__name__
_Meta = type('Meta', (), {'model':_context,'fields':'__all__'})
_crudserializer = type(
classname,
(serializers.ModelSerializer,),
{'Meta': _Meta}
)
return _crudserializer
Moreover, I have to write a service for dynamically routing - I don't wanna hardcode my urls.
Here the example ursl.py from core project:
from base.urls import router
url(r'^api/v1/', include(router.urls))
and from base/urls.py:
from rest_framework.routers import DefaultRouter, SimpleRouter
from base.models.BaseCrudEntity import BaseCrudEntity
from base.views.basecrud_view import BaseCrudViewSetFabric
class CustomRouter(SimpleRouter):
def RoutRegister(self):
childs = getChilds(BaseCrudEntity)
#print(childs)
for ch in childs:
if (ch._meta.abstract == False):
#print(ch.__name__)
prefix = ch.__name__
self.register(prefix, BaseCrudViewSetFabric.CreateViewSet(ch))
return(self)
pass
router = CustomRouter()
router.RoutRegister()
Finally I simply create some concrete models:
from django.db import models
from base.models.BaseCrudEntity import BaseCrudEntity
class Person(BaseCrudEntity):
Name = models.CharField(max_length = 255)
Surname = models.CharField(max_length = 255)
Patronymic = models.CharField(max_length = 255, null = True)
DateOfBirth = models.DateField(null = True)
#slug = models.SlugField(default = 'hui', editable = False)
def __str__(self):
return "{} {} {}".format (self.Surname, self.Name, self.Patronymic)
and thats all.
When application starts it register a route for http://127.0.0.1:8000/api/v1/Person and creates serializers and viewsets so all CRUD operations provided by Django- Rest Framework will be provided as well.
I would suggest using query parameters instead of path parameters, then you just register one URL and process the various elements of the request and route it correctly server-side. When do I use path params vs. query params in a RESTful API?

DRF - in filter, use field-value instead of default pk / id

I'm trying to use DRF's filters so that the URL query is like so:
/roadname/?road=M5
not like so
/roadinfo/?road=1
I can't seem to do it when I've got a ForeignKey relationship.
I've tried using lookup_field with no luck (although not sure how this would work for multiple filter fields anyway - I don't think that's the answer). I've tried using a get_queryset() method in views as in the second example in the documentation. A comment I came across suggested that this is bad RESTApi practice - is it? How would a user know to type in '1' to get results for 'M5' in a front-end client?
I've set up two really simple models (and serializers, views, etc.) to try these out as below.
If I use RoadName, I have to type the name into the filter search box (rather than having a dropdown), but the url query is how I want it.
If I use RoadInfo (which has a ForeignField to RoadName), I get a drop down in the filter box, but the url query uses the ForeignKey pk.
My question: How can I set it so that when I use RoadInfo, the query uses the field value rather than the id/pk?
Models
from django.db import models
class RoadName(models.Model):
road = models.CharField(max_length=50)
def __str__(self):
return str(self.road)
class RoadInfo(models.Model):
road = models.ForeignKey(RoadName, on_delete='CASCADE')
# other data
def __str__(self):
return str(self.road)
Serializers
from traffic.models import *
from rest_framework import serializers
class RoadNameSerializer(serializers.ModelSerializer):
road = serializers.CharField()
class Meta:
model = RoadName
exclude = ('id',)
class RoadInfoSerializer(serializers.ModelSerializer):
road = RoadNameSerializer()
class Meta:
model = RoadInfo
exclude = ('id',)
Views
from traffic.serializers import *
from traffic.models import *
from django_filters import rest_framework as filters
from rest_framework import viewsets
class RoadNameViewSet(viewsets.ReadOnlyModelViewSet):
""" List of all traffic count Counts """
queryset = RoadName.objects.all()
serializer_class = RoadNameSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = '__all__'
class RoadInfoViewSet(viewsets.ReadOnlyModelViewSet):
""" List of all traffic count Counts """
queryset = RoadInfo.objects.all()
serializer_class = RoadInfoSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = '__all__'
The data M5 on the road attribute of RoadName model. It can be filtered by road__road from RoadInfo model.
So, Try /roadname/?road__road=M5

Writable many-to-many field in Django Rest Framework tries to save new child objects?

Edit: This is using django rest framework 2.3
I have a model structure that has 3 relationship "levels", one of which is many to many.
class Shipment(models.Model):
stuff...
class ShipmentItem(models.Model):
shipment = models.ForeignKey(Shipment)
assets = models.ManyToMany(ShipmentAsset)
class ShipmentAsset(models.Model)
serial_number = models.CharField(unique=True)
Using Django rest framework I want to be able to post to the "Shipment" endpoint with a payload that contains the ShipmentItems for the Shipment, and the ShipmentAssets for the ShipmentItems ideally in one request.
The serializers are as follows..
class ShipmentAssetSerializer(serializers.ModelSerializer):
class Meta:
model = ShipmentAsset
field = ('id', 'serial_number', )
class ShipmentItemSerializer(serializers.ModelSerializer):
assets = ShipmentAssetSerializer(
many=True, required=False, allow_add_remove=True,
)
class Meta:
model = ShipmentItem
fields = ('id', 'assets', )
class ShipmentSerializer(serializers.ModelSerializer):
class Meta:
model = Shipment
fields = (
'id',
)
The shipmentItem/Shipment relationship seems to work when I post to it with the assets part disabled, but when I try to post assets in the payload, It appears to be trying to create NEW assets with the posted data (I get an error regarding the unique constraint on the serial number) rather than creating a new many-to-many table object. Any idea what I'm doing wrong?
Edit: Important clarification, I'm using Django Rest Framework 2.3.13

Foreign key field disappears in swagger docs after adding depth attribute in Serializer

Whenever I define the depth attribute, the foreign key field from swagger docs in POST section disappears. That seems strange because I required depth = 1 when I want related data in my GET request. So I can not remove this in order to get this related field parameter in the POST section.
Here is the case.
Model:
from django.db import models
from django.conf import settings
# Create your models here.
User = settings.AUTH_USER_MODEL
class Todo(models.Model):
user = models.ForeignKey(User)
title = models.CharField("Title", max_length=255)
completed = models.BooleanField("Completed")
Serializer without depth =1.
from rest_framework import serializers
from models import Todo
class TodoSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Todo
Swagger output:
Now If I add depth = 1 than Swagger does not display related field.
Let me know if anyone has any clue about this.
Thanks :)
Finally after digging into this, I come up with solution by which we can avoid this issue and achieve the expected solution.
So the solution is "Instead of using depth = 1 attribute we can using related serializer instance it self where it works similar to depth functionality."
Here is tested solution
Model:
from django.db import models
from django.conf import settings
User = settings.AUTH_USER_MODEL
class Todo(models.Model):
user = models.ForeignKey(User)
title = models.CharField("Title", max_length=255)
completed = models.BooleanField("Completed")
Serializer
from rest_framework import serializers
from django.conf import settings
from models import Todo
User = settings.AUTH_USER_MODEL
class UserSerializer(serializers.HyperlinkedSerializer):
class Meta:
model = User
class TodoSerializer(serializers.HyperlinkedModelSerializer):
user = UserSerializer()
class Meta:
model = Todo
fields = ('user', 'title', 'completed')
Swagger Output:
This solution is kind of different approach in order to achieve the required functionality, But still I am expecting an official solution from django-rest-swagger team, Even I have posted the same query on django-rest-swagger github repo here.
One solution is to just don't use depth and override to_representation method of serializer:
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
def to_representation(self, instance):
r = super(TodoSerializer, self).to_representation(instance)
r.update({'user': UserSerializer().to_representation(instance.user)})
return r
This way, in post everything will be as it was, and in get when return json of todo then to_representation will be called and will add user to json data.
You need to update your serialzer as follows
class TodoSerializer(serializers.HyperlinkedModelSerializer):
creator = serializers.RelatedField(queryset=User.objects.all())
class Meta:
model = Todo
fields = ("name", "task", "creator")
depth = 1
you need to mentions fields and RelatedField in your serializers

Resources