1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-04 13:15:24 +00:00

fixed failing tests, added body validation serializer decorator and added endpoints to list and create applications

This commit is contained in:
Bram Wiepjes 2019-09-20 17:32:03 +02:00
parent dc0ad9b45b
commit b5df7da903
19 changed files with 514 additions and 48 deletions

View file

@ -0,0 +1,29 @@
from rest_framework import serializers
from baserow.core.applications import registry
from baserow.core.models import Application
class ApplicationSerializer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
class Meta:
model = Application
fields = ('id', 'name', 'order', 'type')
extra_kwargs = {
'id': {
'read_only': True
}
}
def get_type(self, instance):
application = registry.get_by_model(instance.specific_class)
return application.type
class ApplicationCreateSerializer(serializers.ModelSerializer):
type = serializers.ChoiceField(choices=registry.get_types())
class Meta:
model = Application
fields = ('name', 'type')

View file

@ -1,8 +1,10 @@
from django.conf.urls import url
from .views import ApplicationsView
app_name = 'baserow.api.v0.group'
urlpatterns = [
url(r'(?P<group_id>[0-9]+)/$', ApplicationsView.as_view(), name='list')
]

View file

@ -0,0 +1,47 @@
from django.shortcuts import get_object_or_404
from django.db import transaction
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from baserow.api.v0.decorators import validate_body
from baserow.core.models import GroupUser, Application
from baserow.core.handler import CoreHandler
from .serializers import ApplicationSerializer, ApplicationCreateSerializer
class ApplicationsView(APIView):
permission_classes = (IsAuthenticated,)
core_handler = CoreHandler()
def load_group(self, request, group_id):
return get_object_or_404(
GroupUser.objects.select_related('group'),
group_id=group_id,
user=request.user
)
def get(self, request, group_id):
"""
Responds with a list of applications that belong to the group if the user has
access to that group.
"""
group_user = self.load_group(request, group_id)
applications = Application.objects.filter(
group=group_user.group
).select_related('content_type')
serializer = ApplicationSerializer(applications, many=True)
return Response(serializer.data)
@transaction.atomic
@validate_body(ApplicationCreateSerializer)
def post(self, request, data, group_id):
"""Creates a new group for a user."""
group_user = self.load_group(request, group_id)
application = self.core_handler.create_application(
request.user, group_user.group, data['type'], name=data['name'])
return Response(ApplicationSerializer(application).data)

View file

@ -1,5 +1,10 @@
from collections import defaultdict
from django.utils.encoding import force_text
from rest_framework import status
from rest_framework.exceptions import APIException
from rest_framework.request import Request
def map_exceptions(exceptions):
@ -55,3 +60,70 @@ def map_exceptions(exceptions):
raise exc
return func_wrapper
return map_exceptions_decorator
def validate_body(serializer_class):
"""
This decorator can validate the request body using a serializer. If the body is
valid it will add the data to the kwargs. If not it will raise an APIException with
structured details what is wrong.
Example:
class LoginSerializer(serializers.Serializer):
username = serializers.EmailField()
password = serializers.CharField()
@validate_body(LoginSerializer)
def post(self, request):
raise SomeException('This is a test')
HTTP/1.1 400
{
"error": "ERROR_REQUEST_BODY_VALIDATION",
"detail": {
"username": [
{
"error": "This field is required.",
"code": "required"
}
]
}
}
:param serializer_class: The serializer that must be used for validating.
:type serializer_class: Serializer
"""
def validate_decorator(func):
def func_wrapper(*args, **kwargs):
# Check if the request
if len(args) < 2 or not isinstance(args[1], Request):
raise ValueError('There must be a request in the kwargs.')
request = args[1]
serializer = serializer_class(data=request.data)
if not serializer.is_valid():
# Create a serialized detail on why the validation failed.
detail = defaultdict(list)
for key, errors in serializer.errors.items():
for error in errors:
detail[key].append({
'error': force_text(error),
'code': error.code
})
exc = APIException({
'error': 'ERROR_REQUEST_BODY_VALIDATION',
'detail': detail
})
exc.status_code = 400
raise exc
# We do not want to override already existing data value in the kwargs.
if 'data' in kwargs:
raise ValueError('The data attribute is already in the kwargs.')
kwargs['data'] = serializer.data
return func(*args, **kwargs)
return func_wrapper
return validate_decorator

