mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-14 09:08:32 +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 import status
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
|
from rest_framework.request import Request
|
||||||
|
|
||||||
|
|
||||||
def map_exceptions(exceptions):
|
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:
|
Example:
|
||||||
@map_exceptions({ SomeException: 'ERROR_1' })
|
@map_exceptions({ SomeException: 'ERROR_1' })
|
||||||
|
@ -54,3 +60,70 @@ def map_exceptions(exceptions):
|
||||||
raise exc
|
raise exc
|
||||||
return func_wrapper
|
return func_wrapper
|
||||||
return map_exceptions_decorator
|
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.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
from baserow.api.v0.decorators import validate_body
|
||||||
from baserow.core.models import GroupUser
|
from baserow.core.models import GroupUser
|
||||||
from baserow.core.handler import CoreHandler
|
from baserow.core.handler import CoreHandler
|
||||||
|
|
||||||
|
@ -17,19 +18,17 @@ class GroupsView(APIView):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Responds with a list of groups where the users takes part in."""
|
"""Responds with a list of groups where the users takes part in."""
|
||||||
|
|
||||||
groups = GroupUser.objects.filter(user=request.user).select_related('group')
|
groups = GroupUser.objects.filter(user=request.user).select_related('group')
|
||||||
serializer = GroupUserSerializer(groups, many=True)
|
serializer = GroupUserSerializer(groups, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def post(self, request):
|
@validate_body(GroupSerializer)
|
||||||
|
def post(self, request, data):
|
||||||
"""Creates a new group for a user."""
|
"""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'])
|
group_user = self.core_handler.create_group(request.user, name=data['name'])
|
||||||
|
|
||||||
return Response(GroupUserSerializer(group_user).data)
|
return Response(GroupUserSerializer(group_user).data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,14 +37,16 @@ class GroupView(APIView):
|
||||||
core_handler = CoreHandler()
|
core_handler = CoreHandler()
|
||||||
|
|
||||||
@transaction.atomic
|
@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."""
|
"""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)
|
group_user = get_object_or_404(
|
||||||
serializer.is_valid(raise_exception=True)
|
GroupUser.objects.select_for_update(),
|
||||||
|
group_id=group_id,
|
||||||
|
user=request.user
|
||||||
|
)
|
||||||
|
|
||||||
data = serializer.data
|
|
||||||
group_user.group = self.core_handler.update_group(
|
group_user.group = self.core_handler.update_group(
|
||||||
request.user, group_user.group, name=data['name'])
|
request.user, group_user.group, name=data['name'])
|
||||||
|
|
||||||
|
@ -54,6 +55,7 @@ class GroupView(APIView):
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def delete(self, request, group_id):
|
def delete(self, request, group_id):
|
||||||
"""Deletes an existing group if it belongs to a user."""
|
"""Deletes an existing 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, group_id=group_id, user=request.user)
|
||||||
self.core_handler.delete_group(request.user, group_user.group)
|
self.core_handler.delete_group(request.user, group_user.group)
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
@ -63,11 +65,9 @@ class GroupOrderView(APIView):
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
core_handler = CoreHandler()
|
core_handler = CoreHandler()
|
||||||
|
|
||||||
def post(self, request):
|
@validate_body(OrderGroupsSerializer)
|
||||||
|
def post(self, request, data):
|
||||||
"""Updates to order of some groups for a user."""
|
"""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)
|
return Response(status=204)
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
|
from baserow.core.applications import registry
|
||||||
|
|
||||||
from .user import urls as user_urls
|
from .user import urls as user_urls
|
||||||
from .groups import urls as group_urls
|
from .groups import urls as group_urls
|
||||||
|
from .applications import urls as application_urls
|
||||||
|
|
||||||
|
|
||||||
app_name = 'baserow.api.v0'
|
app_name = 'baserow.api.v0'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('user/', include(user_urls, namespace='user')),
|
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.permissions import AllowAny
|
||||||
from rest_framework_jwt.settings import api_settings
|
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.handler import UserHandler
|
||||||
from baserow.user.exceptions import UserAlreadyExist
|
from baserow.user.exceptions import UserAlreadyExist
|
||||||
|
|
||||||
|
|
||||||
from .serializers import RegisterSerializer, UserSerializer
|
from .serializers import RegisterSerializer, UserSerializer
|
||||||
|
from .errors import ERROR_ALREADY_EXISTS
|
||||||
|
|
||||||
|
|
||||||
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
|
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
|
||||||
|
@ -23,13 +23,12 @@ class UserView(APIView):
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@map_exceptions({
|
@map_exceptions({
|
||||||
UserAlreadyExist: 'ERROR_ALREADY_EXISTS'
|
UserAlreadyExist: ERROR_ALREADY_EXISTS
|
||||||
})
|
})
|
||||||
def post(self, request):
|
@validate_body(RegisterSerializer)
|
||||||
serializer = RegisterSerializer(data=request.data)
|
def post(self, request, data):
|
||||||
serializer.is_valid(raise_exception=True)
|
"""Registers a new user."""
|
||||||
|
|
||||||
data = serializer.data
|
|
||||||
user = self.user_handler.create_user(name=data['name'], email=data['email'],
|
user = self.user_handler.create_user(name=data['name'], email=data['email'],
|
||||||
password=data['password'])
|
password=data['password'])
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,8 @@ INSTALLED_APPS = [
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
|
|
||||||
'baserow.core',
|
'baserow.core',
|
||||||
'baserow.api.v0'
|
'baserow.api.v0',
|
||||||
|
'baserow.contrib.database'
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
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
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class ApiConfig(AppConfig):
|
class CoreConfig(AppConfig):
|
||||||
name = 'baserow.core'
|
name = 'baserow.core'
|
|
@ -1,2 +1,10 @@
|
||||||
class UserNotIngroupError(Exception):
|
class UserNotIngroupError(Exception):
|
||||||
pass
|
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 .exceptions import UserNotIngroupError
|
||||||
|
from .utils import extract_allowed, set_allowed_attrs
|
||||||
|
from .applications import registry
|
||||||
|
|
||||||
|
|
||||||
class CoreHandler:
|
class CoreHandler:
|
||||||
def create_group(self, user, **kwargs):
|
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.
|
:param user: The user that must be in the group.
|
||||||
:type user: User
|
:type user: User
|
||||||
|
@ -12,13 +15,7 @@ class CoreHandler:
|
||||||
:rtype: GroupUser
|
:rtype: GroupUser
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allowed_fields = ['name']
|
group_values = extract_allowed(kwargs, ['name'])
|
||||||
|
|
||||||
group_values = {}
|
|
||||||
for field in allowed_fields:
|
|
||||||
if field in allowed_fields:
|
|
||||||
group_values[field] = kwargs[field]
|
|
||||||
|
|
||||||
group = Group.objects.create(**group_values)
|
group = Group.objects.create(**group_values)
|
||||||
last_order = GroupUser.get_last_order(user)
|
last_order = GroupUser.get_last_order(user)
|
||||||
group_user = GroupUser.objects.create(group=group, user=user, order=last_order)
|
group_user = GroupUser.objects.create(group=group, user=user, order=last_order)
|
||||||
|
@ -26,36 +23,41 @@ class CoreHandler:
|
||||||
return group_user
|
return group_user
|
||||||
|
|
||||||
def update_group(self, user, group, **kwargs):
|
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):
|
if not group.has_user(user):
|
||||||
raise UserNotIngroupError(f'The user {user} does not belong to the group '
|
raise UserNotIngroupError(f'The user {user} does not belong to the group '
|
||||||
f'{group}.')
|
f'{group}.')
|
||||||
|
|
||||||
allowed_fields = ['name']
|
group = set_allowed_attrs(kwargs, ['name'], group)
|
||||||
|
|
||||||
for field in allowed_fields:
|
|
||||||
if field in kwargs:
|
|
||||||
setattr(group, field, kwargs[field])
|
|
||||||
|
|
||||||
group.save()
|
group.save()
|
||||||
|
|
||||||
return group
|
return group
|
||||||
|
|
||||||
def delete_group(self, user, 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):
|
if not group.has_user(user):
|
||||||
raise UserNotIngroupError(f'The user {user} does not belong to the group '
|
raise UserNotIngroupError(f'The user {user} does not belong to the group '
|
||||||
|
@ -64,9 +66,10 @@ class CoreHandler:
|
||||||
group.delete()
|
group.delete()
|
||||||
|
|
||||||
def order_groups(self, user, group_ids):
|
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
|
:type: user: User
|
||||||
:param group_ids: A list of group ids ordered the way they need to be ordered.
|
:param group_ids: A list of group ids ordered the way they need to be ordered.
|
||||||
:type group_ids: List[int]
|
:type group_ids: List[int]
|
||||||
|
@ -77,3 +80,81 @@ class CoreHandler:
|
||||||
user=user,
|
user=user,
|
||||||
group_id=group_id
|
group_id=group_id
|
||||||
).update(order=index + 1)
|
).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.db import models
|
||||||
from django.contrib.auth import get_user_model
|
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 .managers import GroupQuerySet
|
||||||
|
from .mixins import OrderableMixin
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_application_content_type():
|
||||||
|
return ContentType.objects.get_for_model(Application)
|
||||||
|
|
||||||
|
|
||||||
class Group(models.Model):
|
class Group(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
users = models.ManyToManyField(User, through='GroupUser')
|
users = models.ManyToManyField(User, through='GroupUser')
|
||||||
|
@ -15,13 +22,14 @@ class Group(models.Model):
|
||||||
|
|
||||||
def has_user(self, user):
|
def has_user(self, user):
|
||||||
"""Returns true is the user belongs to the group."""
|
"""Returns true is the user belongs to the group."""
|
||||||
|
|
||||||
return self.users.filter(id=user.id).exists()
|
return self.users.filter(id=user.id).exists()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'<Group id={self.id}, name={self.name}>'
|
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)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||||
order = models.PositiveIntegerField()
|
order = models.PositiveIntegerField()
|
||||||
|
@ -31,11 +39,54 @@ class GroupUser(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_last_order(cls, user):
|
def get_last_order(cls, user):
|
||||||
"""Returns a new position that will be last for a new group."""
|
return cls.get_highest_order_of_queryset(cls.objects.filter(user=user)) + 1
|
||||||
highest_order = cls.objects.filter(
|
|
||||||
user=user
|
|
||||||
).aggregate(
|
|
||||||
models.Max('order')
|
|
||||||
).get('order__max', 0) or 0
|
|
||||||
|
|
||||||
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):
|
def test_list_groups(api_client, data_fixture):
|
||||||
user, token = data_fixture.create_user_and_token(
|
user, token = data_fixture.create_user_and_token(
|
||||||
email='test@test.nl', password='password', first_name='Test1')
|
email='test@test.nl', password='password', first_name='Test1')
|
||||||
group_2 = data_fixture.create_user_group(user=user, order=2)
|
user_group_2 = data_fixture.create_user_group(user=user, order=2)
|
||||||
group_1 = data_fixture.create_user_group(user=user, order=1)
|
user_group_1 = data_fixture.create_user_group(user=user, order=1)
|
||||||
|
|
||||||
response = api_client.get(reverse('api_v0:groups:list'), **{
|
response = api_client.get(reverse('api_v0:groups:list'), **{
|
||||||
'HTTP_AUTHORIZATION': f'JWT {token}'
|
'HTTP_AUTHORIZATION': f'JWT {token}'
|
||||||
})
|
})
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
response_json = response.json()
|
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[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
|
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
|
import pytest
|
||||||
|
|
||||||
from baserow.core.handler import CoreHandler
|
from baserow.core.handler import CoreHandler
|
||||||
from baserow.core.models import Group, GroupUser
|
from baserow.core.models import Group, GroupUser, Application
|
||||||
from baserow.core.exceptions import UserNotIngroupError
|
from baserow.core.exceptions import UserNotIngroupError, ApplicationTypeDoesNotExist
|
||||||
|
from baserow.contrib.database.models import Database
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -42,6 +43,9 @@ def test_update_group(data_fixture):
|
||||||
with pytest.raises(UserNotIngroupError):
|
with pytest.raises(UserNotIngroupError):
|
||||||
handler.update_group(user=user_2, group=group, name='New name')
|
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
|
@pytest.mark.django_db
|
||||||
def test_delete_group(data_fixture):
|
def test_delete_group(data_fixture):
|
||||||
|
@ -66,6 +70,9 @@ def test_delete_group(data_fixture):
|
||||||
assert Group.objects.all().count() == 1
|
assert Group.objects.all().count() == 1
|
||||||
assert GroupUser.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
|
@pytest.mark.django_db
|
||||||
def test_order_groups(data_fixture):
|
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]
|
assert [1, 2, 3] == [ug_1.order, ug_2.order, ug_3.order]
|
||||||
|
|
||||||
handler = CoreHandler()
|
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_1.refresh_from_db()
|
||||||
ug_2.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]
|
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_1.refresh_from_db()
|
||||||
ug_2.refresh_from_db()
|
ug_2.refresh_from_db()
|
||||||
ug_3.refresh_from_db()
|
ug_3.refresh_from_db()
|
||||||
|
|
||||||
assert [1, 2, 3] == [ug_2.order, ug_1.order, ug_3.order]
|
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
|
@pytest.mark.django_db
|
||||||
def test_groups_of_user(data_fixture):
|
def test_groups_of_user(data_fixture):
|
||||||
user_1 = data_fixture.create_user()
|
user_1 = data_fixture.create_user()
|
||||||
group_user_1 = data_fixture.create_user_group(user=user_1, order=1)
|
user_group_1 = data_fixture.create_user_group(user=user_1, order=1)
|
||||||
group_user_2 = data_fixture.create_user_group(user=user_1, order=2)
|
user_group_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_3 = data_fixture.create_user_group(user=user_1, order=0)
|
||||||
|
|
||||||
user_2 = data_fixture.create_user()
|
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)
|
groups_user_1 = Group.objects.of_user(user=user_1)
|
||||||
assert len(groups_user_1) == 3
|
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[0].id == user_group_3.group.id
|
||||||
assert groups_user_1[2].id == group_user_2.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)
|
groups_user_2 = Group.objects.of_user(user=user_2)
|
||||||
assert len(groups_user_2) == 1
|
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
|
import pytest
|
||||||
|
|
||||||
from baserow.core.models import GroupUser
|
from baserow.core.models import GroupUser
|
||||||
|
from baserow.contrib.database.models import Database
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@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 user_group.group.has_user(user_group.user)
|
||||||
assert not user_group.group.has_user(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 .user import UserFixtures
|
||||||
from .group import GroupFixtures
|
from .group import GroupFixtures
|
||||||
|
from .application import ApplicationFixtures
|
||||||
|
|
||||||
|
|
||||||
class Fixtures(UserFixtures, GroupFixtures):
|
class Fixtures(UserFixtures, GroupFixtures, ApplicationFixtures):
|
||||||
fake = Faker()
|
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