Skip to content

Commit

Permalink
added the check framework integration for remote api models and fields.
Browse files Browse the repository at this point in the history
  • Loading branch information
darius BERNARD committed Jan 6, 2017
1 parent 2b4982a commit 9734dc3
Show file tree
Hide file tree
Showing 17 changed files with 363 additions and 13 deletions.
5 changes: 2 additions & 3 deletions rest_models/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import, print_function

import logging
from rest_models.checks import register_checks

logger = logging.getLogger(__name__)
register_checks()
5 changes: 3 additions & 2 deletions rest_models/backend/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,8 +771,9 @@ def raise_on_response(self, url, params, response):
if response.status_code == 204:
raise EmptyResultSet
elif response.status_code != 200:
raise ProgrammingError("the query to the api has failed : GET %s \n=> %s" %
(build_url(url, params), message_from_response(response)))
raise ProgrammingError("the query to the api has failed : GET %s/%s \n=> %s" %
(self.connection.connection.url, build_url(url, params),
message_from_response(response)))

def get_meta(self, json, response):
"""
Expand Down
5 changes: 4 additions & 1 deletion rest_models/backend/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ def get_table_list(self, cursor):
"[%s] %s" % (res.request.url, res.status_code, res.text[:500]))
tables = res.json().keys()
for table in tables:
option = cursor.options(table).json()
response = cursor.options(table)
if response.status_code != 200:
raise Exception("bad response from api: %s" % response.text)
option = response.json()
missing_features = self.features - set(option['features'])
if missing_features:
raise Exception("the remote api does not provide all required features : %s" % missing_features)
Expand Down
81 changes: 81 additions & 0 deletions rest_models/checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals

import logging

from django.apps import apps
from django.core.checks import Error, Tags, register

from rest_models.backend.compiler import get_resource_path
from rest_models.router import RestModelRouter

logger = logging.getLogger(__name__)


def api_struct_check(app_configs, **kwargs):
errors = []

all_models = []
if app_configs is None:
all_models.extend(apps.get_models())
else:
for app_config in app_configs:
all_models.extend(app_config.get_models())
router = RestModelRouter()
models = ((router.get_api_connexion(model).cursor(), model)
for model in all_models if router.is_api_model(model))

for db, rest_model in models:
url = get_resource_path(rest_model)
res = db.options(url)
if res.status_code != 200:
errors.append(
Error(
'the remote api does not respond to us. OPTIONS %s/%s => %s' % (db.url, url, res.status_code),
hint='check the url for the remote api or the resource_path',
obj=rest_model,
id='rest_models.E001'
)
)
continue
options = res.json()
missings = {
'include[]', 'exclude[]', 'filter{}', 'page', 'per_page', 'sort[]'
} - set(options.get("features", []))
if missings:
errors.append(
Error(
'the remote api does not support the folowing features: %s' % missings,
hint='is the api on %s/%s running with dynamic-rest ?' % (db.url, url),
obj=rest_model,
id='rest_models.E002'
)
)
continue
for field in rest_model._meta.get_fields():
if field.is_relation:
if router.is_api_model(field.related_model):
if field.name not in options['properties']:
errors.append(
Error(
'the field %s.%s in not present on the remote serializer' % (
rest_model.__name__, field.name
),
obj="%s.%s" % (rest_model.__name__, field.name),
hint='check if the serializer on %s/%s has a field "%s"' % (db.url, url, field.name),
id='rest_models.E003'
)
)
elif field.name not in options['properties']:
errors.append(
Error(
'the field %s.%s in not present on the remote serializer' % (rest_model.__name__, field.name),
hint='check if the serializer on %s/%s has a field "%s"' % (db.url, url, field.name),
id='rest_models.E003'
)
)
return errors


def register_checks():
register(api_struct_check, Tags.models)
10 changes: 10 additions & 0 deletions rest_models/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db import connections

from rest_models.backend.base import DatabaseWrapper

Expand Down Expand Up @@ -74,6 +75,15 @@ def get_api_database(self, model):
self.cache[model] = result
return result

def get_api_connexion(self, model):
"""
return the connection to query the api behind this model
:param model:
:return: the api connection
:rtype: rest_models.backend.base.DatabaseWrapper
"""
return connections[self.get_api_database(model)]

def db_for_read(self, model, **hints):
return self.get_api_database(model)

Expand Down
60 changes: 60 additions & 0 deletions rest_models/tests/test_systemcheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals

import logging

from django.core.management import call_command
from django.core.management.base import SystemCheckError
from django.db.utils import ProgrammingError
from django.test.testcases import TestCase
from django.test.utils import modify_settings, override_settings

logger = logging.getLogger(__name__)


@override_settings(ROOT_URLCONF='testapi.badapi.urls')
@modify_settings(INSTALLED_APPS={
'append': ['testapi.badapi', 'testapp.badapp'],
})
class SystemCheckTest(TestCase):

fixtures = ['user.json']

@classmethod
def setUpClass(cls):
super(SystemCheckTest, cls).setUpClass()
call_command(
'migrate',
verbosity=0,
interactive=False,
test_flush=True)

def setUp(self):
from testapp.badapp import models
self.models = models

def test_query_ok(self):
self.assertEqual(0, self.models.AA.objects.filter(a__name='a').all().count())

def test_query_fail(self):
with self.assertRaisesMessage(ProgrammingError, 'Invalid filter field: aa'):
self.assertEqual(0, self.models.A.objects.filter(aa__name='a').all().count())

def test_check_all_error(self):
with self.assertRaises(SystemCheckError) as ctx:
call_command('check')
msg = ctx.exception.args[0]
self.assertIn('has a field "aa"', msg)
self.assertIn('has a field "bb"', msg)
self.assertIn('OPTIONS http://localapi/api/v1/c => 404', msg)

def test_check_one_error(self):
with self.assertRaises(SystemCheckError) as ctx:
call_command('check', 'badapp')
msg = ctx.exception.args[0]
self.assertIn('has a field "aa"', msg)
self.assertIn('has a field "bb"', msg)
self.assertIn('OPTIONS http://localapi/api/v1/c => 404', msg)

def test_check_one_ok(self):
call_command('check', 'testapp')
1 change: 1 addition & 0 deletions rest_models/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import unicode_literals

import doctest
import os
import tempfile
Expand Down
2 changes: 2 additions & 0 deletions rest_models/tests/tests_routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def test_only_one_api_provided(self):

DB = copy.deepcopy(settings.DATABASES)
del DB['api2']
del DB['apifail']
router.databases = DB
self.assertEqual(router.api_database_name, 'api')

Expand All @@ -45,6 +46,7 @@ def test_no_api_provided(self):
DB = copy.deepcopy(settings.DATABASES)
del DB['api2']
del DB['api']
del DB['apifail']
router.databases = DB
self.assertRaises(ImproperlyConfigured, getattr, router, "api_database_name")

Expand Down
6 changes: 0 additions & 6 deletions testapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import, print_function

import logging

logger = logging.getLogger(__name__)
Empty file added testapi/badapi/__init__.py
Empty file.
51 changes: 51 additions & 0 deletions testapi/badapi/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals

import datetime
import logging

from django.db import models
from django.utils import timezone

logger = logging.getLogger(__name__)


def auto_now_plus_5d():
return timezone.now() + datetime.timedelta(days=5)


class A(models.Model):
name = models.CharField(max_length=135)

def __str__(self):
return self.name # pragma: no cover


class B(models.Model):
name = models.CharField(max_length=135)

def __str__(self):
return self.name # pragma: no cover


class C(models.Model):
name = models.CharField(max_length=135)

def __str__(self):
return self.name # pragma: no cover


class AA(models.Model):
name = models.CharField(max_length=135)
a = models.ForeignKey(A, related_name='aa')

def __str__(self):
return self.name # pragma: no cover


class BB(models.Model):
name = models.CharField(max_length=135)
b = models.ManyToManyField(B, related_name='bb')

def __str__(self):
return self.name # pragma: no cover
39 changes: 39 additions & 0 deletions testapi/badapi/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals

from dynamic_rest.fields.fields import DynamicRelationField
from dynamic_rest.serializers import DynamicModelSerializer

from testapi.badapi.models import AA, BB, A, B


class ASerializer(DynamicModelSerializer):
class Meta:
model = A
name = 'a'
fields = ('id', 'name')


class BSerializer(DynamicModelSerializer):
class Meta:
model = B
name = 'b'
fields = ('id', 'name')


class AASerializer(DynamicModelSerializer):
a = DynamicRelationField(ASerializer, many=False)

class Meta:
model = AA
name = 'aa'
fields = ('id', 'name', 'a')


class BBSerializer(DynamicModelSerializer):
b = DynamicRelationField(BSerializer, many=True)

class Meta:
model = BB
name = 'bb'
fields = ('id', 'name', 'b')
19 changes: 19 additions & 0 deletions testapi/badapi/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals

from django.conf.urls import include, url
from dynamic_rest.routers import DynamicRouter

from .viewset import AAViewSet, AViewSet, BBViewSet, BViewSet

router = DynamicRouter()
router.register('/a', AViewSet)
router.register('/aa', AAViewSet)
router.register('/b', BViewSet)

router.register('/bb', BBViewSet)

urlpatterns = [
url(r'^api/v1', include(router.urls)),
url(r'', include('testapi.urls')),
]
27 changes: 27 additions & 0 deletions testapi/badapi/viewset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals

from dynamic_rest.viewsets import DynamicModelViewSet

from .models import AA, BB, A, B
from .serializers import AASerializer, ASerializer, BBSerializer, BSerializer


class AViewSet(DynamicModelViewSet):
serializer_class = ASerializer
queryset = A.objects.all()


class AAViewSet(DynamicModelViewSet):
serializer_class = AASerializer
queryset = AA.objects.all()


class BViewSet(DynamicModelViewSet):
serializer_class = BSerializer
queryset = B.objects.all()


class BBViewSet(DynamicModelViewSet):
serializer_class = BBSerializer
queryset = BB.objects.all()
Empty file added testapp/badapp/__init__.py
Empty file.
Loading

0 comments on commit 9734dc3

Please sign in to comment.