View file

@ -5,6 +5,7 @@ from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from baserow.api.v0.decorators import validate_body
from baserow.core.models import GroupUser
from baserow.core.handler import CoreHandler
@ -22,14 +23,10 @@ class GroupsView(APIView):
return Response(serializer.data)
@transaction.atomic
def post(self, request):
@validate_body(GroupSerializer)
def post(self, request, data):
"""Creates a new group for a user."""
serializer = GroupSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.data
group_user = self.core_handler.create_group(request.user, name=data['name'])
return Response(GroupUserSerializer(group_user).data)
@ -38,14 +35,15 @@ class GroupView(APIView):
core_handler = CoreHandler()
@transaction.atomic
def patch(self, request, group_id):
@validate_body(GroupSerializer)
def patch(self, request, data, group_id):
"""Updates the group if it belongs to a user."""
group_user = get_object_or_404(GroupUser, group_id=group_id, user=request.user)
group_user = get_object_or_404(
GroupUser.objects.select_for_update(),
group_id=group_id,
user=request.user
)
serializer = GroupSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.data
group_user.group = self.core_handler.update_group(
request.user, group_user.group, name=data['name'])
@ -63,11 +61,8 @@ class GroupOrderView(APIView):
permission_classes = (IsAuthenticated,)
core_handler = CoreHandler()
def post(self, request):
@validate_body(OrderGroupsSerializer)
def post(self, request, data):
"""Updates to order of some groups for a user."""
serializer = OrderGroupsSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.core_handler.order_groups(request.user, serializer.data['groups'])
self.core_handler.order_groups(request.user, data['groups'])
return Response(status=204)

View file

@ -5,7 +5,7 @@ from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework_jwt.settings import api_settings
from baserow.api.v0.decorators import map_exceptions
from baserow.api.v0.decorators import map_exceptions, validate_body
from baserow.user.handler import UserHandler
from baserow.user.exceptions import UserAlreadyExist
@ -25,11 +25,9 @@ class UserView(APIView):
@map_exceptions({
UserAlreadyExist: ERROR_ALREADY_EXISTS
})
def post(self, request):
serializer = RegisterSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.data
@validate_body(RegisterSerializer)
def post(self, request, data):
"""Registers a new user."""
user = self.user_handler.create_user(name=data['name'], email=data['email'],
password=data['password'])

View file

@ -0,0 +1,31 @@
# Generated by Django 2.2.2 on 2019-09-13 12:08
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('core', '0002_application'),
]
operations = [
migrations.CreateModel(
name='Database',
fields=[
('application_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.Application')),
],
bases=('core.application',),
),
migrations.CreateModel(
name='Table',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.PositiveIntegerField()),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='database.Database')),
],
),
]

View file

@ -7,12 +7,11 @@ class Application(object):
"""
This abstract class represents a custom application that can be added to the
application registry. It must be extended so customisation can be done. Each
application will have his own model that must extend the ApplicationModel, this is
needed to that the user can set custom settings per application instance he has
created.
application will have his own model that must extend the Application, this is needed
to that the user can set custom settings per application instance he has created.
Example:
from baserow.core.models import ApplicationModel
from baserow.core.models import Application as ApplicationModel
from baserow.core.applications import Application, registry
class ExampleApplicationModel(ApplicationModel):
@ -90,6 +89,32 @@ class ApplicationRegistry(object):
return self.registry[type]
def get_by_model(self, instance):
"""Returns the application instance of a model or model instance.
:param instance: The modal that must be the applications model_instance.
:type instance: Model or an instance of model.
:return: The registered application instance.
:rtype: Application
"""
for value in self.registry.values():
if value.instance_model == instance \
or isinstance(instance, value.instance_model):
return value
raise ApplicationTypeDoesNotExist(f'The application with model instance '
f'{instance} does not exist. ')
def get_types(self):
"""
Returns a list of available type names.
:return: A list of available types.
:rtype: List
"""
return list(self.registry.keys())
def register(self, application):
"""
Registers a new application in the registry.

