Skip to content

Commit 2e1f083

Browse files
authored
Merge pull request #32 from volCommunity/swagger_versioning
Swagger, Accept header versioning
2 parents f5736fa + e0821e5 commit 2e1f083

File tree

7 files changed

+157
-61
lines changed

7 files changed

+157
-61
lines changed

Pipfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ whitenoise = "*"
1212
"django-analytical" = "*"
1313
djangorestframework = "*"
1414
"factory-boy" = "*"
15+
coreapi = "*"
16+
"django-rest-swagger" = "*"
1517

1618
[dev-packages]
1719

1820
pytest = "*"
1921
"flake8" = "*"
2022
"pytest-flake8" = "*"
2123
coverage = "*"
22-
coreapi = "*"
2324
coveralls = "*"

Pipfile.lock

Lines changed: 122 additions & 45 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,15 @@ Create them using the admin console, which you will be able to access locally at
102102

103103
## API
104104
We run <a href=http://www.django-rest-framework.org>DRF</a>, which exposes a browesable API:
105-
<a href="https://www.vol.community/api/">www.vol.community/api</a>.
105+
<a href="https://www.vol.community/api/">www.vol.community/api</a>. Alternatively use Swagger/OpenAPI at <a href="https://www.vol.community/api/swagger">www.vol.community/api/swagger</a>
106106

107107
Read only access can be accessed without tokens, mutating requires using basic auth or having
108108
an API token.
109109

110110
Pass tokens by adding an Authorization header like so:
111111

112112
```shell
113-
curl -X DELETE https://www.vol.community/api/v0.0.1/jobs/1 --header "Content-Type: application/json" -H 'Authorization: Token 9b13d4942b24dc0eb12eb77f3eaf37f23b250175'
113+
curl -X DELETE https://www.vol.community/api/jobs/1 --header "Content-Type: application/json" -H 'Authorization: Token 9b13d4942b24dc0eb12eb77f3eaf37f23b250175'
114114
```
115115

116116
If you are looking for a client, <a href=https://github.com/core-api>core-api</a> clients do
@@ -122,13 +122,13 @@ If you are looking for a client, <a href=https://github.com/core-api>core-api</a
122122
In [3]: client.get('localhost:8000/api')
123123
In [4]: client.get('http://localhost:8000/api')
124124
Out[4]:
125-
OrderedDict([('labels', 'http://localhost:8000/api/v0.0.1/labels'),
125+
OrderedDict([('labels', 'http://localhost:8000/api/labels'),
126126
('organisations',
127-
'http://localhost:8000/api/v0.0.1/organisations'),
128-
('sites', 'http://localhost:8000/api/v0.0.1/sites'),
129-
('jobs', 'http://localhost:8000/api/v0.0.1/jobs')])
127+
'http://localhost:8000/api/organisations'),
128+
('sites', 'http://localhost:8000/api/sites'),
129+
('jobs', 'http://localhost:8000/api/jobs')])
130130

131-
In [5]: client.get('http://localhost:8000/api/v0.0.1/jobs')
131+
In [5]: client.get('http://localhost:8000/api/jobs')
132132
Out[5]:
133133
OrderedDict([('count', 4),
134134
('next', None),

api/test_views.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
class IndexViewTests(APITestCase):
1818
def setUp(self):
19-
self.base_url = "/api/%s" % settings.REST_FRAMEWORK['DEFAULT_VERSION']
19+
self.base_url = "/api"
2020
# TODO: don't be lazy and create roles too, and use a less privileged user
2121
user = User.objects.create_superuser('admin', '[email protected]', 'test123')
2222
self.token = Token.objects.create(user=user)
@@ -42,8 +42,13 @@ def test_index_insecure_redirects(self):
4242
response = self.client.get('/api', secure=False)
4343
self.assertEqual(response.status_code, 301)
4444

45+
# Assert that we will be redirect to "/api" if we hit "/api"
4546
def test_index_view(self):
4647
response = self.client.get('/api', secure=True)
48+
self.assertEqual(response.status_code, 302)
49+
50+
def test_index_view_trailing(self):
51+
response = self.client.get('/api/', secure=True)
4752
self.assertEqual(response.status_code, 200)
4853
self.assertContains(response, "labels")
4954

api/urls.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
from django.conf.urls import url, include
2+
from django.views.generic.base import RedirectView
3+
24
from rest_framework import routers
5+
from rest_framework.schemas import get_schema_view
6+
from rest_framework_swagger.renderers import OpenAPIRenderer, SwaggerUIRenderer
37

48
from . import views
59

10+
schema_view = get_schema_view(title="Vol.community API",
11+
renderer_classes=[OpenAPIRenderer, SwaggerUIRenderer]
12+
)
13+
614
router = routers.DefaultRouter(trailing_slash=False)
715
router.register(r'labels', views.LabelViewSet)
816
router.register(r'organisations', views.OrganisationViewSet)
@@ -12,11 +20,15 @@
1220
# Wire up our API using automatic URL routing.
1321
# Additionally, we include login URLs for the browsable API.
1422
urlpatterns = [
15-
# YOLO clients who do not care about versioning:
16-
url(r'', include(router.urls)),
23+
# Allow clients to explore our API using Swagger
24+
url(r'swagger', schema_view),
25+
26+
# Redirects to /api to make urls generated by django-rest-api and the swagger
27+
# plug work as advertised -without the trailing slash urls would look like:
28+
# "http://127.0.0.1:8000/apijobs" instead of "http://127.0.0.1:8000/api/jobs"
29+
url(r'^$', RedirectView.as_view(url='/api/', permanent=False), name='index'),
1730

18-
# Explicit version for the more discerning clients:
19-
url(r'^/(?P<version>v[0-9+].[0-9]+.[0-9]+)?/', include(router.urls)),
31+
url(r'/', include(router.urls)),
2032

2133
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
2234
]

vol/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class Job(models.Model):
5151
organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE) # One org per job
5252
sites = models.ManyToManyField(Site) # Possibly more sites per job
5353
country = models.CharField(max_length=70) # TODO: move to country table?
54-
region = models.CharField(max_length=70) # TODO: move to region table?
54+
region = models.CharField(max_length=70, null=True) # TODO: move to region table?
5555
city = models.CharField(max_length=70) # TODO: move to city table?
5656
created_at = models.DateTimeField(auto_now_add=True)
5757
updated_at = models.DateTimeField(auto_now=True)

volproject/settings.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
'django.contrib.staticfiles',
5050
'analytical',
5151
'rest_framework',
52-
'rest_framework.authtoken'
52+
'rest_framework.authtoken',
53+
'rest_framework_swagger'
5354
]
5455

5556
MIDDLEWARE = [
@@ -168,7 +169,7 @@
168169
'DEFAULT_PERMISSION_CLASSES': [
169170
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
170171
],
171-
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
172+
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
172173
# What we'll use if no version is specified
173174
'DEFAULT_VERSION': 'v0.0.1',
174175
# Other versions will receive a 404 with body "Invalid version in URL path."

0 commit comments

Comments
 (0)