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:
parent
3b2eb13042
commit
497447edb8
45 changed files with 1226 additions and 150 deletions
backend
src/baserow
tests
baserow/contrib/database
fixtures
web-frontend
components/sidebar
core
mixins
modules/database
pages/app
store
|
@ -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)
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
|
0
backend/src/baserow/contrib/database/api/__init__.py
Normal file
0
backend/src/baserow/contrib/database/api/__init__.py
Normal file
0
backend/src/baserow/contrib/database/api/v0/__init__.py
Normal file
0
backend/src/baserow/contrib/database/api/v0/__init__.py
Normal file
26
backend/src/baserow/contrib/database/api/v0/serializers.py
Normal file
26
backend/src/baserow/contrib/database/api/v0/serializers.py
Normal 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
|
|
@ -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',)
|
11
backend/src/baserow/contrib/database/api/v0/tables/urls.py
Normal file
11
backend/src/baserow/contrib/database/api/v0/tables/urls.py
Normal 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'),
|
||||
]
|
115
backend/src/baserow/contrib/database/api/v0/tables/views.py
Normal file
115
backend/src/baserow/contrib/database/api/v0/tables/views.py
Normal 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)
|
|
@ -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')),
|
||||
]
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
]
|
|
@ -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()
|
||||
|
|
0
backend/src/baserow/contrib/database/table/__init__.py
Normal file
0
backend/src/baserow/contrib/database/table/__init__.py
Normal file
76
backend/src/baserow/contrib/database/table/handler.py
Normal file
76
backend/src/baserow/contrib/database/table/handler.py
Normal 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()
|
17
backend/src/baserow/contrib/database/table/models.py
Normal file
17
backend/src/baserow/contrib/database/table/models.py
Normal 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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
62
backend/tests/baserow/contrib/database/table/test_handler.py
Normal file
62
backend/tests/baserow/contrib/database/table/test_handler.py
Normal 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
|
15
backend/tests/baserow/contrib/database/table/test_models.py
Normal file
15
backend/tests/baserow/contrib/database/table/test_models.py
Normal 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
|
3
backend/tests/fixtures/__init__.py
vendored
3
backend/tests/fixtures/__init__.py
vendored
|
@ -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()
|
||||
|
|
4
backend/tests/fixtures/application.py
vendored
4
backend/tests/fixtures/application.py
vendored
|
@ -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
15
backend/tests/fixtures/table.py
vendored
Normal 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)
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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' })
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
50
web-frontend/modules/database/components/table/TableForm.vue
Normal file
50
web-frontend/modules/database/components/table/TableForm.vue
Normal 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>
|
|
@ -9,6 +9,7 @@ export default function DatabaseModule(options) {
|
|||
filename: 'plugin.js'
|
||||
})
|
||||
|
||||
// Add all the related routes.
|
||||
this.extendRoutes(routes => {
|
||||
routes.push(...databaseRoutes)
|
||||
})
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<header class="layout-col-3-1 header">
|
||||
<ul class="header-filter">
|
||||
<li class="header-filter-item"> </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>
|
47
web-frontend/modules/database/pages/Table.vue
Normal file
47
web-frontend/modules/database/pages/Table.vue
Normal file
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div>
|
||||
<header class="layout-col-3-1 header">
|
||||
<ul class="header-filter">
|
||||
<li class="header-filter-item"> </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>
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
19
web-frontend/modules/database/services/table.js
Normal file
19
web-frontend/modules/database/services/table.js
Normal 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}/`)
|
||||
}
|
||||
}
|
190
web-frontend/modules/database/store/table.js
Normal file
190
web-frontend/modules/database/store/table.js
Normal 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
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
},
|
||||
/**
|
||||
|
|
Loading…
Add table
Reference in a new issue