1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 14:25:37 +00:00

made it possible to create, update and delete tables, but also the select a table and navigate to the route

This commit is contained in:
Bram Wiepjes 2019-11-03 10:17:22 +00:00
parent 3b2eb13042
commit 497447edb8
45 changed files with 1226 additions and 150 deletions

View file

@ -1,3 +1,5 @@
from django.utils.functional import lazy
from rest_framework import serializers
from baserow.api.v0.groups.serializers import GroupSerializer
@ -19,12 +21,17 @@ class ApplicationSerializer(serializers.ModelSerializer):
}
def get_type(self, instance):
application = registry.get_by_model(instance.specific_class)
# It could be that the application related to the instance is already in the
# context else we can call the specific_class property to find it.
application = self.context.get('application')
if not application:
application = registry.get_by_model(instance.specific_class)
return application.type
class ApplicationCreateSerializer(serializers.ModelSerializer):
type = serializers.ChoiceField(choices=registry.get_types())
type = serializers.ChoiceField(choices=lazy(registry.get_types, list)())
class Meta:
model = Application
@ -35,3 +42,23 @@ class ApplicationUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Application
fields = ('name',)
def get_application_serializer(instance, **kwargs):
"""
Returns an instantiated serialized based on the instance class type. Custom
serializers can be defined per application. This function will return that one is
set else it will return the default one.
:param instance: The instance where a serializer is needed for.
:type instance: Application
:return: An instantiated serializer for the instance.
:rtype: ApplicationSerializer
"""
application = registry.get_by_model(instance.specific_class)
serializer_class = application.instance_serializer
if not serializer_class:
serializer_class = ApplicationSerializer
return serializer_class(instance, context={'application': application}, **kwargs)

View file

