Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenAPI Schema Generation #223

Merged
merged 67 commits into from
Jan 10, 2021
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
d8cf934
add openapi schema generation for filters
dhaval-mehta Mar 15, 2020
a448b13
improve schema generation for filters
dhaval-mehta Mar 15, 2020
0859877
add code to generate schema from serializer
dhaval-mehta Mar 18, 2020
bce22da
rename some variables
dhaval-mehta Mar 19, 2020
75343a0
override schema of pagination
dhaval-mehta Apr 3, 2020
93b0f14
handle GeometrySerializerMethodField
dhaval-mehta Apr 3, 2020
9f94293
fix invalid schema for id field
dhaval-mehta Apr 3, 2020
c7421e4
add test models
dhaval-mehta Apr 3, 2020
69bc672
add test serializer
dhaval-mehta Apr 3, 2020
7b99c1e
add initial test case for schema generation
dhaval-mehta Apr 3, 2020
563ce0d
add model migration
dhaval-mehta Apr 3, 2020
9df1f4f
rename files
dhaval-mehta Apr 3, 2020
b0ff15c
fix handling of GeometrySerializerMethodField
dhaval-mehta Apr 3, 2020
99bf8cf
handle DRF version >= 3.12
dhaval-mehta Apr 8, 2020
255f160
add DEFAULT_SCHEMA_CLASS
dhaval-mehta Apr 8, 2020
5598003
upgrade djangorestframework to 3.11
dhaval-mehta Apr 8, 2020
b830a74
fix _map_serializer
dhaval-mehta Apr 8, 2020
2b7f862
revert DRF version to 3.10
dhaval-mehta Apr 8, 2020
e3f1494
fix schema for polygon
dhaval-mehta Apr 8, 2020
540a7c4
fix enum for polygon
dhaval-mehta Apr 8, 2020
2a8d1b0
add test cases for polygon
dhaval-mehta Apr 8, 2020
f9ab386
remove min items and change example generation logic
dhaval-mehta Apr 8, 2020
60682e7
add test cases for multi polygon
dhaval-mehta Apr 8, 2020
bbf392d
add test case for multi line string model
dhaval-mehta Apr 8, 2020
d2895d8
add test case for multi point model
dhaval-mehta Apr 8, 2020
14ceb6b
add test cases for pagination schema
dhaval-mehta Apr 8, 2020
af8773a
add test cases for filter schemas
dhaval-mehta Apr 8, 2020
a94a7bf
do not handle source attribute
dhaval-mehta Apr 8, 2020
dac80da
add missing dependency
dhaval-mehta Apr 8, 2020
4a47129
add number as type of array member for bbox
dhaval-mehta Apr 8, 2020
f1a0a2b
add test case fox auto bbox
dhaval-mehta Apr 8, 2020
d137946
fix bbox_geo_field handling
dhaval-mehta Apr 8, 2020
2bdba3d
add test case for schema generation of bbox_geo_field
dhaval-mehta Apr 8, 2020
73bda51
add support for DRF 3.12
dhaval-mehta May 3, 2020
e3cb217
Merge remote-tracking branch 'upstream/master'
dhaval-mehta Oct 18, 2020
02f34ca
change DRF version to 3.12
dhaval-mehta Oct 18, 2020
1c2e457
fix schema generation for 3.12
dhaval-mehta Oct 18, 2020
3e7966f
remove support for DRF < 3.12
dhaval-mehta Oct 18, 2020
e374ed4
fix support for distance to point filter
dhaval-mehta Oct 18, 2020
01c1e92
fix linting issues
dhaval-mehta Oct 18, 2020
59d346a
add name to AUTHORS
dhaval-mehta Oct 18, 2020
11430b4
fix linting issues
dhaval-mehta Oct 18, 2020
331f764
fix linting issues using black
dhaval-mehta Oct 18, 2020
1aefe46
[fix] Conflicts between black and flake8
dhaval-mehta Oct 19, 2020
8780ea6
[fix] Postgresql Setup
dhaval-mehta Oct 25, 2020
8de2e18
[fix] revert unnecessary changes made by black
dhaval-mehta Oct 25, 2020
27443e8
[fix] Wrong call to _map_serializer
dhaval-mehta Oct 25, 2020
4c1dc11
[change] Update documentation
dhaval-mehta Oct 25, 2020
e31c72d
add packaging dependency
dhaval-mehta Oct 25, 2020
f6ea625
fix has_geometry_distance
dhaval-mehta Oct 25, 2020
fb01e25
run schema generation tests only if DRF >= 3.12
dhaval-mehta Oct 25, 2020
83d3dd7
remove unnecessary check for DRF < 3.9
dhaval-mehta Oct 25, 2020
1d551ba
[fix] Fix tox.ini
dhaval-mehta Oct 25, 2020
644c379
[fix] Fix .travis.yml
dhaval-mehta Oct 25, 2020
6abd208
[fix] Fix linting issues
dhaval-mehta Oct 25, 2020
1f14ee5
[fix] revert unnecessary changes.
dhaval-mehta Oct 25, 2020
917b490
[fix] Linting issues
dhaval-mehta Oct 25, 2020
b1c30e6
[fix] Support for Django 2.1
dhaval-mehta Oct 25, 2020
0009d2a
[fix] travis env
dhaval-mehta Oct 25, 2020
5af7366
[fix] Fix tox.ini
dhaval-mehta Oct 25, 2020
218fec5
[Merge] Merge remote-tracking branch 'upstream/master'
dhaval-mehta Nov 10, 2020
c9ffd55
[Merge] Merge remote-tracking branch 'upstream/master'
dhaval-mehta Dec 24, 2020
438527c
[ci] Fix .travis.yml
dhaval-mehta Dec 25, 2020
f890c91
[code] Add support for GeometryField and GeometryCollectionField
dhaval-mehta Dec 30, 2020
d61949d
[docs] improve docs
dhaval-mehta Dec 31, 2020
88d0b12
[ci] Remove support for Django 2.1 and support for Python 3.9
dhaval-mehta Dec 31, 2020
d0f8848
[lint] Fix linting issues
dhaval-mehta Dec 31, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Federico Capoano https://github.com/nemesisdesign/
Shanto https://github.com/Shanto
Eric Theise https://github.com/erictheise
Asif Saifuddin https://github.com/auvipy
Dhaval Mehta https://github.com/dhaval-mehta
142 changes: 109 additions & 33 deletions rest_framework_gis/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,49 +38,68 @@


