Skip to content

Commit

Permalink
Use URLPathVersioning instead of NamespaceVersioning
Browse files Browse the repository at this point in the history
They are essentially equivalent, the api version is still determined via the
URL. The biggest change is how URLs are reversed. DRF OpenAPI currently only
supports URL path versioning so switch to it for testing.

http://www.django-rest-framework.org/api-guide/versioning/#urlpathversioning
  • Loading branch information
djanderson committed Dec 22, 2017
1 parent e16c28a commit 97edfb1
Show file tree
Hide file tree
Showing 37 changed files with 2,267 additions and 222 deletions.
12 changes: 7 additions & 5 deletions docker/docker-compose.yml
Expand Up @@ -39,11 +39,7 @@ services:
- ${SSL_KEY_PATH}:/etc/ssl/private/ssl-cert.key:ro

# This is a stop-gap until Docker adds the capability to restart unhealthy
# containers natively. This container mounts the host's docker unix socket,
# which in theory introduces a root privilege escalation vector, but since
# this image isn't internet-facing, it's Probably Okay (tm), and definitely
# a better temporary solution than mounting the socket directly in the api
# container.
# containers natively.
#
# https://github.com/moby/moby/issues/28400
# https://github.com/willfarrell/docker-autoheal
Expand All @@ -56,3 +52,9 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- ./autoheal_entrypoint.sh:/docker/autoheal_entrypoint.sh:ro
command: /docker/autoheal_entrypoint.sh

ws_logger:
restart: always
image: gliderlabs/logspout
volumes:
- /var/run/docker.sock:/var/run/docker.sock
28 changes: 23 additions & 5 deletions gunicorn/config.py
Expand Up @@ -11,13 +11,31 @@
loglevel = os.environ.get('GUNICORN_LOG_LEVEL', 'info')


def worker_exit(server, worker):
"""Notify worker process's scheduler thread that it needs to shut down."""
def _modify_path():
"""Ensure Django project is on sys.path."""
from os import path

PATH = path.join(path.dirname(path.abspath(__file__)), '..', '/src')
sys.path.append(PATH)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sensor.settings")
project_path = path.join(path.dirname(path.abspath(__file__)), '../src')
if project_path not in sys.path:
sys.path.append(project_path)


def post_worker_init(worker):
"""Start scheduler in worker."""
_modify_path()
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sensor.settings')

import django
django.setup()

from scheduler import scheduler
scheduler.thread.start()


def worker_exit(server, worker):
"""Notify worker process's scheduler thread that it needs to shut down."""
_modify_path()
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sensor.settings')

import django
django.setup()
Expand Down
4 changes: 4 additions & 0 deletions nginx/conf.template
Expand Up @@ -7,6 +7,7 @@ upstream wsgi-server {
server api:8000 fail_timeout=0;
}


