1
0
Fork 0
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 

See merge request 
This commit is contained in:
Bram Wiepjes 2019-09-22 13:48:32 +00:00
commit e1fc7ac922
42 changed files with 1317 additions and 83 deletions

View file

@ -1 +1 @@
app_name = 'baserow.api.v0'
default_app_config = 'baserow.api.v0.config.ApiConfig'

View file

@ -0,0 +1 @@
ERROR_USER_NOT_IN_GROUP = 'ERROR_USER_NOT_IN_GROUP'

View 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',)

View 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'),
]

View 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)

View file

@ -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

View file

@ -5,6 +5,7 @@ from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from baserow.api.v0.decorators import validate_body
from baserow.core.models import GroupUser
from baserow.core.handler import CoreHandler
@ -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)

View file

@ -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

View file

@ -0,0 +1 @@
ERROR_ALREADY_EXISTS = 'ERROR_ALREADY_EXISTS'

View file

@ -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'])

View file

@ -26,7 +26,8 @@ INSTALLED_APPS = [
'corsheaders',
'baserow.core',
'baserow.api.v0'
'baserow.api.v0',
'baserow.contrib.database'
]
MIDDLEWARE = [

View file

View file

@ -0,0 +1 @@
default_app_config = 'baserow.contrib.database.config.DatabaseConfig'

View file

@ -0,0 +1,5 @@
app_name = 'baserow.contrib.database'
urlpatterns = [
]

View 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)),
]

View 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())

View file

@ -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')
),
],
),
]

View 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()

View file

@ -1 +1 @@
app_name = 'baserow.group'
default_app_config = 'baserow.core.config.CoreConfig'

View 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()

View file

@ -1,5 +1,5 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
class CoreConfig(AppConfig):
name = 'baserow.core'

View file

@ -1,2 +1,10 @@
class UserNotIngroupError(Exception):
pass
class ApplicationAlreadyRegistered(Exception):
pass
class ApplicationTypeDoesNotExist(Exception):
pass

View file

@ -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()

View 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),
),
]

View 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

View file

@ -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

View 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

View file

@ -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

View file

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

View file

@ -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])

View 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']

View file

@ -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

View file

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

View file

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

View 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

View file

@ -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
View 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)