__all__ = [
'InBBoxFilter',
'InBBOXFilter',
'GeometryFilter',
'GeoFilterSet',
'TMSTileFilter',
'DistanceToPointFilter',
'DistanceToPointOrderingFilter',
"InBBoxFilter",
"InBBOXFilter",
"GeometryFilter",
"GeoFilterSet",
"TMSTileFilter",
"DistanceToPointFilter",
"DistanceToPointOrderingFilter",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please do not change the quoting style already in use, applies also to the other similar changes

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is done by black. I will find all unnecessary modifications and revert them.

At the end of all tests, while destroying the database, the free function throws an error. Please look into it. Not only mine but all PRs are failing because of this issue.

Destroying test database for alias 'default'...
free(): invalid pointer

]


class InBBoxFilter(BaseFilterBackend):
bbox_param = 'in_bbox' # The URL query parameter which contains the bbox.
bbox_param = "in_bbox" # The URL query parameter which contains the bbox.

def get_filter_bbox(self, request):
bbox_string = request.query_params.get(self.bbox_param, None)
if not bbox_string:
return None

try:
p1x, p1y, p2x, p2y = (float(n) for n in bbox_string.split(','))
p1x, p1y, p2x, p2y = (float(n) for n in bbox_string.split(","))
except ValueError:
raise ParseError(
'Invalid bbox string supplied for parameter {0}'.format(self.bbox_param)
"Invalid bbox string supplied for parameter {0}".format(self.bbox_param)
)

x = Polygon.from_bbox((p1x, p1y, p2x, p2y))
return x

