How to generate schema for multiple choice filter? - django

In our API we have an endpoint to list locations. We allow filtering on location type, and we allow multiple values for this filter. For example:
GET /location/?type=hotel&type=airport
For filtering we are using django-filter. However, drf-yasg doesn't seem to correctly generate the schema for this parameter.
The view class can be boiled down to this:
from rest_framework.generics import ListAPIView
from .models import Location
from .serializers import LocationListSerializer
from .filters import LocationFilterSet
from django_filters.rest_framework import DjangoFilterBackend
class LocationListView(ListAPIView):
queryset = Location.objects.all()
serializer_class = LocationListSerializer
filter_backends = (
DjangoFilterBackend,
)
filter_class = LocationFilterSet
and the filter class looks like this:
from django_filters import rest_framework as filters
from .models import Location
class LocationFilterSet(filters.FilterSet):
type = filters.MultipleChoiceFilter(choices=Location.TYPE_CHOICES)
class Meta:
model = Location
fields = (
'type',
)
This view works as intended - the following test passes:
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from .models import Location
class TestLocationView(TestCase):
def test_filter_by_multiple_types(self):
Location.objects.create(type='airport')
Location.objects.create(type='hotel')
Location.objects.create(type='home')
response = self.client.get('/location/?type=hotel&type=airport')
self.assertEqual(len(response.data), 2)
I'd expect the generated yaml for this parameter to look like this:
parameters:
- name: type
in: query
description: ''
required: false
schema:
type: array
items:
type: string
explode: true
but instead, it looks like this:
- name: type
in: query
description: ''
required: false
type: string
Is this a limitation of drf-yasg?
It's not possible to use swagger_auto_schema's query_serializer since it doesn't allow overriding the schemas generated by the filter backends.
This seems to happen because django_filters.rest_framework.backends.DjangoFilterBackend.get_coreschema_field only outputs two field types, number and strings. I went ahead and overrode that method, however, it then throws errors in drf_yasg.inspectors.query.CoreAPICompatInspector.coreapi_field_to_parameter, doesn't accept the array type.

Related

How to use different Pydantic Models in a view

Im currently trying to understand the Pydantic FastAPI setup within my Django app and have the following issue:
When I create a new Order object, I want to return the newly created object using a different Pydantic model as I use for the creation (as after the creation I now have the ID of the order that I can also return).
# routes
# Create a new Order
#router.post("/create/", response_model=schemas.OrderResponse)
def create_orders(order: schemas.OrderBase):
return views.create_order(order=order)
# views.py
from uuid import UUID
from . import models, schemas
# Create a new order
def create_order(order: schemas.OrderBase):
new_order = models.Order.objects.get_or_create(order)
return new_order
# schemas.py
# pydantic models
from uuid import UUID
from pydantic import BaseModel
# Base Class for Model "Order"
class OrderBase(BaseModel):
product: str
period: str
power: int
side: str
class Config:
orm_mode = True
class OrderResponse(OrderBase):
id: UUID
When I now send an API request with this body:
{
"product": "Base",
"period": "Hour",
"power": 10,
"side": "Buy"
}
I get this error -- how to set it up such that when creating the instance it doesnt validate for the UUID and after creation and when returning the instance it includes the UUID in the response?
pydantic.error_wrappers.ValidationError: 5 validation errors for OrderResponse
response -> product
field required (type=value_error.missing)
response -> period
field required (type=value_error.missing)
response -> power
field required (type=value_error.missing)
response -> side
field required (type=value_error.missing)
response -> id
field required (type=value_error.missing)
It seems like converting your django model instance to pydantic automatically is not supported on fastAPI.
So I add a code that convert your model to dict, and that dict will initiate OrderResponse instance.
Try this,
from django.forms.models import model_to_dict
...
#router.post("/create/", response_model=schemas.OrderResponse)
def create_orders(order: schemas.OrderBase):
return model_to_dict(views.create_order(order=order))

Django filter geometry given a coordinate