server {
listen 80 default_server;
listen [::]:80 default_server;
Expand All @@ -17,6 +18,7 @@ server {
return 307 https://$host$request_uri;
}


server {
# SSL configuration
listen 443 ssl;
Expand All @@ -35,6 +37,8 @@ server {
# path for static files
root /var/www/scos-sensor/;

location = /favicon.ico { access_log off; log_not_found off; }

location / {
# checks for static file, if not found proxy to wsgi server
try_files $uri @proxy_to_wsgi_server;
Expand Down
2 changes: 2 additions & 0 deletions scripts/build.sh
@@ -1,3 +1,5 @@
#!/bin/bash

REPO_ROOT=$(git rev-parse --show-toplevel)

docker build -f ${REPO_ROOT}/Dockerfile -t ntiaits/test_scossensor_api ${REPO_ROOT}
11 changes: 6 additions & 5 deletions src/acquisitions/serializers.py
Expand Up @@ -18,7 +18,7 @@ class Meta:
)
extra_kwargs = {
'url': {
'view_name': 'v1:acquisition-list',
'view_name': 'acquisition-list',
'lookup_field': 'name',
'lookup_url_kwarg': 'schedule_entry_name'
}
Expand All @@ -29,7 +29,8 @@ def get_acquisitions_available(self, obj):

def get_schedule_entry(self, obj):
request = self.context['request']
return reverse('v1:schedule-detail', args=(obj.name,), request=request)
kwargs = {'pk': obj.name}
return reverse('schedule-detail', kwargs=kwargs, request=request)


class AcquisitionHyperlinkedRelatedField(serializers.HyperlinkedRelatedField):
Expand All @@ -44,12 +45,12 @@ def get_url(self, obj, view_name, request, format):

class AcquisitionSerializer(serializers.ModelSerializer):
url = AcquisitionHyperlinkedRelatedField(
view_name='v1:acquisition-detail',
view_name='acquisition-detail',
read_only=True,
source='*' # pass whole object
)
archive = AcquisitionHyperlinkedRelatedField(
view_name='v1:acquisition-archive',
view_name='acquisition-archive',
read_only=True,
source='*' # pass whole object
)
Expand All @@ -66,7 +67,7 @@ class Meta:
)
extra_kwargs = {
'schedule_entry': {
'view_name': 'v1:schedule-detail',
'view_name': 'schedule-detail',
'lookup_field': 'name'
}
}
5 changes: 4 additions & 1 deletion src/acquisitions/tests/test_detail_view.py
Expand Up @@ -8,6 +8,7 @@
simulate_acquisitions,
HTTPS_KWARG
)
from sensor import V1
from sensor.tests.utils import validate_response


Expand Down Expand Up @@ -58,7 +59,9 @@ def test_delete_single(user_client, testclock):
def test_private_entries_have_private_acquisitons(admin_client, user_client,
testclock):
entry_name = simulate_acquisitions(admin_client, is_private=True)
entry_url = reverse('v1:schedule-detail', [entry_name])
kws = {'pk': entry_name}
kws.update(V1)
entry_url = reverse('schedule-detail', kwargs=kws)

admin_response = admin_client.get(entry_url, **HTTPS_KWARG)
admin_acquisition_url = admin_response.data['acquisitions']
Expand Down
13 changes: 9 additions & 4 deletions src/acquisitions/tests/utils.py
Expand Up @@ -6,8 +6,10 @@
from acquisitions.tests import SINGLE_ACQUISITION, MULTIPLE_ACQUISITIONS
from schedule.tests.utils import post_schedule
from scheduler.tests.utils import simulate_scheduler_run
from sensor import V1
from sensor.tests.utils import validate_response


HTTPS_KWARG = {'wsgi.url_scheme': 'https'}


Expand Down Expand Up @@ -36,16 +38,17 @@ def reverse_acquisitions_overview():

request = rf.get('/api/v1/acquisitions', **HTTPS_KWARG)

return reverse('v1:acquisitions-overview', request=request)
return reverse('acquisitions-overview', kwargs=V1, request=request)


def reverse_acquisition_list(schedule_entry_name):
rf = RequestFactory()

request = rf.get('/acquisitions/' + schedule_entry_name, **HTTPS_KWARG)
kws = {'schedule_entry_name': schedule_entry_name}
kws.update(V1)

return reverse('v1:acquisition-list', kwargs=kws, request=request)
return reverse('acquisition-list', kwargs=kws, request=request)


def reverse_acquisition_detail(schedule_entry_name, task_id):
Expand All @@ -54,8 +57,9 @@ def reverse_acquisition_detail(schedule_entry_name, task_id):
request = rf.get(
'/acquisitions/' + schedule_entry_name + '/' + task_str, **HTTPS_KWARG)
kws = {'schedule_entry_name': schedule_entry_name, 'task_id': task_id}
kws.update(V1)

return reverse('v1:acquisition-detail', kwargs=kws, request=request)
return reverse('acquisition-detail', kwargs=kws, request=request)


def reverse_acquisition_archive(schedule_entry_name, task_id):
Expand All @@ -65,8 +69,9 @@ def reverse_acquisition_archive(schedule_entry_name, task_id):
url_str = '/'.join(['/acquisitions', entry_name, task_str, 'archive'])
request = rf.get(url_str, **HTTPS_KWARG)
kws = {'schedule_entry_name': entry_name, 'task_id': task_id}
kws.update(V1)

return reverse('v1:acquisition-archive', kwargs=kws, request=request)
return reverse('acquisition-archive', kwargs=kws, request=request)


def get_acquisitions_overview(client):
Expand Down
4 changes: 2 additions & 2 deletions src/acquisitions/views.py
Expand Up @@ -59,7 +59,7 @@ class AcquisitionListViewSet(MultipleFieldLookupMixin,
lookup_fields = ('schedule_entry__name', 'task_id')

@list_route(methods=('delete',))
def destroy_all(self, request, schedule_entry_name):
def destroy_all(self, request, version, schedule_entry_name):
queryset = self.get_queryset()
queryset = queryset.filter(schedule_entry__name=schedule_entry_name)

Expand All @@ -82,7 +82,7 @@ class AcquisitionInstanceViewSet(MultipleFieldLookupMixin,
lookup_fields = ('schedule_entry__name', 'task_id')

@detail_route()
def archive(self, request, schedule_entry_name, task_id):
def archive(self, request, version, schedule_entry_name, task_id):
entry_name = schedule_entry_name
acq = self.get_object()

Expand Down
4 changes: 2 additions & 2 deletions src/authentication/serializers.py
Expand Up @@ -18,13 +18,13 @@ class Meta:
)
extra_kwargs = {
'url': {
'view_name': 'v1:user-detail'
'view_name': 'user-detail'
},
'is_active': {
'initial': True
},
'schedule_entries': {
'view_name': 'v1:schedule-detail'
'view_name': 'schedule-detail'
},
}
read_only_fields = (
Expand Down
1 change: 1 addition & 0 deletions src/authentication/views.py
Expand Up @@ -37,6 +37,7 @@ class UserProfilesListView(ListAPIView):

class UserInstanceView(APIView):
def dispatch(self, request, *args, **kwargs):
kwargs.pop('version', None)
if not kwargs: # /users/me
kwargs = {'pk': request.user.pk}

Expand Down
2 changes: 1 addition & 1 deletion src/capabilities/views.py
Expand Up @@ -35,7 +35,7 @@ def get_capabilities():


@api_view()
def capabilities(request, format=None):
def capabilities(request, version, format=None):
capabilities = get_capabilities()
filtered_actions = get_actions(include_admin_actions=request.user.is_staff)
capabilities['actions'] = filtered_actions
Expand Down
5 changes: 2 additions & 3 deletions src/requirements.txt
Expand Up @@ -4,9 +4,8 @@ Markdown==2.6.9
coreapi==2.3.1
docker-compose==1.16.1
django-extensions==1.8.1
django-rest-swagger==2.1.2
django-sslserver==0.20
djangorestframework==3.6.3
djangorestframework==3.7.7
drf-openapi==1.2.0
futures==3.1.1
gunicorn==19.7.1
jsonfield==2.0.2
Expand Down
8 changes: 5 additions & 3 deletions src/schedule/serializers.py
Expand Up @@ -2,6 +2,7 @@
from rest_framework.reverse import reverse

import actions
from sensor import V1
from .models import ScheduleEntry


Expand Down Expand Up @@ -32,10 +33,10 @@ class Meta:
)
extra_kwargs = {
'url': {
'view_name': 'v1:schedule-detail'
'view_name': 'schedule-detail'
},
'owner': {
'view_name': 'v1:user-detail'
'view_name': 'user-detail'
}
}
read_only_fields = ('is_active', 'is_private')
Expand All @@ -44,7 +45,8 @@ class Meta:
def get_acquisitions(self, obj):
request = self.context['request']
kws = {'schedule_entry_name': obj.name}
return reverse('v1:acquisition-list', kwargs=kws, request=request)
kws.update(V1)
return reverse('acquisition-list', kwargs=kws, request=request)

def to_internal_value(self, data):
"""Strip incoming start=None so that model uses default start value."""
Expand Down
23 changes: 16 additions & 7 deletions src/schedule/tests/test_admin_views.py
Expand Up @@ -3,6 +3,7 @@

from schedule.tests import TEST_SCHEDULE_ENTRY, TEST_PRIVATE_SCHEDULE_ENTRY
from schedule.tests.utils import post_schedule, update_schedule
from sensor import V1
from sensor.tests.utils import validate_response


Expand All @@ -12,7 +13,9 @@
def test_post_admin_private_schedule(admin_client):
rjson = post_schedule(admin_client, TEST_PRIVATE_SCHEDULE_ENTRY)
entry_name = rjson['name']
entry_url = reverse('v1:schedule-detail', [entry_name])
kws = {'pk': entry_name}
kws.update(V1)
entry_url = reverse('schedule-detail', kwargs=kws)
admin_user_respose = admin_client.get(entry_url, **HTTPS_KWARG)

for k, v in TEST_SCHEDULE_ENTRY.items():
Expand All @@ -28,14 +31,17 @@ def test_admin_can_view_all_entries(admin_client, user_client,
# user schedule entry
user_rjson = post_schedule(user_client, TEST_SCHEDULE_ENTRY)
user_entry_name = user_rjson['name']
user_url = reverse('v1:schedule-detail', [user_entry_name])
kws = {'pk': user_entry_name}
kws.update(V1)
user_url = reverse('schedule-detail', kwargs=kws)

# alternate admin user schedule entry
alternate_admin_rjson = post_schedule(
alternate_admin_client, TEST_PRIVATE_SCHEDULE_ENTRY)
alternate_admin_entry_name = alternate_admin_rjson['name']
alternate_admin_url = reverse(
'v1:schedule-detail', [alternate_admin_entry_name])
kws = {'pk': alternate_admin_entry_name}
kws.update(V1)
alternate_admin_url = reverse('schedule-detail', kwargs=kws)

response = admin_client.get(user_url, **HTTPS_KWARG)
validate_response(response, status.HTTP_200_OK)
Expand All @@ -49,14 +55,17 @@ def test_admin_can_delete_all_entries(admin_client, user_client,
# user schedule entry
user_rjson = post_schedule(user_client, TEST_SCHEDULE_ENTRY)
user_entry_name = user_rjson['name']
user_url = reverse('v1:schedule-detail', [user_entry_name])
kws = {'pk': user_entry_name}
kws.update(V1)
user_url = reverse('schedule-detail', kwargs=kws)

# admin user schedule entry
alternate_admin_rjson = post_schedule(
alternate_admin_client, TEST_PRIVATE_SCHEDULE_ENTRY)
alternate_admin_entry_name = alternate_admin_rjson['name']
alternate_admin_url = reverse(
'v1:schedule-detail', [alternate_admin_entry_name])
kws = {'pk': alternate_admin_entry_name}
kws.update(V1)
alternate_admin_url = reverse('schedule-detail', kwargs=kws)

response = admin_client.delete(user_url, **HTTPS_KWARG)
validate_response(response, status.HTTP_204_NO_CONTENT)
Expand Down

0 comments on commit 97edfb1

Please sign in to comment.