def filter_queryset(self, request, queryset, view):
filter_field = getattr(view, 'bbox_filter_field', None)
include_overlapping = getattr(view, 'bbox_filter_include_overlapping', False)
filter_field = getattr(view, "bbox_filter_field", None)
include_overlapping = getattr(view, "bbox_filter_include_overlapping", False)
if include_overlapping:
geoDjango_filter = 'bboverlaps'
geoDjango_filter = "bboverlaps"
else:
geoDjango_filter = 'contained'
geoDjango_filter = "contained"

if not filter_field:
return queryset

bbox = self.get_filter_bbox(request)
if not bbox:
return queryset
return queryset.filter(Q(**{'%s__%s' % (filter_field, geoDjango_filter): bbox}))
return queryset.filter(Q(**{"%s__%s" % (filter_field, geoDjango_filter): bbox}))

def get_schema_operation_parameters(self, view):
return [
{
"name": self.bbox_param,
"required": False,
"in": "query",
"description": "Specify a bounding box as filter: in_bbox=min_lon,min_lat,max_lon,max_lat",
"schema": {
"type": "array",
"items": {"type": "float"},
"minItems": 4,
"maxItems": 4,
"example": [0, 0, 10, 10],
},
"style": "form",
"explode": False,
},
]


# backward compatibility
Expand All @@ -91,13 +110,13 @@ class GeometryFilter(django_filters.Filter):
field_class = forms.GeometryField

def __init__(self, *args, **kwargs):
kwargs.setdefault('widget', forms.TextInput)
kwargs.setdefault("widget", forms.TextInput)
super().__init__(*args, **kwargs)


class GeoFilterSet(django_filters.FilterSet):
GEOFILTER_FOR_DBFIELD_DEFAULTS = {
models.GeometryField: {'filter_class': GeometryFilter},
models.GeometryField: {"filter_class": GeometryFilter},
}

def __new__(cls, *args, **kwargs):
Expand All @@ -111,38 +130,49 @@ def __new__(cls, *args, **kwargs):


class TMSTileFilter(InBBoxFilter):
tile_param = 'tile' # The URL query paramater which contains the tile address
tile_param = "tile" # The URL query parameter which contains the tile address

def get_filter_bbox(self, request):
tile_string = request.query_params.get(self.tile_param, None)
if not tile_string:
return None

try:
z, x, y = (int(n) for n in tile_string.split('/'))
z, x, y = (int(n) for n in tile_string.split("/"))
except ValueError:
raise ParseError(
'Invalid tile string supplied for parameter {0}'.format(self.tile_param)
"Invalid tile string supplied for parameter {0}".format(self.tile_param)
)

bbox = Polygon.from_bbox(tile_edges(x, y, z))
return bbox

def get_schema_operation_parameters(self, view):
return [
{
"name": self.tile_param,
"required": False,
"in": "query",
"description": "Specify a bounding box filter defined by a TMS tile address: tile=Z/X/Y",
"schema": {"type": "string", "example": "12/56/34"},
},
]


class DistanceToPointFilter(BaseFilterBackend):
dist_param = 'dist'
point_param = 'point' # The URL query parameter which contains the
dist_param = "dist"
point_param = "point" # The URL query parameter which contains the

def get_filter_point(self, request, **kwargs):
point_string = request.query_params.get(self.point_param, None)
if not point_string:
return None