I want to get a row from a postgis table given a coordinate/point. With raw sql I do it with:
SELECT * FROM parcelas
WHERE fk_area=152
AND ST_contains(geometry,ST_SetSRID(ST_Point(342884.86705619487, 6539464.45201204),32721));
The query before returns one row.
When I try to do this on django it doesn't return me any row:
from django.contrib.gis.geos import GEOSGeometry
class TestView(APIView):
def get(self, request, format=None):
pnt = GEOSGeometry('POINT(342884.86705619487 6539464.45201204)', srid=32721)
parcelas = Parcelas.objects.filter(fk_area=152,geometry__contains=pnt)
#Also tried this
#parcelas = Parcelas.objects.filter(fk_area=pk,geometry__contains='SRID=32721;POINT(342884.86705619487 6539464.45201204)')
serializer = ParcelasSerializer(parcelas, many=True)
return Response(serializer.data)
Even with django raw query it fails although in this case it returns me an internal server error (argument 3: class 'TypeError': wrong type):
class TestView(APIView):
def get(self, request, format=None):
parcelas = Parcelas.objects.raw('SELECT * FROM parcelas WHERE fk_area=152 AND ST_contains(geometry,ST_SetSRID(ST_Point(342884.86705619487, 6539464.45201204),32721))')
for p in parcelas:
#Internal server error
print(p.id)
return Response('Test')
My model parcelas look like this:
from django.contrib.gis.db import models
class Parcelas(models.Model):
id = models.BigAutoField(primary_key=True)
fk_area = models.ForeignKey(Areas, models.DO_NOTHING, db_column='fk_area')
geometry = models.GeometryField()
class Meta:
managed = False
db_table = 'parcelas'
I don't know what I'm doing wrongly if someone has any idea.
EDIT:
If I print the raw query that django made:
SELECT "parcelas"."id", "parcelas"."fk_area", "parcelas"."geometry"::bytea FROM "parcelas" WHERE ("parcelas"."fk_area" = 152 AND ST_Contains("parcelas"."geometry", ST_Transform(ST_GeomFromEWKB('\001\001\000\000 \321\177\000\000C\224\335w\223\355\024A\350\303\355\0342\362XA'::bytea), 4326)))
Seems like django is not converting it to the correct srid (32721) but I don't know why
EDIT 2:
If in my model I specify the SRID it works correctly:
class Parcelas(models.Model):
geometry = models.GeometryField(srid=32721)
The problem is that the SRID can be variable depending on the query the rows have one SRID or another so I don't want to set it to always being one.
Test database is created separately and does not contain the same data as the main application database. Try using pdb and listing all entries inside the parcelas table. Unless TestView means just a mock view for the time being.
Using pdb:
import pdb, pdb.set_trace()
Parcelas.objects.all()
In case the records geometry needs to be compared to a geojson like object one approach is to convert the object to GEOSGeometry and then find the record using .get(), .filter() etc.
For example, in case of an API JSON request payload that contains somewhere in the payload the following field:
"geometry": {
"type": "Polygon",
"coordinates": [
[
[21.870314, 39.390873],
[21.871913, 39.39319],
[21.874029, 39.392443],
[21.873401, 39.391328],
[21.873369, 39.391272],
[21.873314, 39.391171],
[21.872715, 39.390024],
[21.870314, 39.390873]
]
]
}
One can use the following code:
import json
from django.contrib.gis.geos import GEOSGeometry
# Assuming the python dictionary containing the geometry field is geometry_dict
payload_geometry = GEOSGeometry(json.dumps(geometry_dict))
parcel = Parcel.objects.get(geometry=payload_geometry)

Testing Django Rest Framework: how to test hyperlink relations?

