mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-04 21:25:24 +00:00
Merge branch '11-add-application-abstraction-and-possibility-to-add-application-instances-in-the-frontend' into 'develop'
Resolve "Add application abstraction and possibility to add application instances in the frontend." Closes #11 See merge request bramw/baserow!6
This commit is contained in:
commit
e1fc7ac922
42 changed files with 1317 additions and 83 deletions
backend
src/baserow
api/v0
config/settings
contrib
__init__.py
database
core
tests
|
@ -1 +1 @@
|
|||
app_name = 'baserow.api.v0'
|
||||
default_app_config = 'baserow.api.v0.config.ApiConfig'
|
||||
|
|
0
backend/src/baserow/api/v0/applications/__init__.py
Normal file
0
backend/src/baserow/api/v0/applications/__init__.py
Normal file
1
backend/src/baserow/api/v0/applications/errors.py
Normal file
1
backend/src/baserow/api/v0/applications/errors.py
Normal file
|
@ -0,0 +1 @@
|
|||
ERROR_USER_NOT_IN_GROUP = 'ERROR_USER_NOT_IN_GROUP'
|
35
backend/src/baserow/api/v0/applications/serializers.py
Normal file
35
backend/src/baserow/api/v0/applications/serializers.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
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')
|
||||
|
||||
|
||||
class ApplicationUpdateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Application
|
||||
fields = ('name',)
|
11
backend/src/baserow/api/v0/applications/urls.py
Normal file
11
backend/src/baserow/api/v0/applications/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from .views import ApplicationsView, ApplicationView
|
||||
|
||||
|
||||
app_name = 'baserow.api.v0.group'
|
||||
|
||||
urlpatterns = [
|
||||
url(r'group/(?P<group_id>[0-9]+)/$', ApplicationsView.as_view(), name='list'),
|
||||
url(r'(?P<application_id>[0-9]+)/$', ApplicationView.as_view(), name='item'),
|
||||
]
|
89
backend/src/baserow/api/v0/applications/views.py
Normal file
89
backend/src/baserow/api/v0/applications/views.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
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, map_exceptions
|
||||
from baserow.core.models import GroupUser, Application
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.exceptions import UserNotIngroupError
|
||||
|
||||
from .serializers import (
|
||||
ApplicationSerializer, ApplicationCreateSerializer, ApplicationUpdateSerializer
|
||||
)
|
||||
from .errors import ERROR_USER_NOT_IN_GROUP
|
||||
|
||||
|
||||
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 application 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)
|
||||
|
||||
|
||||
class ApplicationView(APIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
core_handler = CoreHandler()
|
||||
|
||||
@transaction.atomic
|
||||
@validate_body(ApplicationUpdateSerializer)
|
||||
@map_exceptions({
|
||||
UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
|
||||
})
|
||||
def patch(self, request, data, application_id):
|
||||
"""Updates the application if the user belongs to the group."""
|
||||
|
||||
application = get_object_or_404(
|
||||
Application.objects.select_related('group').select_for_update(),
|
||||
pk=application_id
|
||||
)
|
||||
application = self.core_handler.update_application(
|
||||
request.user, application, name=data['name'])
|
||||
|
||||
return Response(ApplicationSerializer(application).data)
|
||||
|
||||
@transaction.atomic
|
||||
@map_exceptions({
|
||||
UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
|
||||
})
|
||||
def delete(self, request, application_id):
|
||||
"""Deletes an existing application if the user belongs to the group."""
|
||||
|
||||
application = get_object_or_404(
|
||||
Application.objects.select_related('group'),
|
||||
pk=application_id
|
||||
)
|
||||
self.core_handler.delete_application(request.user, application)
|
||||
|
||||
return Response(status=204)
|
|
@ -1,9 +1,15 @@
|
|||
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):
|
||||
"""This decorator easily maps specific exceptions to a standard api response.
|
||||
"""
|
||||
This decorator simplifies mapping specific exceptions to a standard api response.
|
||||
|
||||
Example:
|
||||
@map_exceptions({ SomeException: 'ERROR_1' })
|
||||
|
@ -54,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 about 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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -17,19 +18,17 @@ class GroupsView(APIView):
|
|||
|
||||
def get(self, request):
|
||||
"""Responds with a list of groups where the users takes part in."""
|
||||
|
||||
groups = GroupUser.objects.filter(user=request.user).select_related('group')
|
||||
serializer = GroupUserSerializer(groups, many=True)
|
||||
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 +37,16 @@ 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)
|
||||
|
||||
serializer = GroupSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
group_user = get_object_or_404(
|
||||
GroupUser.objects.select_for_update(),
|
||||
group_id=group_id,
|
||||
user=request.user
|
||||
)
|
||||
|
||||
data = serializer.data
|
||||
group_user.group = self.core_handler.update_group(
|
||||
request.user, group_user.group, name=data['name'])
|
||||
|
||||
|
@ -54,6 +55,7 @@ class GroupView(APIView):
|
|||
@transaction.atomic
|
||||
def delete(self, request, group_id):
|
||||
"""Deletes an existing group if it belongs to a user."""
|
||||
|
||||
group_user = get_object_or_404(GroupUser, group_id=group_id, user=request.user)
|
||||
self.core_handler.delete_group(request.user, group_user.group)
|
||||
return Response(status=204)
|
||||
|
@ -63,11 +65,9 @@ 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)
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
from django.urls import path, include
|
||||
|
||||
from baserow.core.applications import registry
|
||||
|
||||
from .user import urls as user_urls
|
||||
from .groups import urls as group_urls
|
||||
from .applications import urls as application_urls
|
||||
|
||||
|
||||
app_name = 'baserow.api.v0'
|
||||
|
||||
urlpatterns = [
|
||||
path('user/', include(user_urls, namespace='user')),
|
||||
path('groups/', include(group_urls, namespace='groups'))
|
||||
]
|
||||
path('groups/', include(group_urls, namespace='groups')),
|
||||
path('applications/', include(application_urls, namespace='applications'))
|
||||
] + registry.api_urls
|
||||
|
|
1
backend/src/baserow/api/v0/user/errors.py
Normal file
1
backend/src/baserow/api/v0/user/errors.py
Normal file
|
@ -0,0 +1 @@
|
|||
ERROR_ALREADY_EXISTS = 'ERROR_ALREADY_EXISTS'
|
|
@ -5,12 +5,12 @@ 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
|
||||
|
||||
|
||||
from .serializers import RegisterSerializer, UserSerializer
|
||||
from .errors import ERROR_ALREADY_EXISTS
|
||||
|
||||
|
||||
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
|
||||
|
@ -23,13 +23,12 @@ class UserView(APIView):
|
|||
|
||||
@transaction.atomic
|
||||
@map_exceptions({
|
||||
UserAlreadyExist: 'ERROR_ALREADY_EXISTS'
|
||||
UserAlreadyExist: ERROR_ALREADY_EXISTS
|
||||
})
|
||||
def post(self, request):
|
||||
serializer = RegisterSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
@validate_body(RegisterSerializer)
|
||||
def post(self, request, data):
|
||||
"""Registers a new user."""
|
||||
|
||||
data = serializer.data
|
||||
user = self.user_handler.create_user(name=data['name'], email=data['email'],
|
||||
password=data['password'])
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@ INSTALLED_APPS = [
|
|||
'corsheaders',
|
||||
|
||||
'baserow.core',
|
||||
'baserow.api.v0'
|
||||
'baserow.api.v0',
|
||||
'baserow.contrib.database'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
0
backend/src/baserow/contrib/__init__.py
Normal file
0
backend/src/baserow/contrib/__init__.py
Normal file
1
backend/src/baserow/contrib/database/__init__.py
Normal file
1
backend/src/baserow/contrib/database/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
default_app_config = 'baserow.contrib.database.config.DatabaseConfig'
|
5
backend/src/baserow/contrib/database/api_urls.py
Normal file
5
backend/src/baserow/contrib/database/api_urls.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
app_name = 'baserow.contrib.database'
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
]
|
16
backend/src/baserow/contrib/database/applications.py
Normal file
16
backend/src/baserow/contrib/database/applications.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.urls import path, include
|
||||
|
||||
from baserow.core.applications import Application
|
||||
|
||||
from . import api_urls
|
||||
from .models import Database
|
||||
|
||||
|
||||
class DatabaseApplication(Application):
|
||||
type = 'database'
|
||||
instance_model = Database
|
||||
|
||||
def get_api_urls(self):
|
||||
return [
|
||||
path('database/', include(api_urls, namespace=self.type)),
|
||||
]
|
11
backend/src/baserow/contrib/database/config.py
Normal file
11
backend/src/baserow/contrib/database/config.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
from baserow.core.applications import registry
|
||||
|
||||
|
||||
class DatabaseConfig(AppConfig):
|
||||
name = 'baserow.contrib.database'
|
||||
|
||||
def ready(self):
|
||||
from .applications import DatabaseApplication
|
||||
registry.register(DatabaseApplication())
|
|
@ -0,0 +1,46 @@
|
|||
# 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')
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
12
backend/src/baserow/contrib/database/models.py
Normal file
12
backend/src/baserow/contrib/database/models.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from django.db import models
|
||||
|
||||
from baserow.core.models import Application
|
||||
|
||||
|
||||
class Database(Application):
|
||||
pass
|
||||
|
||||
|
||||
class Table(models.Model):
|
||||
group = models.ForeignKey(Database, on_delete=models.CASCADE)
|
||||
order = models.PositiveIntegerField()
|
|
@ -1 +1 @@
|
|||
app_name = 'baserow.group'
|
||||
default_app_config = 'baserow.core.config.CoreConfig'
|
||||
|
|
175
backend/src/baserow/core/applications.py
Normal file
175
backend/src/baserow/core/applications.py
Normal file
|
@ -0,0 +1,175 @@
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from .exceptions import ApplicationAlreadyRegistered, ApplicationTypeDoesNotExist
|
||||
|
||||
|
||||
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 Application model, this is
|
||||
needed so that the user can set custom settings per application instance he has
|
||||
created.
|
||||
|
||||
Example:
|
||||
from baserow.core.models import Application as ApplicationModel
|
||||
from baserow.core.applications import Application, registry
|
||||
|
||||
class ExampleApplicationModel(ApplicationModel):
|
||||
pass
|
||||
|
||||
class ExampleApplication(Application):
|
||||
type = 'a-unique-type-name'
|
||||
instance_model = ExampleApplicationModel
|
||||
|
||||
registry.register(ExampleApplication())
|
||||
|
||||
"""
|
||||
|
||||
type = None
|
||||
instance_model = None
|
||||
|
||||
def __init__(self):
|
||||
if not self.type:
|
||||
raise ImproperlyConfigured('The type of an application must be set.')
|
||||
|
||||
if not self.instance_model:
|
||||
raise ImproperlyConfigured('The instance model of an application must be '
|
||||
'set.')
|
||||
|
||||
def get_api_urls(self):
|
||||
"""
|
||||
If needed custom api related urls to the application can be added here.
|
||||
|
||||
Example:
|
||||
|
||||
def get_api_urls(self):
|
||||
from . import api_urls
|
||||
|
||||
return [
|
||||
path('some-application/', include(api_urls, namespace=self.type)),
|
||||
]
|
||||
|
||||
# api_urls.py
|
||||
from django.conf.urls import url
|
||||
|
||||
urlpatterns = [
|
||||
url(r'some-view^$', SomeView.as_view(), name='some_view'),
|
||||
]
|
||||
|
||||
:return: A list containing the urls.
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
return []
|
||||
|
||||
|
||||
class ApplicationRegistry(object):
|
||||
"""
|
||||
With the application registry it is possible to register new applications. An
|
||||
application is an abstraction made specifically for Baserow. If added to the
|
||||
registry a user can create new instances of that application via the ap and register
|
||||
api related urls.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.registry = {}
|
||||
|
||||
def get(self, type):
|
||||
"""
|
||||
Returns a registered application their type name.
|
||||
|
||||
:param type: The type name of the registered application.
|
||||
:param type: str
|
||||
:return: The requested application.
|
||||
:rtype: Application
|
||||
"""
|
||||
|
||||
if type not in self.registry:
|
||||
raise ApplicationTypeDoesNotExist(f'The application type {type} does not '
|
||||
f'exist.')
|
||||
|
||||
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.
|
||||
|
||||
:param application: The application that needs to be registered.
|
||||
:type application:
|
||||
"""
|
||||
|
||||
if not isinstance(application, Application):
|
||||
raise ValueError('The application must be an instance of Application.')
|
||||
|
||||
if application.type in self.registry:
|
||||
raise ApplicationAlreadyRegistered(
|
||||
f'The application with type {application.type} is already registered.')
|
||||
|
||||
self.registry[application.type] = application
|
||||
|
||||
def unregister(self, value):
|
||||
"""
|
||||
Removes a registered application from the registry. An application instance or
|
||||
type name can be provided as value.
|
||||
|
||||
:param value: The application instance or type name.
|
||||
:type value: Application or str
|
||||
"""
|
||||
|
||||
if isinstance(value, Application):
|
||||
for type, application in self.registry.items():
|
||||
if application == value:
|
||||
value = type
|
||||
|
||||
if isinstance(value, str):
|
||||
del self.registry[value]
|
||||
else:
|
||||
raise ValueError('The value must either be an application instance or type '
|
||||
'name')
|
||||
|
||||
@property
|
||||
def api_urls(self):
|
||||
"""
|
||||
Returns a list of all the api urls that are in the registered applications.
|
||||
|
||||
:return: The api urls of the registered applications.
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
api_urls = []
|
||||
for application in self.registry.values():
|
||||
api_urls += application.get_api_urls()
|
||||
return api_urls
|
||||
|
||||
|
||||
# A default application is created here, this is the one that is used throughout the
|
||||
# whole Baserow application. To add a new application use this registry.
|
||||
registry = ApplicationRegistry()
|
|
@ -1,5 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
class CoreConfig(AppConfig):
|
||||
name = 'baserow.core'
|
|
@ -1,2 +1,10 @@
|
|||
class UserNotIngroupError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ApplicationAlreadyRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ApplicationTypeDoesNotExist(Exception):
|
||||
pass
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
from .models import Group, GroupUser
|
||||
from .models import Group, GroupUser, Application
|
||||
from .exceptions import UserNotIngroupError
|
||||
from .utils import extract_allowed, set_allowed_attrs
|
||||
from .applications import registry
|
||||
|
||||
|
||||
class CoreHandler:
|
||||
def create_group(self, user, **kwargs):
|
||||
"""Creates a new group for an existing user.
|
||||
"""
|
||||
Creates a new group for an existing user.
|
||||
|
||||
:param user: The user that must be in the group.
|
||||
:type user: User
|
||||
|
@ -12,13 +15,7 @@ class CoreHandler:
|
|||
:rtype: GroupUser
|
||||
"""
|
||||
|
||||
allowed_fields = ['name']
|
||||
|
||||
group_values = {}
|
||||
for field in allowed_fields:
|
||||
if field in allowed_fields:
|
||||
group_values[field] = kwargs[field]
|
||||
|
||||
group_values = extract_allowed(kwargs, ['name'])
|
||||
group = Group.objects.create(**group_values)
|
||||
last_order = GroupUser.get_last_order(user)
|
||||
group_user = GroupUser.objects.create(group=group, user=user, order=last_order)
|
||||
|
@ -26,36 +23,41 @@ class CoreHandler:
|
|||
return group_user
|
||||
|
||||
def update_group(self, user, group, **kwargs):
|
||||
"""Updates fields of a group.
|
||||
|
||||
:param user:
|
||||
:param group:
|
||||
:return:
|
||||
"""
|
||||
Updates the values of a group.
|
||||
|
||||
:param user: The user on whose behalf the change is made.
|
||||
:type user: User
|
||||
:param group: The group instance that must be updated.
|
||||
:type group: Group
|
||||
:return: The updated group
|
||||
:rtype: Group
|
||||
"""
|
||||
|
||||
if not isinstance(group, Group):
|
||||
raise ValueError('The group is not an instance of Group.')
|
||||
|
||||
if not group.has_user(user):
|
||||
raise UserNotIngroupError(f'The user {user} does not belong to the group '
|
||||
f'{group}.')
|
||||
|
||||
allowed_fields = ['name']
|
||||
|
||||
for field in allowed_fields:
|
||||
if field in kwargs:
|
||||
setattr(group, field, kwargs[field])
|
||||
|
||||
group = set_allowed_attrs(kwargs, ['name'], group)
|
||||
group.save()
|
||||
|
||||
return group
|
||||
|
||||
def delete_group(self, user, group):
|
||||
"""Deletes an existing group.
|
||||
|
||||
:param user:
|
||||
:type: user: User
|
||||
:param group:
|
||||
:type: group: Group
|
||||
:return:
|
||||
"""
|
||||
Deletes an existing group.
|
||||
|
||||
:param user: The user on whose behalf the delete is done.
|
||||
:type: user: User
|
||||
:param group: The group instance that must be deleted.
|
||||
:type: group: Group
|
||||
"""
|
||||
|
||||
if not isinstance(group, Group):
|
||||
raise ValueError('The group is not an instance of Group.')
|
||||
|
||||
if not group.has_user(user):
|
||||
raise UserNotIngroupError(f'The user {user} does not belong to the group '
|
||||
|
@ -64,9 +66,10 @@ class CoreHandler:
|
|||
group.delete()
|
||||
|
||||
def order_groups(self, user, group_ids):
|
||||
"""Changes the order of groups for a user.
|
||||
"""
|
||||
Changes the order of groups for a user.
|
||||
|
||||
:param user:
|
||||
:param user: The user on whose behalf the ordering is done.
|
||||
:type: user: User
|
||||
:param group_ids: A list of group ids ordered the way they need to be ordered.
|
||||
:type group_ids: List[int]
|
||||
|
@ -77,3 +80,81 @@ class CoreHandler:
|
|||
user=user,
|
||||
group_id=group_id
|
||||
).update(order=index + 1)
|
||||
|
||||
def create_application(self, user, group, type, **kwargs):
|
||||
"""
|
||||
Creates a new application based on the provided type.
|
||||
|
||||
:param user: The user on whose behalf the application is created.
|
||||
:type user: User
|
||||
:param group: The group that the application instance belongs to.
|
||||
:type group: Group
|
||||
:param type: The type name of the application. Application can be registered via
|
||||
the ApplicationRegistry.
|
||||
:type type: str
|
||||
:param kwargs: The fields that need to be set upon creation.
|
||||
:type kwargs: object
|
||||
:return: The created application instance.
|
||||
:rtype: Application
|
||||
"""
|
||||
|
||||
if not group.has_user(user):
|
||||
raise UserNotIngroupError(f'The user {user} does not belong to the group '
|
||||
f'{group}.')
|
||||
|
||||
# Figure out which model is used for the given application type.
|
||||
application = registry.get(type)
|
||||
model = application.instance_model
|
||||
application_values = extract_allowed(kwargs, ['name'])
|
||||
|
||||
if 'order' not in application_values:
|
||||
application_values['order'] = model.get_last_order(group)
|
||||
|
||||
instance = model.objects.create(group=group, **application_values)
|
||||
|
||||
return instance
|
||||
|
||||
def update_application(self, user, application, **kwargs):
|
||||
"""
|
||||
Updates an existing application instance.
|
||||
|
||||
:param user: The user on whose behalf the application is updated.
|
||||
:type user: User
|
||||
:param application: The application instance that needs to be updated.
|
||||
:type application: Application
|
||||
:param kwargs: The fields that need to be updated.
|
||||
:type kwargs: object
|
||||
:return: The updated application instance.
|
||||
:rtype: Application
|
||||
"""
|
||||
|
||||
if not isinstance(application, Application):
|
||||
raise ValueError('The application is not an instance of Application')
|
||||
|
||||
if not application.group.has_user(user):
|
||||
raise UserNotIngroupError(f'The user {user} does not belong to the group '
|
||||
f'{application.group}.')
|
||||
|
||||
application = set_allowed_attrs(kwargs, ['name'], application)
|
||||
application.save()
|
||||
|
||||
return application
|
||||
|
||||
def delete_application(self, user, application):
|
||||
"""
|
||||
Deletes an existing application instance.
|
||||
|
||||
:param user: The user on whose behalf the application is deleted.
|
||||
:type user: User
|
||||
:param application: The application instance that needs to be deleted.
|
||||
:type application: Application
|
||||
"""
|
||||
|
||||
if not isinstance(application, Application):
|
||||
raise ValueError('The application is not an instance of Application')
|
||||
|
||||
if not application.group.has_user(user):
|
||||
raise UserNotIngroupError(f'The user {user} does not belong to the group '
|
||||
f'{application.group}.')
|
||||
|
||||
application.delete()
|
||||
|
|
38
backend/src/baserow/core/migrations/0002_application.py
Normal file
38
backend/src/baserow/core/migrations/0002_application.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
20
backend/src/baserow/core/mixins.py
Normal file
20
backend/src/baserow/core/mixins.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class OrderableMixin:
|
||||
"""
|
||||
This mixin introduces a set of helpers of the model is orderable by a field.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_highest_order_of_queryset(cls, queryset, field='order'):
|
||||
"""
|
||||
|
||||
:param queryset:
|
||||
:param field:
|
||||
:return:
|
||||
"""
|
||||
|
||||
return queryset.aggregate(
|
||||
models.Max(field)
|
||||
).get(f'{field}__max', 0) or 0
|
|
@ -1,12 +1,19 @@
|
|||
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
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def get_default_application_content_type():
|
||||
return ContentType.objects.get_for_model(Application)
|
||||
|
||||
|
||||
class Group(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
users = models.ManyToManyField(User, through='GroupUser')
|
||||
|
@ -15,13 +22,14 @@ class Group(models.Model):
|
|||
|
||||
def has_user(self, user):
|
||||
"""Returns true is the user belongs to the group."""
|
||||
|
||||
return self.users.filter(id=user.id).exists()
|
||||
|
||||
def __str__(self):
|
||||
return f'<Group id={self.id}, name={self.name}>'
|
||||
|
||||
|
||||
class GroupUser(models.Model):
|
||||
class GroupUser(OrderableMixin, models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
order = models.PositiveIntegerField()
|
||||
|
@ -31,11 +39,54 @@ class GroupUser(models.Model):
|
|||
|
||||
@classmethod
|
||||
def get_last_order(cls, user):
|
||||
"""Returns a new position that will be last for a new group."""
|
||||
highest_order = cls.objects.filter(
|
||||
user=user
|
||||
).aggregate(
|
||||
models.Max('order')
|
||||
).get('order__max', 0) or 0
|
||||
return cls.get_highest_order_of_queryset(cls.objects.filter(user=user)) + 1
|
||||
|
||||
return highest_order + 1
|
||||
|
||||
class Application(OrderableMixin, models.Model):
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=50)
|
||||
order = models.PositiveIntegerField()
|
||||
content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
verbose_name='content type',
|
||||
related_name='applications',
|
||||
on_delete=models.SET(get_default_application_content_type)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('order',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not self.id:
|
||||
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(
|
||||
Application.objects.filter(group=group)) + 1
|
||||
|
|
67
backend/src/baserow/core/utils.py
Normal file
67
backend/src/baserow/core/utils.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
def extract_allowed(values, allowed_fields):
|
||||
"""
|
||||
Returns a new dict with the values of the key names that are in the allowed_fields.
|
||||
The other keys are ignored.
|
||||
|
||||
Example:
|
||||
object_1 = {
|
||||
'value_1': 'value',
|
||||
'value_2': 'value'
|
||||
}
|
||||
|
||||
extract_allowed(object_1, ['value_1'])
|
||||
>> {'value_1': 'value'}
|
||||
|
||||
:param values: A dict containing the values.
|
||||
:type dict:
|
||||
:param allowed_fields: A list containing the keys of the values that need to be
|
||||
extracted from the values.
|
||||
:type allowed_fields: list
|
||||
:return: The extracted values.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
allowed_values = {}
|
||||
for field in allowed_fields:
|
||||
if field in values:
|
||||
allowed_values[field] = values[field]
|
||||
|
||||
return allowed_values
|
||||
|
||||
|
||||
def set_allowed_attrs(values, allowed_fields, instance):
|
||||
"""
|
||||
Sets the attributes of the instance with the values of the key names that are in the
|
||||
allowed_fields. The other keys are ignored.
|
||||
|
||||
Examples:
|
||||
class Tmp(object):
|
||||
value_1 = 'value'
|
||||
value_2 = 'value'
|
||||
|
||||
object_1 = {
|
||||
'value_1': 'value_2',
|
||||
'value_2': 'value_2'
|
||||
}
|
||||
|
||||
tmp = set_allowed_attrs(object_1, ['value_1'], Tmp())
|
||||
tmp.value_1
|
||||
>> 'value_2'
|
||||
tmp.value_2
|
||||
>> 'value'
|
||||
|
||||
:param values: The dict containing the values.
|
||||
:type values: dict
|
||||
:param allowed_fields: A list containing the keys of the value that need to be set
|
||||
on the instance.
|
||||
:type allowed_fields: list
|
||||
:param instance: The instance of which the attributes must be updated.
|
||||
:type instance: object
|
||||
:return: The updated instance.
|
||||
"""
|
||||
|
||||
for field in allowed_fields:
|
||||
if field in values:
|
||||
setattr(instance, field, values[field])
|
||||
|
||||
return instance
|
|
@ -0,0 +1,165 @@
|
|||
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
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_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)
|
||||
application = data_fixture.create_database_application(group=group)
|
||||
application_2 = data_fixture.create_database_application(group=group_2)
|
||||
|
||||
url = reverse('api_v0:applications:item',
|
||||
kwargs={'application_id': application_2.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
{'name': 'Test 1'},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 400
|
||||
assert response_json['error'] == 'ERROR_USER_NOT_IN_GROUP'
|
||||
|
||||
url = reverse('api_v0:applications:item', kwargs={'application_id': application.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
{'UNKNOWN_FIELD': 'Test 1'},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 400
|
||||
assert response_json['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
|
||||
|
||||
url = reverse('api_v0:applications:item', kwargs={'application_id': application.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
{'name': 'Test 1'},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == 200
|
||||
assert response_json['id'] == application.id
|
||||
assert response_json['name'] == 'Test 1'
|
||||
|
||||
application.refresh_from_db()
|
||||
assert application.name == 'Test 1'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_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)
|
||||
application = data_fixture.create_database_application(group=group)
|
||||
application_2 = data_fixture.create_database_application(group=group_2)
|
||||
|
||||
url = reverse('api_v0:applications:item',
|
||||
kwargs={'application_id': application_2.id})
|
||||
response = api_client.delete(url, HTTP_AUTHORIZATION=f'JWT {token}')
|
||||
response_json = response.json()
|
||||
assert response.status_code == 400
|
||||
assert response_json['error'] == 'ERROR_USER_NOT_IN_GROUP'
|
||||
|
||||
url = reverse('api_v0:applications:item', kwargs={'application_id': application.id})
|
||||
response = api_client.delete(url, HTTP_AUTHORIZATION=f'JWT {token}')
|
||||
assert response.status_code == 204
|
||||
|
||||
assert Database.objects.all().count() == 1
|
|
@ -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
|
||||
|
||||
|
106
backend/tests/baserow/api/v0/test_api_decorators.py
Normal file
106
backend/tests/baserow/api/v0/test_api_decorators.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
import pytest
|
||||
import json
|
||||
|
||||
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, 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'
|
||||
})
|
||||
def test_1():
|
||||
raise TemporaryException
|
||||
|
||||
with pytest.raises(APIException) as api_exception_1:
|
||||
test_1()
|
||||
|
||||
assert api_exception_1.value.detail['error'] == 'ERROR_TEMPORARY'
|
||||
assert api_exception_1.value.detail['detail'] == ''
|
||||
assert api_exception_1.value.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
@map_exceptions({
|
||||
TemporaryException: ('ERROR_TEMPORARY_2', 404, 'Another message')
|
||||
})
|
||||
def test_2():
|
||||
raise TemporaryException
|
||||
|
||||
with pytest.raises(APIException) as api_exception_2:
|
||||
test_2()
|
||||
|
||||
assert api_exception_2.value.detail['error'] == 'ERROR_TEMPORARY_2'
|
||||
assert api_exception_2.value.detail['detail'] == 'Another message'
|
||||
assert api_exception_2.value.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
@map_exceptions({
|
||||
TemporaryException: 'ERROR_TEMPORARY_3'
|
||||
})
|
||||
def test_3():
|
||||
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])
|
||||
|
88
backend/tests/baserow/core/test_core_applications.py
Normal file
88
backend/tests/baserow/core/test_core_applications.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
import pytest
|
||||
|
||||
from baserow.core.applications import Application, ApplicationRegistry
|
||||
from baserow.core.exceptions import (
|
||||
ApplicationAlreadyRegistered, ApplicationTypeDoesNotExist
|
||||
)
|
||||
|
||||
|
||||
class FakeModel(object):
|
||||
pass
|
||||
|
||||
|
||||
class FakeModel2(object):
|
||||
pass
|
||||
|
||||
|
||||
class TemporaryApplication1(Application):
|
||||
type = 'temporary_1'
|
||||
instance_model = FakeModel
|
||||
|
||||
def get_api_urls(self):
|
||||
return ['url_1', 'url_2']
|
||||
|
||||
|
||||
class TemporaryApplication2(Application):
|
||||
type = 'temporary_2'
|
||||
instance_model = FakeModel2
|
||||
|
||||
def get_api_urls(self):
|
||||
return ['url_3']
|
||||
|
||||
|
||||
def test_application_registry_register():
|
||||
temporary_1 = TemporaryApplication1()
|
||||
temporary_2 = TemporaryApplication2()
|
||||
|
||||
registry = ApplicationRegistry()
|
||||
registry.register(temporary_1)
|
||||
registry.register(temporary_2)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
registry.register('NOT AN APPLICATION')
|
||||
|
||||
with pytest.raises(ApplicationAlreadyRegistered):
|
||||
registry.register(temporary_1)
|
||||
|
||||
assert len(registry.registry.items()) == 2
|
||||
assert registry.registry['temporary_1'] == temporary_1
|
||||
assert registry.registry['temporary_2'] == temporary_2
|
||||
|
||||
registry.unregister(temporary_1)
|
||||
|
||||
assert len(registry.registry.items()) == 1
|
||||
|
||||
registry.unregister('temporary_2')
|
||||
|
||||
assert len(registry.registry.items()) == 0
|
||||
|
||||
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']
|
|
@ -1,8 +1,9 @@
|
|||
import pytest
|
||||
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.models import Group, GroupUser
|
||||
from baserow.core.exceptions import UserNotIngroupError
|
||||
from baserow.core.models import Group, GroupUser, Application
|
||||
from baserow.core.exceptions import UserNotIngroupError, ApplicationTypeDoesNotExist
|
||||
from baserow.contrib.database.models import Database
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -42,6 +43,9 @@ def test_update_group(data_fixture):
|
|||
with pytest.raises(UserNotIngroupError):
|
||||
handler.update_group(user=user_2, group=group, name='New name')
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
handler.update_group(user=user_2, group=object(), name='New name')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_group(data_fixture):
|
||||
|
@ -66,6 +70,9 @@ def test_delete_group(data_fixture):
|
|||
assert Group.objects.all().count() == 1
|
||||
assert GroupUser.objects.all().count() == 1
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
handler.delete_group(user=user_2, group=object())
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_groups(data_fixture):
|
||||
|
@ -77,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()
|
||||
|
@ -85,10 +92,76 @@ 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()
|
||||
ug_3.refresh_from_db()
|
||||
|
||||
assert [1, 2, 3] == [ug_2.order, ug_1.order, ug_3.order]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_database_application(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
user_2 = data_fixture.create_user()
|
||||
group = data_fixture.create_group(user=user)
|
||||
|
||||
handler = CoreHandler()
|
||||
handler.create_application(user=user, group=group, type='database',
|
||||
name='Test database')
|
||||
|
||||
assert Application.objects.all().count() == 1
|
||||
assert Database.objects.all().count() == 1
|
||||
|
||||
database = Database.objects.all().first()
|
||||
assert database.name == 'Test database'
|
||||
assert database.order == 1
|
||||
assert database.group == group
|
||||
|
||||
with pytest.raises(UserNotIngroupError):
|
||||
handler.create_application(user=user_2, group=group, type='database', name='')
|
||||
|
||||
with pytest.raises(ApplicationTypeDoesNotExist):
|
||||
handler.create_application(user=user, group=group, type='UNKNOWN', name='')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_database_application(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
user_2 = data_fixture.create_user()
|
||||
group = data_fixture.create_group(user=user)
|
||||
database = data_fixture.create_database_application(group=group)
|
||||
|
||||
handler = CoreHandler()
|
||||
|
||||
with pytest.raises(UserNotIngroupError):
|
||||
handler.update_application(user=user_2, application=database, name='Test 1')
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
handler.update_application(user=user_2, application=object(), name='Test 1')
|
||||
|
||||
handler.update_application(user=user, application=database, name='Test 1')
|
||||
|
||||
database.refresh_from_db()
|
||||
|
||||
assert database.name == 'Test 1'
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_database_application(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
user_2 = data_fixture.create_user()
|
||||
group = data_fixture.create_group(user=user)
|
||||
database = data_fixture.create_database_application(group=group)
|
||||
|
||||
handler = CoreHandler()
|
||||
|
||||
with pytest.raises(UserNotIngroupError):
|
||||
handler.delete_application(user=user_2, application=database)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
handler.delete_application(user=user_2, application=object())
|
||||
|
||||
assert Database.objects.all().count() == 1
|
||||
handler.delete_application(user=user, application=database)
|
||||
assert Database.objects.all().count() == 0
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
30
backend/tests/baserow/core/test_core_utils.py
Normal file
30
backend/tests/baserow/core/test_core_utils.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from baserow.core.utils import extract_allowed, set_allowed_attrs
|
||||
|
||||
|
||||
def test_extract_allowed():
|
||||
assert extract_allowed({
|
||||
'test_1': 'test_1',
|
||||
'test_2': 'test_2'
|
||||
}, ['test_1']) == {
|
||||
'test_1': 'test_1'
|
||||
}
|
||||
|
||||
assert extract_allowed({}, ['test_1']) == {}
|
||||
assert extract_allowed({'test_1': 'test'}, ['test_2']) == {}
|
||||
assert extract_allowed({'test_1': 'test'}, []) == {}
|
||||
|
||||
|
||||
def test_set_allowed_attrs():
|
||||
class Tmp(object):
|
||||
test_1 = None
|
||||
test_2 = None
|
||||
|
||||
tmp1 = Tmp()
|
||||
tmp1 = set_allowed_attrs(
|
||||
{'test_1': 'test', 'test_2': 'test'},
|
||||
['test_1'],
|
||||
tmp1
|
||||
)
|
||||
|
||||
assert tmp1.test_1 == 'test'
|
||||
assert tmp1.test_2 is None
|
3
backend/tests/fixtures/__init__.py
vendored
3
backend/tests/fixtures/__init__.py
vendored
|
@ -2,7 +2,8 @@ from faker import Faker
|
|||
|
||||
from .user import UserFixtures
|
||||
from .group import GroupFixtures
|
||||
from .application import ApplicationFixtures
|
||||
|
||||
|
||||
class Fixtures(UserFixtures, GroupFixtures):
|
||||
class Fixtures(UserFixtures, GroupFixtures, ApplicationFixtures):
|
||||
fake = Faker()
|
||||
|
|
15
backend/tests/fixtures/application.py
vendored
Normal file
15
backend/tests/fixtures/application.py
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
from baserow.contrib.database.models import Database
|
||||
|
||||
|
||||
class ApplicationFixtures:
|
||||
def create_database_application(self, **kwargs):
|
||||
if 'group' not in kwargs:
|
||||
kwargs['group'] = self.create_group()
|
||||
|
||||
if 'name' not in kwargs:
|
||||
kwargs['name'] = self.fake.name()
|
||||
|
||||
if 'order' not in kwargs:
|
||||
kwargs['order'] = 0
|
||||
|
||||
return Database.objects.create(**kwargs)
|
Loading…
Add table
Reference in a new issue