@ -6,21 +6,22 @@ from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from baserow.api.v0.decorators import validate_body, map_exceptions
from baserow.api.v0.errors import ERROR_USER_NOT_IN_GROUP
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
ApplicationCreateSerializer, ApplicationUpdateSerializer, get_application_serializer
)
from .errors import ERROR_USER_NOT_IN_GROUP
class ApplicationsView(APIView):
permission_classes = (IsAuthenticated,)
core_handler = CoreHandler()
def load_group(self, request, group_id):
@staticmethod
def get_group(request, group_id):
return get_object_or_404(
GroupUser.objects.select_related('group'),
group_id=group_id,
@ -29,27 +30,30 @@ class ApplicationsView(APIView):
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.
Responds with a list of serialized applications that belong to the group if the
user has access to that group.
"""
group_user = self.load_group(request, group_id)
group_user = self.get_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)
data = [
get_application_serializer(application).data
for application in applications
]
return Response(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)
group_user = self.get_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)
return Response(get_application_serializer(application).data)
class ApplicationView(APIView):
@ -63,7 +67,7 @@ class ApplicationView(APIView):
pk=application_id, group__users__in=[request.user]
)
return Response(ApplicationSerializer(application).data)
return Response(get_application_serializer(application).data)
@transaction.atomic
@validate_body(ApplicationUpdateSerializer)
@ -80,7 +84,7 @@ class ApplicationView(APIView):
application = self.core_handler.update_application(
request.user, application, name=data['name'])
return Response(ApplicationSerializer(application).data)
return Response(get_application_serializer(application).data)
@transaction.atomic
@map_exceptions({

View file

@ -40,7 +40,7 @@ def map_exceptions(exceptions):
except tuple(exceptions.keys()) as e:
value = exceptions.get(e.__class__)
status_code = status.HTTP_400_BAD_REQUEST
detail = str(e)
detail = ''
if isinstance(value, str):
error = value

View file

@ -0,0 +1,26 @@
from rest_framework import serializers
from baserow.api.v0.applications.serializers import ApplicationSerializer
from baserow.contrib.database.table.models import Table
from baserow.contrib.database.api.v0.tables.serializers import TableSerializer
class DatabaseSerializer(ApplicationSerializer):
tables = serializers.SerializerMethodField()
class Meta(ApplicationSerializer.Meta):
fields = ApplicationSerializer.Meta.fields + ('tables',)
def get_tables(self, instance):
"""
Because the the instance doesn't know at this point it is a Database we have to
select the related tables this way.
:param instance: The database application instance.
:type instance: Application
:return: A list of serialized tables that belong to this instance.
:rtype: list
"""
tables = Table.objects.filter(database_id=instance.pk)
return TableSerializer(tables, many=True).data

View file

@ -0,0 +1,20 @@
from rest_framework import serializers
from baserow.contrib.database.table.models import Table
class TableSerializer(serializers.ModelSerializer):
class Meta:
model = Table
fields = ('id', 'name', 'order',)
extra_kwargs = {
'id': {
'read_only': True
}
}
class TableCreateUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Table
fields = ('name',)

View file

@ -0,0 +1,11 @@
from django.conf.urls import url
from .views import TablesView, TableView
app_name = 'baserow.contrib.api.v0.tables'
urlpatterns = [
url(r'database/(?P<database_id>[0-9]+)/$', TablesView.as_view(), name='list'),
url(r'(?P<table_id>[0-9]+)/$', TableView.as_view(), name='item'),
]

View file

@ -0,0 +1,115 @@
from django.db import transaction
from django.shortcuts import get_object_or_404
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.api.v0.errors import ERROR_USER_NOT_IN_GROUP
from baserow.core.exceptions import UserNotIngroupError
from baserow.contrib.database.models import Database
from baserow.contrib.database.table.models import Table
from baserow.contrib.database.table.handler import TableHandler
from .serializers import TableSerializer, TableCreateUpdateSerializer
class TablesView(APIView):
permission_classes = (IsAuthenticated,)
table_handler = TableHandler()
@staticmethod
def get_database(user, database_id):
database = get_object_or_404(
Database.objects.select_related('group'),
pk=database_id
)
if not database.group.has_user(user):
raise UserNotIngroupError(f'User {user} doesn\'t belong to group '
f'{database.group}.')
return database
@map_exceptions({
UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
})
def get(self, request, database_id):
"""Lists all the tables of a database."""
database = self.get_database(request.user, database_id)
tables = Table.objects.filter(database=database)
serializer = TableSerializer(tables, many=True)
return Response(serializer.data)
@transaction.atomic
@map_exceptions({
UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
})
@validate_body(TableCreateUpdateSerializer)
def post(self, request, data, database_id):
"""Creates a new table in a database."""
database = self.get_database(request.user, database_id)
table = self.table_handler.create_table(
request.user, database, name=data['name'])
serializer = TableSerializer(table)
return Response(serializer.data)
class TableView(APIView):
permission_classes = (IsAuthenticated,)
table_handler = TableHandler()
@staticmethod
def get_table(user, table_id):
table = get_object_or_404(
Table.objects.select_related('database__group'),
pk=table_id
)
if not table.database.group.has_user(user):
raise UserNotIngroupError(f'User {user} doesn\'t belong to group '
f'{table.database.group}.')
return table
@map_exceptions({
UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
})
def get(self, request, table_id):
"""Responds with a serialized table instance."""
table = self.get_table(request.user, table_id)
serializer = TableSerializer(table)
return Response(serializer.data)
@transaction.atomic
@map_exceptions({
UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
})
@validate_body(TableCreateUpdateSerializer)
def patch(self, request, data, table_id):
"""Updates the values a table instance."""
table = self.table_handler.update_table(
request.user,
self.get_table(request.user, table_id),
name=data['name']
)
serializer = TableSerializer(table)
return Response(serializer.data)
@transaction.atomic
@map_exceptions({
UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
})
def delete(self, request, table_id):
"""Deletes an existing table."""
self.table_handler.delete_table(
request.user,
self.get_table(request.user, table_id)
)
return Response(status=204)

View file

@ -1,5 +1,9 @@
from django.urls import path, include
from .api.v0.tables import urls as table_urls
app_name = 'baserow.contrib.database'
urlpatterns = [
path('tables/', include(table_urls, namespace='tables')),
]

View file

@ -4,11 +4,13 @@ from baserow.core.applications import Application
from . import api_urls
from .models import Database
from .api.v0.serializers import DatabaseSerializer
class DatabaseApplication(Application):
type = 'database'
instance_model = Database
instance_serializer = DatabaseSerializer
def get_api_urls(self):
return [

View file

@ -0,0 +1,24 @@
# Generated by Django 2.2.2 on 2019-10-18 07:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('database', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='table',
old_name='group',
new_name='database',
),
migrations.AddField(
model_name='table',
name='name',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
]

View file

@ -1,12 +1,11 @@
from django.db import models
from baserow.core.models import Application
from .table.models import Table
__all__ = [
'Table'
]
class Database(Application):
pass
class Table(models.Model):
group = models.ForeignKey(Database, on_delete=models.CASCADE)
order = models.PositiveIntegerField()

View file

@ -0,0 +1,76 @@
from baserow.core.exceptions import UserNotIngroupError
from baserow.core.utils import extract_allowed, set_allowed_attrs
from .models import Table
class TableHandler:
def create_table(self, user, database, **kwargs):
"""
Creates a new table.
:param user: The user on whose behalf the table is created.
:type user: User
:param database: The database that the table instance belongs to.
:type database: Database
:param kwargs: The fields that need to be set upon creation.
:type kwargs: object
:return: The created table instance.
:rtype: Table
"""
if not database.group.has_user(user):
raise UserNotIngroupError(f'The user {user} does not belong to the group '
f'{database.group}.')
table_values = extract_allowed(kwargs, ['name'])
last_order = Table.get_last_order(database)
table = Table.objects.create(database=database, order=last_order,
**table_values)
return table
def update_table(self, user, table, **kwargs):
"""
Updates an existing table instance.
:param user: The user on whose behalf the table is updated.
:type user: User
:param table: The table instance that needs to be updated.
:type table: Table
:param kwargs: The fields that need to be updated.
:type kwargs: object
:return: The updated table instance.
:rtype: Table
"""
if not isinstance(table, Table):
raise ValueError('The table is not an instance of Table')
if not table.database.group.has_user(user):
raise UserNotIngroupError(f'The user {user} does not belong to the group '
f'{table.database.group}.')
table = set_allowed_attrs(kwargs, ['name'], table)
table.save()
return table
def delete_table(self, user, table):
"""
Deletes an existing table instance.
:param user: The user on whose behalf the table is deleted.
:type user: User
:param table: The table instance that needs to be deleted.
:type table: Table
"""
if not isinstance(table, Table):
raise ValueError('The table is not an instance of Table')
if not table.database.group.has_user(user):
raise UserNotIngroupError(f'The user {user} does not belong to the group '
f'{table.database.group}.')
table.delete()

View file

@ -0,0 +1,17 @@
from django.db import models
from baserow.core.mixins import OrderableMixin
class Table(OrderableMixin, models.Model):
database = models.ForeignKey('database.Database', on_delete=models.CASCADE)
order = models.PositiveIntegerField()
name = models.CharField(max_length=255)
class Meta:
ordering = ('order',)
@classmethod
def get_last_order(cls, database):
queryset = Table.objects.filter(database=database)
return cls.get_highest_order_of_queryset(queryset) + 1

View file

@ -27,7 +27,13 @@ class Application(object):
"""
type = None
"""A unique string that identifies the application."""
instance_model = None
"""The model instance that is created when adding an application."""
instance_serializer = None
"""This serializer that is used to serialize the instance model."""
def __init__(self):
if not self.type:
@ -92,7 +98,8 @@ class ApplicationRegistry(object):
return self.registry[type]
def get_by_model(self, instance):
"""Returns the application instance of a model or model 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.

View file

@ -106,11 +106,10 @@ class CoreHandler:
application = registry.get(type)
model = application.instance_model
application_values = extract_allowed(kwargs, ['name'])
last_order = model.get_last_order(group)
if 'order' not in application_values:
application_values['order'] = model.get_last_order(group)
instance = model.objects.create(group=group, **application_values)
instance = model.objects.create(group=group, order=last_order,
**application_values)
return instance

View file

@ -39,7 +39,8 @@ class GroupUser(OrderableMixin, models.Model):
@classmethod
def get_last_order(cls, user):
return cls.get_highest_order_of_queryset(cls.objects.filter(user=user)) + 1
queryset = cls.objects.filter(user=user)
return cls.get_highest_order_of_queryset(queryset) + 1
class Application(OrderableMixin, models.Model):
@ -88,5 +89,5 @@ class Application(OrderableMixin, models.Model):
@classmethod
def get_last_order(cls, group):
return cls.get_highest_order_of_queryset(
Application.objects.filter(group=group)) + 1
queryset = Application.objects.filter(group=group)
return cls.get_highest_order_of_queryset(queryset) + 1

View file

@ -0,0 +1,210 @@
import pytest
from django.shortcuts import reverse
from baserow.contrib.database.table.models import Table
@pytest.mark.django_db
def test_list_tables(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
database = data_fixture.create_database_application(user=user)
database_2 = data_fixture.create_database_application()
table_1 = data_fixture.create_database_table(database=database, order=2)
table_2 = data_fixture.create_database_table(database=database, order=1)
table_3 = data_fixture.create_database_table(database=database_2)
url = reverse('api_v0:database:tables:list', kwargs={'database_id': database.id})
response = api_client.get(url, HTTP_AUTHORIZATION=f'JWT {token}')
response_json = response.json()
assert response.status_code == 200
assert len(response_json) == 2
assert response_json[0]['id'] == table_2.id
assert response_json[1]['id'] == table_1.id
url = reverse('api_v0:database:tables:list', kwargs={'database_id': database_2.id})
response = api_client.get(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:database:tables:list', kwargs={'database_id': 9999})
response = api_client.get(url, **{
'HTTP_AUTHORIZATION': f'JWT {token}'
})
assert response.status_code == 404
@pytest.mark.django_db
def test_create_table(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
database = data_fixture.create_database_application(user=user)
database_2 = data_fixture.create_database_application()
url = reverse('api_v0:database:tables:list', kwargs={'database_id': database.id})
response = api_client.post(
url,
{'name': 'Test 1'},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
assert response.status_code == 200
json_response = response.json()
Table.objects.all().count() == 1
table = Table.objects.filter(database=database).first()
assert table.order == json_response['order'] == 1
assert table.name == json_response['name']
assert table.id == json_response['id']
url = reverse('api_v0:database:tables:list', kwargs={'database_id': database_2.id})
response = api_client.post(
url,
{'name': 'Test 1'},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
assert response.status_code == 400
assert response.json()['error'] == 'ERROR_USER_NOT_IN_GROUP'
url = reverse('api_v0:database:tables:list', kwargs={'database_id': database.id})
response = api_client.post(
url,
{'not_a_name': 'Test 1'},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
assert response.status_code == 400
assert response.json()['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
url = reverse('api_v0:database:tables:list', kwargs={'database_id': 9999})
response = api_client.post(
url,
{'name': 'Test 1'},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
assert response.status_code == 404
@pytest.mark.django_db
def test_get_table(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table_1 = data_fixture.create_database_table(user=user)
table_2 = data_fixture.create_database_table()
url = reverse('api_v0:database:tables:item', kwargs={'table_id': table_1.id})
response = api_client.get(url, HTTP_AUTHORIZATION=f'JWT {token}')
assert response.status_code == 200
json_response = response.json()
assert json_response['id'] == table_1.id
assert json_response['name'] == table_1.name
assert json_response['order'] == table_1.order
url = reverse('api_v0:database:tables:item', kwargs={'table_id': table_2.id})
response = api_client.get(url, HTTP_AUTHORIZATION=f'JWT {token}')
json_response = response.json()
assert response.status_code == 400
assert json_response['error'] == 'ERROR_USER_NOT_IN_GROUP'
url = reverse('api_v0:database:tables:item', kwargs={'table_id': 9999})
response = api_client.get(url, HTTP_AUTHORIZATION=f'JWT {token}')
assert response.status_code == 404
@pytest.mark.django_db
def test_update_table(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table_1 = data_fixture.create_database_table(user=user)
table_2 = data_fixture.create_database_table()
url = reverse('api_v0:database:tables:item', kwargs={'table_id': table_1.id})
response = api_client.patch(
url,
{'name': 'New name'},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
assert response.status_code == 200
response_json = response.json()
table_1.refresh_from_db()
assert response_json['id'] == table_1.id
assert response_json['name'] == table_1.name == 'New name'
url = reverse('api_v0:database:tables:item', kwargs={'table_id': table_2.id})
response = api_client.patch(
url,
{'name': 'New name'},
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:database:tables:item', kwargs={'table_id': table_1.id})
response = api_client.patch(
url,
{'not_a_name': 'New name'},
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:database:tables:item', kwargs={'table_id': 999})
response = api_client.patch(
url,
{'name': 'New name'},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
assert response.status_code == 404
@pytest.mark.django_db
def test_delete_group(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table_1 = data_fixture.create_database_table(user=user)
table_2 = data_fixture.create_database_table()
assert Table.objects.all().count() == 2
url = reverse('api_v0:database:tables:item', kwargs={'table_id': table_1.id})
response = api_client.delete(url, HTTP_AUTHORIZATION=f'JWT {token}')
assert response.status_code == 204
assert Table.objects.all().count() == 1
url = reverse('api_v0:database:tables:item', kwargs={'table_id': table_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:database:tables:item', kwargs={'table_id': 9999})
response = api_client.delete(url, HTTP_AUTHORIZATION=f'JWT {token}')
assert response.status_code == 404
@pytest.mark.django_db
def test_get_database_application_with_tables(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
database = data_fixture.create_database_application(user=user)
table_1 = data_fixture.create_database_table(database=database, order=0)
table_2 = data_fixture.create_database_table(database=database, order=1)
table_3 = data_fixture.create_database_table()
url = reverse('api_v0:applications:item', kwargs={'application_id': database.id})
response = api_client.get(
url,
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
response_json = response.json()
assert response.status_code == 200
assert len(response_json['tables']) == 2
assert response_json['tables'][0]['id'] == table_1.id
assert response_json['tables'][1]['id'] == table_2.id

View file

@ -0,0 +1,62 @@
import pytest
from baserow.core.exceptions import UserNotIngroupError
from baserow.contrib.database.table.models import Table
from baserow.contrib.database.table.handler import TableHandler
@pytest.mark.django_db
def test_create_database_table(data_fixture):
user = data_fixture.create_user()
user_2 = data_fixture.create_user()
database = data_fixture.create_database_application(user=user)
handler = TableHandler()
handler.create_table(user=user, database=database, name='Test table')
assert Table.objects.all().count() == 1
table = Table.objects.all().first()
assert table.name == 'Test table'
assert table.order == 1
assert table.database == database
with pytest.raises(UserNotIngroupError):
handler.create_table(user=user_2, database=database, name='')
@pytest.mark.django_db
def test_update_database_table(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)
table = data_fixture.create_database_table(database=database)
handler = TableHandler()
with pytest.raises(UserNotIngroupError):
handler.update_table(user=user_2, table=table, name='Test 1')
handler.update_table(user=user, table=table, name='Test 1')
table.refresh_from_db()
assert table.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)
table = data_fixture.create_database_table(database=database)
handler = TableHandler()
with pytest.raises(UserNotIngroupError):
handler.delete_table(user=user_2, table=table)
assert Table.objects.all().count() == 1
handler.delete_table(user=user, table=table)
assert Table.objects.all().count() == 0

View file

@ -0,0 +1,15 @@
import pytest
from baserow.contrib.database.table.models import Table
@pytest.mark.django_db
def test_group_user_get_next_order(data_fixture):
database = data_fixture.create_database_application()
database_2 = data_fixture.create_database_application()
data_fixture.create_database_table(order=1, database=database)
data_fixture.create_database_table(order=2, database=database)
data_fixture.create_database_table(order=10, database=database_2)
assert Table.get_last_order(database) == 3
assert Table.get_last_order(database_2) == 11

View file

@ -3,7 +3,8 @@ from faker import Faker
from .user import UserFixtures
from .group import GroupFixtures
from .application import ApplicationFixtures
from .table import TableFixtures
class Fixtures(UserFixtures, GroupFixtures, ApplicationFixtures):
class Fixtures(UserFixtures, GroupFixtures, ApplicationFixtures, TableFixtures):
fake = Faker()

View file

@ -2,9 +2,9 @@ from baserow.contrib.database.models import Database
class ApplicationFixtures:
def create_database_application(self, **kwargs):
def create_database_application(self, user=None, **kwargs):
if 'group' not in kwargs:
kwargs['group'] = self.create_group()
kwargs['group'] = self.create_group(user=user)
if 'name' not in kwargs:
kwargs['name'] = self.fake.name()

15
backend/tests/fixtures/table.py vendored Normal file
View file

@ -0,0 +1,15 @@
from baserow.contrib.database.table.models import Table
class TableFixtures:
def create_database_table(self, user=None, **kwargs):
if 'database' not in kwargs:
kwargs['database'] = self.create_database_application(user=user)
if 'name' not in kwargs:
kwargs['name'] = self.fake.name()
if 'order' not in kwargs:
kwargs['order'] = 0
return Table.objects.create(**kwargs)

View file

@ -95,6 +95,15 @@ export default {
})
},
selectApplication(application) {
// If there is no route associated with the application we just change the
// selected state.
if (application._.type.routeName === null) {
this.$store.dispatch('application/select', application)
return
}
// If we do have a route, this the related component with that route has to do
// the state change.
this.setLoading(application, true)
this.$nuxt.$router.push(

View file

@ -72,9 +72,6 @@ export class Application {
if (this.name === null) {
throw new Error('The name of an application must be set.')
}
if (this.routeName === null) {
throw new Error('The route name of an application must be set.')
}
}
/**
@ -89,4 +86,14 @@ export class Application {
hasSelectedSidebarComponent: this.getSelectedSidebarComponent() !== null
}
}
/**
* Every time a fresh application object is fetched from the backend, it will
* be populated, this is the moment to update some values. Because each
* application can have unique properties, they might need to be populated.
* This method can be overwritten in order the populate the correct values.
*/
populate(application) {
return application
}
}

View file

@ -1,30 +0,0 @@
import { notify404 } from '@/utils/error'
/**
* This mixin can be used in combination with the component where the
* application routes to when selected. It will make sure that the application
* preSelect action is called so that the all the depending information is
* loaded. If something goes wrong while loading this information it will show a
* standard error.
*/
export default {
props: {
id: {
type: Number,
required: true
}
},
mounted() {
this.$store.dispatch('application/preSelect', this.id).catch(error => {
notify404(
this.$store.dispatch,
error,
'Application not found.',
"The application with the provided id doesn't exist or you " +
"don't have access to it."
)
this.$nuxt.$router.push({ name: 'app' })
})
}
}

View file

@ -1,11 +1,16 @@
import { Application } from '@/core/applications'
import Sidebar from '@/modules/database/components/Sidebar'
import { populateTable } from '@/modules/database/store/table'
export class DatabaseApplication extends Application {
getType() {
static getType() {
return 'database'
}
getType() {
return DatabaseApplication.getType()
}
getIconClass() {
return 'database'
}
@ -14,11 +19,13 @@ export class DatabaseApplication extends Application {
return 'Database'
}
getRouteName() {
return 'application-database'
}
getSelectedSidebarComponent() {
return Sidebar
}
populate(application) {
const values = super.populate(application)
values.tables.forEach((object, index, tables) => populateTable(object))
return values
}
}

View file

@ -1,38 +1,36 @@
<template>
<div>
<ul class="tree-subs">
<li class="tree-sub active">
<a href="#" class="tree-sub-link">@TODO</a>
<a
class="tree-options"
@click="
$refs.context.toggle($event.currentTarget, 'bottom', 'right', 0)
"
>
<i class="fas fa-ellipsis-v"></i>
</a>
<Context ref="context">
<div class="context-menu-title">@TODO</div>
<ul class="context-menu">
<li>
<a>
<i class="context-menu-icon fas fa-fw fa-pen"></i>
Rename
</a>
</li>
<li>
<a>
<i class="context-menu-icon fas fa-fw fa-trash"></i>
Delete
</a>
</li>
</ul>
</Context>
</li>
<SidebarItem
v-for="table in application.tables"
:key="table.id"
:database="application"
:table="table"
></SidebarItem>
</ul>
<a href="#" class="tree-sub-add">
<a class="tree-sub-add" @click="$refs.createTableModal.show()">
<i class="fas fa-plus"></i>
Create table
</a>
<CreateTableModal
ref="createTableModal"
:application="application"
></CreateTableModal>
</div>
</template>
<script>
import SidebarItem from '@/modules/database/components/table/SidebarItem'
import CreateTableModal from '@/modules/database/components/table/CreateTableModal'
export default {
name: 'Sidebar',
components: { SidebarItem, CreateTableModal },
props: {
application: {
type: Object,
required: true
}
}
}
</script>

View file

@ -0,0 +1,55 @@
<template>
<Modal>
<h2 class="box-title">Create new table</h2>
<TableForm ref="tableForm" @submitted="submitted">
<div class="actions">
<div class="align-right">
<button
class="button button-large"
:class="{ 'button-loading': loading }"
:disabled="loading"
>
Add table
</button>
</div>
</div>
</TableForm>
</Modal>
</template>
<script>
import TableForm from './TableForm'
import modal from '@/mixins/modal'
export default {
name: 'CreateTableModal',
components: { TableForm },
mixins: [modal],
props: {
application: {
type: Object,
required: true
}
},
data() {
return {
loading: false
}
},
methods: {
submitted(values) {
this.loading = true
this.$store
.dispatch('table/create', { database: this.application, values })
.then(() => {
this.loading = false
this.hide()
})
.catch(() => {
this.loading = false
})
}
}
}
</script>

View file

@ -0,0 +1,99 @@
<template>
<li class="tree-sub" :class="{ active: table._.selected }">
<nuxt-link
:to="{
name: 'database-table',
params: {
id: database.id,
tableId: table.id
}
}"
class="tree-sub-link"
>
<Editable
ref="rename"
:value="table.name"
@change="renameTable(database, table, $event)"
></Editable>
</nuxt-link>
<a
v-show="!database._.loading"
class="tree-options"
@click="$refs.context.toggle($event.currentTarget, 'bottom', 'right', 0)"
>
<i class="fas fa-ellipsis-v"></i>
</a>
<Context ref="context">
<div class="context-menu-title">{{ table.name }}</div>
<ul class="context-menu">
<li>
<a @click="enableRename()">
<i class="context-menu-icon fas fa-fw fa-pen"></i>
Rename
</a>
</li>
<li>
<a @click="deleteTable(database, table)">
<i class="context-menu-icon fas fa-fw fa-trash"></i>
Delete
</a>
</li>
</ul>
</Context>
</li>
</template>
<script>
export default {
name: 'SidebarItem',
props: {
database: {
type: Object,
required: true
},
table: {
type: Object,
required: true
}
},
methods: {
setLoading(database, value) {
this.$store.dispatch('application/setItemLoading', {
application: database,
value
})
},
deleteTable(database, table) {
this.$refs.context.hide()
this.setLoading(database, true)
this.$store.dispatch('table/delete', { database, table }).then(() => {
this.setLoading(database, false)
})
},
enableRename() {
this.$refs.context.hide()
this.$refs.rename.edit()
},
renameTable(database, table, event) {
this.setLoading(database, true)
this.$store
.dispatch('table/update', {
database,
table,
values: {
name: event.value
}
})
.catch(() => {
// If something is going wrong we will reset the original value.
this.$refs.rename.set(event.oldValue)
})
.then(() => {
this.setLoading(database, false)
})
}
}
}
</script>

View file

@ -0,0 +1,50 @@
<template>
<form @submit.prevent="submit">
<div class="control">
<label class="control-label">
<i class="fas fa-font"></i>
Name
</label>
<div class="control-elements">
<input
ref="name"
v-model="values.name"
:class="{ 'input-error': $v.values.name.$error }"
type="text"
class="input input-large"
@blur="$v.values.name.$touch()"
/>
<div v-if="$v.values.name.$error" class="error">
This field is required.
</div>
</div>
</div>
<slot></slot>
</form>
</template>
<script>
import { required } from 'vuelidate/lib/validators'
import form from '@/mixins/form'
export default {
name: 'TableForm',
mixins: [form],
data() {
return {
values: {
name: ''
}
}
},
validations: {
values: {
name: { required }
}
},
mounted() {
this.$refs.name.focus()
}
}
</script>

View file

@ -9,6 +9,7 @@ export default function DatabaseModule(options) {
filename: 'plugin.js'
})
// Add all the related routes.
this.extendRoutes(routes => {
routes.push(...databaseRoutes)
})

View file

@ -1,29 +0,0 @@
<template>
<div>
<header class="layout-col-3-1 header">
<ul class="header-filter">
<li class="header-filter-item">&nbsp;</li>
</ul>
<ul class="header-info">
<li>{{ selectedApplication.name }}</li>
<li>@TODO table name</li>
</ul>
</header>
</div>
</template>
<script>
import { mapState } from 'vuex'
import application from '@/mixins/application'
export default {
layout: 'app',
mixins: [application],
computed: {
...mapState({
selectedApplication: state => state.application.selected
})
}
}
</script>

View file

@ -0,0 +1,47 @@
<template>
<div>
<header class="layout-col-3-1 header">
<ul class="header-filter">
<li class="header-filter-item">&nbsp;</li>
</ul>
<ul class="header-info">
<li>{{ database }}</li>
<li>{{ table }}</li>
</ul>
</header>
</div>
</template>
<script>
// import { notify404 } from '@/utils/error'
export default {
layout: 'app',
props: {
id: {
type: Number,
required: true
},
tableId: {
type: Number,
required: true
}
},
asyncData({ store, params, redirect }) {
// @TODO figure out why the id's aren't converted to an int in the route.
const databaseId = parseInt(params.id)
const tableId = parseInt(params.tableId)
return store
.dispatch('table/preSelect', { databaseId, tableId })
.then(data => {
return { database: data.database, table: data.table }
})
.catch(() => {
// If something went wrong this will probably mean that the user doesn't have
// access to the database so we will need to redirect back to the index page.
redirect({ name: 'app' })
})
}
}
</script>

View file

@ -1,5 +1,7 @@
import { DatabaseApplication } from '@/modules/database/application'
import tableStore from '@/modules/database/store/table'
export default ({ store }) => {
store.registerModule('table', tableStore)
store.dispatch('application/register', new DatabaseApplication())
}

View file

@ -2,13 +2,15 @@ import path from 'path'
export const databaseRoutes = [
{
name: 'application-database',
path: '/database/:id',
component: path.resolve(__dirname, 'pages/Database.vue'),
name: 'database-table',
path: '/database/:id/table/:tableId',
component: path.resolve(__dirname, 'pages/Table.vue'),
props(route) {
const props = { ...route.params }
props.id = parseInt(props.id)
return props
// @TODO figure out why the route param is empty on the server side.
const p = { ...route.params }
p.id = parseInt(p.id)
p.tableId = parseInt(p.tableId)
return p
}
}
]

View file

@ -0,0 +1,19 @@
import { client } from '@/services/client'
export default {
fetchAll(databaseId) {
return client.get(`/database/tables/database/${databaseId}/`)
},
create(databaseId, values) {
return client.post(`/database/tables/database/${databaseId}/`, values)
},
get(tableId) {
return client.get(`/database/tables/${tableId}/`)
},
update(tableId, values) {
return client.patch(`/database/tables/${tableId}/`, values)
},
delete(tableId) {
return client.delete(`/database/tables/${tableId}/`)
}
}

View file

@ -0,0 +1,190 @@
import TableService from '@/modules/database/services/table'
import { DatabaseApplication } from '@/modules/database/application'
import { notify404, notifyError } from '@/utils/error'
export function populateTable(table) {
table._ = {
disabled: false,
selected: false
}
return table
}
export const state = () => ({
selected: {}
})
export const mutations = {
ADD_ITEM(state, { database, table }) {
populateTable(table)
database.tables.push(table)
},
UPDATE_ITEM(state, { table, values }) {
Object.assign(table, table, values)
},
SET_SELECTED(state, { database, table }) {
Object.values(database.tables).forEach(item => {
item._.selected = false
})
table._.selected = true
state.selected = table
},
DELETE_ITEM(state, { database, id }) {
const index = database.tables.findIndex(item => item.id === id)
database.tables.splice(index, 1)
}
}
export const actions = {
/**
* Create a new table based on the provided values and add it to the tables
* of the provided database.
*/
create({ commit, dispatch }, { database, values }) {
const type = DatabaseApplication.getType()
// Check if the provided database (application) has the correct type.
if (database.type !== type) {
throw new Error(
`The provided database application doesn't have the required type
"${type}".`
)
}
return TableService.create(database.id, values)
.then(({ data }) => {
commit('ADD_ITEM', { database, table: data })
})
.catch(error => {
notify404(
dispatch,
error,
'Could not create table',
"You're unable to create a new table for the selected database. " +
". This is because the database doesn't exist."
)
notifyError(
dispatch,
error,
'ERROR_USER_NOT_IN_GROUP',
'Could not create table',
"You're unable to create a new table for the selected database. " +
". This is because you're not part of the group."
)
throw error
})
},
/**
* Update an existing table of the provided database with the provided tables.
*/
update({ commit, dispatch }, { database, table, values }) {
return TableService.update(table.id, values)
.then(({ data }) => {
commit('UPDATE_ITEM', { database, table, values: data })
})
.catch(error => {
notify404(
dispatch,
error,
'Could not update table',
"You're unable to update the table. This is because the " +
"table doesn't exist."
)
notifyError(
dispatch,
error,
'ERROR_USER_NOT_IN_GROUP',
'Could not update table',
"You're unable to update the table. This is because you're " +
'not part of the group.'
)
throw error
})
},
/**
* Deletes an existing application.
*/
delete({ commit, dispatch }, { database, table }) {
return TableService.delete(table.id)
.then(() => {
commit('DELETE_ITEM', { database, id: table.id })
})
.catch(error => {
notify404(
dispatch,
error,
'Could not delete table',
"You're unable to delete the table for the selected database. " +
". This is because the database doesn't exist."
)
notifyError(
dispatch,
error,
'ERROR_USER_NOT_IN_GROUP',
'Unable to delete',
"You're not allowed to delete the table because you're" +
' not part of the group where the application is in.'
)
throw error
})
},
/**
* Select a table of a database.
*/
select({ commit }, { database, table }) {
commit('SET_SELECTED', { database, table })
return { database, table }
},
/**
* First it will preSelect the application to make sure the groups are
* fetched, the correct group is selected, the related applications are
* selected and the provided application id is selected. After that is will
* check if the application has the correct type which is a database and makes
* sure that the table actually belongs to that database. If so it will select
* the table and return the database and table so it can be used.
*/
preSelect({ dispatch, getters, rootGetters }, { databaseId, tableId }) {
// Preselect the application
return dispatch('application/preSelect', databaseId, { root: true }).then(
database => {
const type = DatabaseApplication.getType()
// Check if the just selected application has the correct type because
// it needs to have tables.
if (database.type !== type) {
throw new Error(
`The application doesn't have the required ${type} type.`
)
}
// Check if the provided table id is found is the just selected
// database.
const index = database.tables.findIndex(item => item.id === tableId)
if (index === -1) {
throw new Error('The table is not found in the selected application.')
}
const table = database.tables[index]
// Select the table table and return the database and table instance
// when done.
return dispatch('select', { database, table })
}
)
}
}
export const getters = {}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View file

@ -10,7 +10,9 @@
<br /><br />
{{ groupApplications }}
<br /><br />
<nuxt-link :to="{ name: 'application-database', params: { id: 1 } }">
<nuxt-link
:to="{ name: 'database-table', params: { id: 1, tableId: 1 } }"
>
<i class="fas fa-arrow-left"></i>
App
</nuxt-link>

View file

@ -10,7 +10,7 @@ function populateApplication(application, getters) {
loading: false,
selected: false
}
return application
return type.populate(application)
}
export const state = () => ({
@ -143,6 +143,8 @@ export const actions = {
"You're unable to create a new application for the selected " +
"group. This could be because you're not part of the group."
)
throw error
})
},
/**
@ -158,10 +160,12 @@ export const actions = {
dispatch,
error,
'ERROR_USER_NOT_IN_GROUP',
'Rename not allowed',
"You're not allowed to rename the application because you're " +
'Change not allowed',
"You're not allowed to change the application because you're " +
'not part of the group where the application is in.'
)
throw error
})
},
/**
@ -181,6 +185,8 @@ export const actions = {
"You're not allowed to rename the application because you're" +
' not part of the group where the application is in.'
)
throw error
})
},
/**
@ -188,6 +194,7 @@ export const actions = {
*/
select({ commit }, application) {
commit('SET_SELECTED', application)
return application
},
/**
* Select an application by a given application id.
@ -213,17 +220,17 @@ export const actions = {
* to date. In short it will make sure that the depending state of the given
* application will be there.
*/
preSelect({ dispatch, getters, rootGetters }, id) {
preSelect({ dispatch, getters, rootGetters, state }, id) {
// First we will check if the application is already in the items.
const application = getters.get(id)
// If the application is already selected we don't have to do anything.
if (application !== undefined && application._.selected) {
return
return application
}
// This function will select a group by its id which will then automatically
// fetch the applications related to that group. When done it will select
// fetches the applications related to that group. When done it will select
// the provided application id.
const selectGroupAndApplication = (groupId, applicationId) => {
return dispatch('group/selectById', groupId, {
@ -237,19 +244,20 @@ export const actions = {
// If the application is already in the selected groups, which means that
// the groups and applications are already loaded, we can just select that
// application.
dispatch('select', application)
return dispatch('select', application)
} else {
// The application is not in the selected group so we need to figure out
// in which he is by fetching the application.
return ApplicationService.get(id).then(data => {
return ApplicationService.get(id).then(response => {
if (!rootGetters['group/isLoaded']) {
// If the groups are not already loaded we need to load them first.
return dispatch('group/fetchAll', {}, { root: true }).then(() => {
return selectGroupAndApplication(data.data.group.id, id)
return selectGroupAndApplication(response.data.group.id, id)
})
} else {
// The groups are already loaded so we
return selectGroupAndApplication(data.data.group.id, id)
// The groups are already loaded so we need to select the group and
// application.
return selectGroupAndApplication(response.data.group.id, id)
}
})
}

View file

@ -122,10 +122,12 @@ export const actions = {
notify404(
dispatch,
error,
'Unable to rename',
"You're unable to rename the group. This could be because " +
'Unable to update',
"You're unable to update the group. This could be because " +
"you're not part of the group."
)
throw error
})
},
/**
@ -148,6 +150,8 @@ export const actions = {
"You're unable to delete the group. This could be because " +
"you're not part of the group."
)
throw error
})
},
/**