I'm trying to create a true unit test for a customized DjangoRestFramework Hyperlinked related field. But I cannot seem to get around this error:
django.core.exceptions.ImproperlyConfigured: Could not resolve URL for hyperlinked relationship using view name "relatedtestmodel-detail". You may have failed to include the related model in your API, or incorrectly configured the `lookup_field` attribute on this field.
And here is the unit test, stripped down to simplify the example:
from django.conf.urls import url
from django.test import TestCase, override_settings
from api_tests.models import APITestModel, RelatedTestModel
from api_tests.serializers import APITestModelSerializer
def dummy_view(request, pk):
pass
urlpatterns = [
url(r'/path/is/irrelevant/', dummy_view, name='relatedtestmodel-detail')
]
#override_settings(ROOT_URLCONF='tests.test_relations')
class HyperlinkedRelatedFieldTestCase(TestCase):
def setUp(self):
self.parent = APITestModel.objects.create()
self.child = RelatedTestModel.objects.create(parent=self.parent)
assert self.child.parent.id == self.parent.id
def test_to_internal_value_correct_error_message(self):
queryset = APITestModel.objects.all()
serializer = APITestModelSerializer(queryset, many=True, context={'request': None})
expected = [{'foo': 'bar'}]
self.assertEqual(serializer.data, expected)
I more or less lifted the test from https://github.com/encode/django-rest-framework/blob/master/tests/test_relations_hyperlink.py, because I figured who knows best how to unit test DRF than the makers of DRF? But as it stands, my test refuses to run. The error is raised during the test, when I attempt to access serializer.data for the assert.
Notice in particular that I override the settings with a custom urlpatterns (which is this same file, hence the urlpatterns at the top). So I don't understand why DRF thinks that url name doesn't exist - I can clearly see that my url conf in fact has ONLY that view name! I've even gone so far as to edit my actual URL conf and replace it with the single, named, dummy url pattern shown here, and removedthe settings override, just to be sure that it wasn't that the override_settings simply wasn't working, but even then I get the same error.
To my eye, the dummy url pattern is exactly the same as how DRF did it in their tests. Anyone have any ideas what is going on?
A bit more requested context:
api_tests.models:
from django.db import models
class APITestModel(models.Model):
pass
class RelatedTestModel(models.Model):
parent = models.ForeignKey(
APITestModel,
related_name='children',
related_query_name='child'
)
I do not have access to the traceback at this time, but I can confirm it did not pass through any of my code - it was all isolated to the DjangoRestFramework code, basically exclusively relations.py
Preamble
A few things this question is lacking
No definition of APITestModelSerializer
RelatedTestModel is not used in the test and therefore irrelevant for the example
No error stacktrace
No "useful" expected dict for the asserts
APITestModel has no fields so it can't be serialized (your test shouldn't even have run)
Minor things but still relevant
You are creating specific instances of APITestModel and RelatedTestModel in the setUp but in the test you serialize all instances of APITestModel
The line assert self.child.parent.id == self.parent.id should not be in the setUp. It should be in a separate test
My changes
I deleted all irrelevant information for this question mentioned above
I added an integer field to APITestModel
I changed the urlpatterns element from url(...) to path(...)
I added a regex to the relative path
The serializer is a subclass of HyperlinkedModelSerializer and includes fields "url" and "year"
My project and app urls.py files are the "stock" ones (not shown here) to emphasize that this test resolves the path in isolation.
Changed #override_settings(ROOT_URLCONF='tests.test_relations') to #override_settings(ROOT_URLCONF=__name__)
Code
models.py
from django.db import models
class APITestModel(models.Model):
year = models.IntegerField(null=False)
serializers.py
from rest_framework import serializers
from api.models import APITestModel
class APITestModelSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = APITestModel
fields = ["url", "year"]
test_serializers.py
from django.test import TestCase, override_settings
from django.urls import path
from api.models import APITestModel
from api.serializers import APITestModelSerializer
urlpatterns = [
path('whateveryouwant/<int:pk>/', lambda request: None, name='apitestmodel-detail'),
]
#override_settings(ROOT_URLCONF=__name__)
class HyperlinkedRelatedFieldTestCase(TestCase):
def setUp(self):
# Populate db with APITestModel instances
_ = APITestModel.objects.create(year=1960)
_ = APITestModel.objects.create(year=1961)
_ = APITestModel.objects.create(year=1962)
def test_to_internal_value_correct_error_message(self):
queryset = APITestModel.objects.all()
serializer = APITestModelSerializer(queryset, many=True, context={'request': None})
expected = [
{'url': '/whateveryouwant/1/', 'year': 1960},
{'url': '/whateveryouwant/2/', 'year': 1961},
{'url': '/whateveryouwant/3/', 'year': 1962},
]
self.assertEqual(serializer.data, expected)
The other files in the project are the default ones created automatically by django + djangorestframework.
For future readers, I created a github project with this working code and can be found here: https://github.com/Alechan/drf_test_hyperlink_relations

With drf-yasg, how can I support multiple serializers in the Response?