try:
(x, y) = (float(n) for n in point_string.split(','))
(x, y) = (float(n) for n in point_string.split(","))
except ValueError:
raise ParseError(
'Invalid geometry string supplied for parameter {0}'.format(
"Invalid geometry string supplied for parameter {0}".format(
self.point_param
)
)
Expand Down Expand Up @@ -179,9 +209,9 @@ def dist_to_deg(self, distance, latitude):
return distance / (earthRadius * latitudeCorrection) * rad2deg

def filter_queryset(self, request, queryset, view):
filter_field = getattr(view, 'distance_filter_field', None)
convert_distance_input = getattr(view, 'distance_filter_convert_meters', False)
geoDjango_filter = 'dwithin' # use dwithin for points
filter_field = getattr(view, "distance_filter_field", None)
convert_distance_input = getattr(view, "distance_filter_convert_meters", False)
geoDjango_filter = "dwithin" # use dwithin for points

if not filter_field:
return queryset
Expand All @@ -196,7 +226,7 @@ def filter_queryset(self, request, queryset, view):
dist = float(dist_string)
except ValueError:
raise ParseError(
'Invalid distance string supplied for parameter {0}'.format(
"Invalid distance string supplied for parameter {0}".format(
self.dist_param
)
)
Expand All @@ -206,19 +236,47 @@ def filter_queryset(self, request, queryset, view):
dist = self.dist_to_deg(dist, point[1])

return queryset.filter(
Q(**{'%s__%s' % (filter_field, geoDjango_filter): (point, dist)})
Q(**{"%s__%s" % (filter_field, geoDjango_filter): (point, dist)})
)

def get_schema_operation_parameters(self, view):
return [
{
"name": self.dist_param,
"required": False,
"in": "query",
"schema": {"type": "number", "format": "float", "default": 1000},
"description": f"Represents **Distance** in **Distance to point** filter. "
f"Default value is used only if ***{self.point_param}*** is passed.",
},
{
"name": self.point_param,
"required": False,
"in": "query",
"description": "Point represented in **x,y** format. "
"Represents **point** in **Distance to point filter**",
"schema": {
"type": "array",
"items": {"type": "float"},
"minItems": 2,
"maxItems": 2,
"example": [0, 10],
},
"style": "form",
"explode": False,
},
]


class DistanceToPointOrderingFilter(DistanceToPointFilter):
srid = 4326
order_param = 'order'
order_param = "order"

def filter_queryset(self, request, queryset, view):
if not GeometryDistance:
raise ValueError('GeometryDistance not available on this version of django')
raise ValueError("GeometryDistance not available on this version of django")

filter_field = getattr(view, 'distance_ordering_filter_field', None)
filter_field = getattr(view, "distance_ordering_filter_field", None)

if not filter_field:
return queryset
Expand All @@ -228,7 +286,25 @@ def filter_queryset(self, request, queryset, view):
return queryset

order = request.query_params.get(self.order_param)
if order == 'desc':
if order == "desc":
return queryset.order_by(-GeometryDistance(filter_field, point))
else:
return queryset.order_by(GeometryDistance(filter_field, point))

def get_schema_operation_parameters(self, view):
params = super().get_schema_operation_parameters(view)
params.append(
{
"name": self.order_param,
"required": False,
"in": "query",
"description": "",
"schema": {
"type": "enum",
"items": {"type": "string", "enum": ["asc", "desc"]},
"example": "desc",
},
"style": "form",
"explode": False,
}
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to return the params. It is throwing an error in drf spectacular

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this implementation is not based on DRF spectacular btw

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@auvipy yes but check all implementations of get_schema_operation_parameters, they all return data. This is the only one that does not. Settings params inside of get_schema_operation_parameters and then not returning it also does not make sense since no one can access these parameters, since they are not set on self or returned. It is a get function, which should return something. In this case the params.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you up for making a contribution? I will review it

21 changes: 15 additions & 6 deletions rest_framework_gis/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,26 @@ class GeoJsonPagination(pagination.PageNumberPagination):
A geoJSON implementation of a pagination serializer.
"""

page_size_query_param = 'page_size'
page_size_query_param = "page_size"

def get_paginated_response(self, data):
return Response(
OrderedDict(
[
('type', 'FeatureCollection'),
('count', self.page.paginator.count),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('features', data['features']),
("type", "FeatureCollection"),
("count", self.page.paginator.count),
("next", self.get_next_link()),
("previous", self.get_previous_link()),
("features", data["features"]),
]
)
)

def get_paginated_response_schema(self, view):
schema = super().get_paginated_response_schema(view)
schema["properties"]["features"] = schema["properties"].pop("results")
schema["properties"] = {
"type": {"type": "string", "enum": ["FeatureCollection"]},
**schema["properties"],
}
return schema