View file

@ -0,0 +1,31 @@
# Generated by Django 2.2.2 on 2019-09-13 12:08
import baserow.core.mixins
import baserow.core.models
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Application',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('order', models.PositiveIntegerField()),
('content_type', models.ForeignKey(on_delete=models.SET(baserow.core.models.get_default_application_content_type), related_name='applications', to='contenttypes.ContentType', verbose_name='content type')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Group')),
],
options={
'ordering': ('order',),
},
bases=(baserow.core.mixins.OrderableMixin, models.Model),
),
]

View file

@ -1,6 +1,7 @@
from django.db import models
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.functional import cached_property
from .managers import GroupQuerySet
from .mixins import OrderableMixin
@ -51,6 +52,9 @@ class Application(OrderableMixin, models.Model):
on_delete=models.SET(get_default_application_content_type)
)
class Meta:
ordering = ('order',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -58,6 +62,29 @@ class Application(OrderableMixin, models.Model):
if not self.content_type_id:
self.content_type = ContentType.objects.get_for_model(self)
@cached_property
def specific(self):
"""
Return this page in its most specific subclassed form.
"""
content_type = ContentType.objects.get_for_id(self.content_type_id)
model_class = self.specific_class
if model_class is None:
return self
elif isinstance(self, model_class):
return self
else:
return content_type.get_object_for_this_type(id=self.id)
@cached_property
def specific_class(self):
"""
Return the class that this application would be if instantiated in its
most specific form
"""
content_type = ContentType.objects.get_for_id(self.content_type_id)
return content_type.model_class()
@classmethod
def get_last_order(cls, group):
return cls.get_highest_order_of_queryset(

View file

@ -0,0 +1,94 @@
import pytest
from django.shortcuts import reverse
from baserow.contrib.database.models import Database
@pytest.mark.django_db
def test_list_applications(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
email='test@test.nl', password='password', first_name='Test1')
group_1 = data_fixture.create_group(user=user)
group_2 = data_fixture.create_group()
application_1 = data_fixture.create_database_application(group=group_1, order=1)
application_2 = data_fixture.create_database_application(group=group_1, order=3)
application_3 = data_fixture.create_database_application(group=group_1, order=2)
data_fixture.create_database_application(group=group_2, order=1)
response = api_client.get(
reverse('api_v0:applications:list', kwargs={'group_id': group_1.id}), **{
'HTTP_AUTHORIZATION': f'JWT {token}'
}
)
assert response.status_code == 200
response_json = response.json()
assert len(response_json) == 3
assert response_json[0]['id'] == application_1.id
assert response_json[0]['type'] == 'database'
assert response_json[1]['id'] == application_3.id
assert response_json[1]['type'] == 'database'
assert response_json[2]['id'] == application_2.id
assert response_json[2]['type'] == 'database'
response = api_client.get(
reverse('api_v0:applications:list', kwargs={'group_id': group_2.id}), **{
'HTTP_AUTHORIZATION': f'JWT {token}'
}
)
assert response.status_code == 404
@pytest.mark.django_db
def test_create_application(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
user_2, token_2 = data_fixture.create_user_and_token()
group = data_fixture.create_group(user=user)
group_2 = data_fixture.create_group(user=user_2)
response = api_client.post(
reverse('api_v0:applications:list', kwargs={'group_id': group.id}),
{
'name': 'Test 1',
'type': 'NOT_EXISTING'
},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
response_json = response.json()
assert response.status_code == 400
assert response_json['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
assert response_json['detail']['type'][0]['code'] == 'invalid_choice'
response = api_client.post(
reverse('api_v0:applications:list', kwargs={'group_id': group_2.id}),
{
'name': 'Test 1',
'type': 'database'
},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
assert response.status_code == 404
response = api_client.post(
reverse('api_v0:applications:list', kwargs={'group_id': group.id}),
{
'name': 'Test 1',
'type': 'database'
},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
response_json = response.json()
assert response.status_code == 200
assert response_json['type'] == 'database'
database = Database.objects.filter()[0]
assert response_json['id'] == database.id
assert response_json['name'] == database.name
assert response_json['order'] == database.order

View file

@ -9,17 +9,17 @@ from baserow.core.models import Group, GroupUser
def test_list_groups(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
email='test@test.nl', password='password', first_name='Test1')
group_2 = data_fixture.create_user_group(user=user, order=2)
group_1 = data_fixture.create_user_group(user=user, order=1)
user_group_2 = data_fixture.create_user_group(user=user, order=2)
user_group_1 = data_fixture.create_user_group(user=user, order=1)
response = api_client.get(reverse('api_v0:groups:list'), **{
'HTTP_AUTHORIZATION': f'JWT {token}'
})
assert response.status_code == 200
response_json = response.json()
assert response_json[0]['id'] == group_1.id
assert response_json[0]['id'] == user_group_1.group.id
assert response_json[0]['order'] == 1
assert response_json[1]['id'] == group_2.id
assert response_json[1]['id'] == user_group_2.group.id
assert response_json[1]['order'] == 2

View file

@ -1,15 +1,28 @@
import pytest
import json
from rest_framework import status
from unittest.mock import MagicMock
from django.http.request import HttpRequest
from rest_framework import status, serializers
from rest_framework.request import Request
from rest_framework.parsers import JSONParser
from rest_framework.exceptions import APIException
from rest_framework.test import APIRequestFactory
from baserow.api.v0.decorators import map_exceptions
from baserow.api.v0.decorators import map_exceptions, validate_body
class TemporaryException(Exception):
pass
class TemporarySerializer(serializers.Serializer):
field_1 = serializers.CharField()
field_2 = serializers.ChoiceField(choices=('choice_1', 'choice_2'))
def test_map_exceptions():
@map_exceptions({
TemporaryException: 'ERROR_TEMPORARY'
@ -44,3 +57,50 @@ def test_map_exceptions():
pass
test_3()
def test_validate_body():
factory = APIRequestFactory()
request = Request(factory.post(
'/some-page/',
data=json.dumps({'field_1': 'test'}),
content_type='application/json'
), parsers=[JSONParser()])
func = MagicMock()
with pytest.raises(APIException) as api_exception_1:
validate_body(TemporarySerializer)(func)(*[object, request])
assert api_exception_1.value.detail['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
assert api_exception_1.value.detail['detail']['field_2'][0]['error'] == \
'This field is required.'
assert api_exception_1.value.detail['detail']['field_2'][0]['code'] == 'required'
assert api_exception_1.value.status_code == status.HTTP_400_BAD_REQUEST
request = Request(factory.post(
'/some-page/',
data=json.dumps({'field_1': 'test', 'field_2': 'wrong'}),
content_type='application/json'
), parsers=[JSONParser()])
func = MagicMock()
with pytest.raises(APIException) as api_exception_1:
validate_body(TemporarySerializer)(func)(*[object, request])
assert api_exception_1.value.detail['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
assert api_exception_1.value.detail['detail']['field_2'][0]['error'] == \
'"wrong" is not a valid choice.'
assert api_exception_1.value.detail['detail']['field_2'][0]['code'] == \
'invalid_choice'
assert api_exception_1.value.status_code == status.HTTP_400_BAD_REQUEST
request = Request(factory.post(
'/some-page/',
data=json.dumps({'field_1': 'test', 'field_2': 'choice_1'}),
content_type='application/json'
), parsers=[JSONParser()])
func = MagicMock()
validate_body(TemporarySerializer)(func)(*[object, request])

View file

@ -1,17 +1,33 @@
import pytest
from baserow.core.applications import Application, ApplicationRegistry
from baserow.core.exceptions import ApplicationAlreadyRegistered
from baserow.core.exceptions import (
ApplicationAlreadyRegistered, ApplicationTypeDoesNotExist
)
class FakeModel(object):
pass
class FakeModel2(object):
pass
class TemporaryApplication1(Application):
type = 'temporary_1'
instance_model = object
instance_model = FakeModel
def get_api_urls(self):
return ['url_1', 'url_2']
class TemporaryApplication2(Application):
type = 'temporary_2'
instance_model = object
instance_model = FakeModel2
def get_api_urls(self):
return ['url_3']
def test_application_registry_register():
@ -42,3 +58,31 @@ def test_application_registry_register():
with pytest.raises(ValueError):
registry.unregister(000)
def test_application_registry_get():
temporary_1 = TemporaryApplication1()
registry = ApplicationRegistry()
registry.register(temporary_1)
assert registry.get('temporary_1') == temporary_1
with pytest.raises(ApplicationTypeDoesNotExist):
registry.get('something')
assert registry.get_by_model(FakeModel) == temporary_1
assert registry.get_by_model(FakeModel()) == temporary_1
with pytest.raises(ApplicationTypeDoesNotExist):
registry.get_by_model(FakeModel2)
with pytest.raises(ApplicationTypeDoesNotExist):
registry.get_by_model(FakeModel2())
def test_application_get_api_urls():
temporary_1 = TemporaryApplication1()
temporary_2 = TemporaryApplication2()
registry = ApplicationRegistry()
registry.register(temporary_1)
registry.register(temporary_2)
assert registry.api_urls == ['url_1', 'url_2', 'url_3']

View file

@ -84,7 +84,7 @@ def test_order_groups(data_fixture):
assert [1, 2, 3] == [ug_1.order, ug_2.order, ug_3.order]
handler = CoreHandler()
handler.order_groups(user, [ug_3.id, ug_2.id, ug_1.id])
handler.order_groups(user, [ug_3.group.id, ug_2.group.id, ug_1.group.id])
ug_1.refresh_from_db()
ug_2.refresh_from_db()
@ -92,7 +92,7 @@ def test_order_groups(data_fixture):
assert [1, 2, 3] == [ug_3.order, ug_2.order, ug_1.order]
handler.order_groups(user, [ug_2.id, ug_1.id, ug_3.id])
handler.order_groups(user, [ug_2.group.id, ug_1.group.id, ug_3.group.id])
ug_1.refresh_from_db()
ug_2.refresh_from_db()

View file

@ -6,19 +6,20 @@ from baserow.core.models import Group
@pytest.mark.django_db
def test_groups_of_user(data_fixture):
user_1 = data_fixture.create_user()
group_user_1 = data_fixture.create_user_group(user=user_1, order=1)
group_user_2 = data_fixture.create_user_group(user=user_1, order=2)
group_user_3 = data_fixture.create_user_group(user=user_1, order=0)
user_group_1 = data_fixture.create_user_group(user=user_1, order=1)
user_group_2 = data_fixture.create_user_group(user=user_1, order=2)
user_group_3 = data_fixture.create_user_group(user=user_1, order=0)
user_2 = data_fixture.create_user()
group_user_4 = data_fixture.create_user_group(user=user_2, order=0)
user_group_4 = data_fixture.create_user_group(user=user_2, order=0)
groups_user_1 = Group.objects.of_user(user=user_1)
assert len(groups_user_1) == 3
assert groups_user_1[0].id == group_user_3.id
assert groups_user_1[1].id == group_user_1.id
assert groups_user_1[2].id == group_user_2.id
assert groups_user_1[0].id == user_group_3.group.id
assert groups_user_1[1].id == user_group_1.group.id
assert groups_user_1[2].id == user_group_2.group.id
groups_user_2 = Group.objects.of_user(user=user_2)
assert len(groups_user_2) == 1
assert groups_user_2[0].id == group_user_4.id
assert groups_user_2[0].id == user_group_4.group.id

View file

@ -1,6 +1,7 @@
import pytest
from baserow.core.models import GroupUser
from baserow.contrib.database.models import Database
@pytest.mark.django_db
@ -24,3 +25,12 @@ def test_group_has_user(data_fixture):
assert user_group.group.has_user(user_group.user)
assert not user_group.group.has_user(user)
@pytest.mark.django_db
def test_application_content_type_init(data_fixture):
group = data_fixture.create_group()
database = Database.objects.create(name='Test 1', order=0, group=group)
assert database.content_type.app_label == 'database'
assert database.content_type.model == 'database'