With a response from my drf just containing the data given by a single serializer, we can implement it as:
#swagger_auto_schema(
operation_id='ID example',
operation_description="Description example.",
responses={status.HTTP_200_OK: Serializer4ModelA(many=True)},
)
Which works fantastic, but with some requests constructing a dictionary, where two or three of the keys correspond to different serializers, e.g.
response = {
"a": serializer_data_for_model_a,
"b": serializer_data_for_model_b,
"c": serializer_data_for_model_c
}
How can we describe that in the auto schema? I've tried a few different approaches, mostly similar to the following:
#swagger_auto_schema(
operation_id='ID example',
operation_description="Description example.",
responses={status.HTTP_200_OK: openapi.Response(
description='response description',
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'a': Serializer4ModelA(many=True),
'b': Serializer4ModelB(many=True),
'c': Serializer4ModelC(many=True)
})
)}
)
But always fails when loading the documentation, with flex saying:
"/usr/local/lib/python3.6/site-packages/flex/utils.py", line 125, in
get_type_for_value raise ValueError("Unable to identify type of
{0}".format(repr(value)))
ValueError: Unable to identify type of
Serializer4ModelA(many=True):
I've read the documentation over and over again, and scoured over github for an example, but I couldn't find an example or anyone doing this. So my question is how to successfully manually define a schema for a response that contains different serializers for different keys in the returned response?
What I usually do is to create another serializer (just so that drf-yasg can generate the docs).
For example if I have an endpoint that returns:
{
"results": [..list of serialized results with serializer X...]
}
I create a second serializer:
class Y(serializers.Serializer):
results = X(many=True)
and use Y serializer in the swagger_auto_schema decorator.
I ended up being able to do it, although probably not the most elegant solution but it does work.
My drf has a custom app-label format, so all my apps are in a folder, and let's call this folder apps.
In my question, for a serializer, we can replace Serializer4ModelA in the properties section of the openapi.Schema with a custom function, lets say get_serializer(Serializer4ModelA()).
So my idea was to basically construct the schema myself by getting the information automatically and automatically constructing the properties dictionary. It's very hacky, but useful for me because in my documentation I also want to pass in the serializers for Dynamodb, so I made a very similar function for Dynamodb serializers.
I only just made it, and it works, but obviously needs more attention to cover all fields in the field mapping, better dealing with SerializerMethodFields.
But none the less, it is a solution that works but is not generic, tweaks and stuff will have to be made depending on your particular project.
I implemented the function roughly as follows:
from drf_yasg import openapi
from drf_yasg.inspectors import SwaggerAutoSchema
from drf_yasg.utils import swagger_auto_schema
from drf_yasg.inspectors import FieldInspector
from drf_yasg.utils import swagger_serializer_method
import rest_framework
rest_framework_openapi_field_mapping = {
"ListField": openapi.TYPE_ARRAY,
"CharField": openapi.TYPE_STRING,
"BooleanField": openapi.TYPE_BOOLEAN,
"FloatField": openapi.TYPE_NUMBER,
"DateTimeField": openapi.TYPE_STRING,
"IntegerField": openapi.TYPE_INTEGER,
"SerializerMethodField": openapi.TYPE_STRING
}
def parse_rest_framework_field(field):
rest_framework_field_type = field.split("(")[0]
openapi_field_type =
rest_framework_openapi_field_mapping[rest_framework_field_type]
if "help_text=" in field:
field_description = field.split("help_text='")[-1].split("'")[0]
else:
field_description = None
return openapi.Schema(type=openapi_field_type, description=field_description)
def parse_serializer(serializer):
properties = {}
for k,v in serializer.get_fields().items():
if v.__module__ == "rest_framework.fields":
properties[k] = parse_rest_framework_field(str(v))
elif v.__module__.startswith("apps."):
serializer = str(v).strip().split("(")[0]
exec(f"from {v.__module__} import {serializer}")
eval_serializer = eval(f"{serializer}()")
properties[k] = openapi.Schema(type=openapi.TYPE_OBJECT, properties=parse_serializer(eval_serializer))
else:
pass
return properties
def get_serializer(serializer, description):
""" Needs to return openapi.Schema() """
properties = parse_serializer(serializer)
return_openapi_schema = openapi.Schema( type=openapi.TYPE_OBJECT, properties=properties, description=description)
return return_openapi_schema
I faced this problem and was looking if there is another way than my initial solution (same as how #Hernan explained it) but found none. The code of drf_yasg.openapi.Schema (drf_yasg==1.20.0) showed that it doesn't accept any serializer object. So as already said by #Hernan, the way around this is to have an additional serializer and define there the nested child serializers. Then, pass it to either the swagger_auto_schema.responses directly or through an openapi.Response.schema (as below):
from django.urls import path
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import serializers, status, views
class Serializer4ModelA(serializers.Serializer):
dog = serializers.CharField(label="My dog is a good boy")
class Serializer4ModelB(serializers.Serializer):
perro = serializers.CharField(label="Mi perro es un buen chico")
hund = serializers.CharField(label="Mein Hund ist ein guter Junge")
aso = serializers.CharField(label="Ang aso ko ay mabait na bata")
class Serializer4ModelC(serializers.Serializer):
eey = serializers.CharField(label="Eygaygu waa wiil fiican")
class SampleResponseSerializer(serializers.Serializer):
a = Serializer4ModelA(many=True)
b = Serializer4ModelB(many=True)
c = Serializer4ModelC(many=True)
class SampleView(views.APIView):
#swagger_auto_schema(
responses={
status.HTTP_200_OK: openapi.Response(
description="response description",
schema=SampleResponseSerializer,
)
}
)
def get(self, request):
pass
urlpatterns = [
path("sample/", SampleView.as_view()),
]
Output:

Graphene-Django: In schema combine Query-objects (only takes first argument)

I am trying to combine multiple Query schemas located in different apps in Django 2.1. Using graphene-django 2.2 (have tried 2.1 with same problem). Python 3.7.
The Query class only registers the first variable. As an example shop.schema.Query.
import graphene
import graphql_jwt
from django.conf import settings
import about.schema
import shop.schema
import landingpage.schema
class Query(about.schema.Query, shop.schema.Query, landingpage.schema.Query, graphene.ObjectType):
pass
class Mutation(shop.schema.Mutation, graphene.ObjectType):
token_auth = graphql_jwt.ObtainJSONWebToken.Field()
verify_token = graphql_jwt.Verify.Field()
refresh_token = graphql_jwt.Refresh.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
Why is it like this? Have something changed with classes in python 3.7? The graphene tutorial says this will inherit for multiple...
class Query(cookbook.ingredients.schema.Query, graphene.ObjectType):
# This class will inherit from multiple Queries
# as we begin to add more apps to our project
pass
schema = graphene.Schema(query=Query)
I am exporting my schema to schema.json for using it with react relay. I do find my object "collection" Query schema from landingpage(the 3. variable). Relay returns:
ERROR: GraphQLParser: Unknown field collection on type Viewer.
Source: document AppQuery file: containers/App/index.js.
Is it a problem with Relay reading my schema.json?
I managed to solve it shortly after writing this. My problem was that I had a Viewer object in every app. Because I find it useful to have a viewer-graphql-root, like this:
graphql'
viewer {
collection {
somestuff
}
}
'
I moved the Viewer object up to the root schema.py like this:
class Viewer(about.schema.Query, landingpage.schema.Query, shop.schema.Query, graphene.ObjectType):
class Meta:
interfaces = [relay.Node, ]
class Query(graphene.ObjectType):
viewer = graphene.Field(Viewer)
def resolve_viewer(self, info, **kwargs):
return Viewer()
class Mutation(shop.schema.Mutation, graphene.ObjectType):
token_auth = graphql_jwt.ObtainJSONWebToken.Field()
verify_token = graphql_jwt.Verify.Field()
refresh_token = graphql_jwt.Refresh.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
In setting.py add a new file as schema.py
Combine your Queries and Mutations in schema.py as follows:
import graphene
import about.schema as about
import shop.schema as projects
import landingpage.schema as projects
then add:
class Query(about.schema.Query, shop.schema.Query, landingpage.schema.Query, graphene.ObjectType):
pass
class Mutation(about.schema.Mutation, shop.schema.Mutation, landingpage.schema.Mutation, graphene.ObjectType):
pass
schema = graphene.Schema(query=Query, mutation=Mutation)
Configure your combined schema in settings.py as follows:
GRAPHENE = {
"SCHEMA": "core.schema.schema",
}