diff --git a/backend/src/baserow/api/v0/applications/serializers.py b/backend/src/baserow/api/v0/applications/serializers.py index 1f992c925..7c40978bd 100644 --- a/backend/src/baserow/api/v0/applications/serializers.py +++ b/backend/src/baserow/api/v0/applications/serializers.py @@ -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) diff --git a/backend/src/baserow/api/v0/applications/views.py b/backend/src/baserow/api/v0/applications/views.py index f44a76926..5cf35bc9e 100644 --- a/backend/src/baserow/api/v0/applications/views.py +++ b/backend/src/baserow/api/v0/applications/views.py @@ -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({ diff --git a/backend/src/baserow/api/v0/decorators.py b/backend/src/baserow/api/v0/decorators.py index d909decfd..f8bbd9cbb 100644 --- a/backend/src/baserow/api/v0/decorators.py +++ b/backend/src/baserow/api/v0/decorators.py @@ -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 diff --git a/backend/src/baserow/api/v0/applications/errors.py b/backend/src/baserow/api/v0/errors.py similarity index 100% rename from backend/src/baserow/api/v0/applications/errors.py rename to backend/src/baserow/api/v0/errors.py diff --git a/backend/src/baserow/contrib/database/api/__init__.py b/backend/src/baserow/contrib/database/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/baserow/contrib/database/api/v0/__init__.py b/backend/src/baserow/contrib/database/api/v0/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/baserow/contrib/database/api/v0/serializers.py b/backend/src/baserow/contrib/database/api/v0/serializers.py new file mode 100644 index 000000000..b2d102b48 --- /dev/null +++ b/backend/src/baserow/contrib/database/api/v0/serializers.py @@ -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 diff --git a/backend/src/baserow/contrib/database/api/v0/tables/__init__.py b/backend/src/baserow/contrib/database/api/v0/tables/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/baserow/contrib/database/api/v0/tables/serializers.py b/backend/src/baserow/contrib/database/api/v0/tables/serializers.py new file mode 100644 index 000000000..5c397cf03 --- /dev/null +++ b/backend/src/baserow/contrib/database/api/v0/tables/serializers.py @@ -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',) diff --git a/backend/src/baserow/contrib/database/api/v0/tables/urls.py b/backend/src/baserow/contrib/database/api/v0/tables/urls.py new file mode 100644 index 000000000..8e769becb --- /dev/null +++ b/backend/src/baserow/contrib/database/api/v0/tables/urls.py @@ -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'), +] diff --git a/backend/src/baserow/contrib/database/api/v0/tables/views.py b/backend/src/baserow/contrib/database/api/v0/tables/views.py new file mode 100644 index 000000000..048011a02 --- /dev/null +++ b/backend/src/baserow/contrib/database/api/v0/tables/views.py @@ -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) diff --git a/backend/src/baserow/contrib/database/api_urls.py b/backend/src/baserow/contrib/database/api_urls.py index 916b115e0..357961847 100644 --- a/backend/src/baserow/contrib/database/api_urls.py +++ b/backend/src/baserow/contrib/database/api_urls.py @@ -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')), ] diff --git a/backend/src/baserow/contrib/database/applications.py b/backend/src/baserow/contrib/database/applications.py index 327f4deae..3ccd99c8b 100644 --- a/backend/src/baserow/contrib/database/applications.py +++ b/backend/src/baserow/contrib/database/applications.py @@ -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 [ diff --git a/backend/src/baserow/contrib/database/migrations/0002_auto_20191018_0740.py b/backend/src/baserow/contrib/database/migrations/0002_auto_20191018_0740.py new file mode 100644 index 000000000..6eb53c199 --- /dev/null +++ b/backend/src/baserow/contrib/database/migrations/0002_auto_20191018_0740.py @@ -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, + ), + ] diff --git a/backend/src/baserow/contrib/database/models.py b/backend/src/baserow/contrib/database/models.py index 344526c13..c34e677c8 100644 --- a/backend/src/baserow/contrib/database/models.py +++ b/backend/src/baserow/contrib/database/models.py @@ -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() diff --git a/backend/src/baserow/contrib/database/table/__init__.py b/backend/src/baserow/contrib/database/table/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/baserow/contrib/database/table/handler.py b/backend/src/baserow/contrib/database/table/handler.py new file mode 100644 index 000000000..6bc21101b --- /dev/null +++ b/backend/src/baserow/contrib/database/table/handler.py @@ -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() diff --git a/backend/src/baserow/contrib/database/table/models.py b/backend/src/baserow/contrib/database/table/models.py new file mode 100644 index 000000000..04b9a0c78 --- /dev/null +++ b/backend/src/baserow/contrib/database/table/models.py @@ -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 diff --git a/backend/src/baserow/core/applications.py b/backend/src/baserow/core/applications.py index 221689d42..6b2bf7697 100644 --- a/backend/src/baserow/core/applications.py +++ b/backend/src/baserow/core/applications.py @@ -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. diff --git a/backend/src/baserow/core/handler.py b/backend/src/baserow/core/handler.py index 7e3b853a2..0fef89666 100644 --- a/backend/src/baserow/core/handler.py +++ b/backend/src/baserow/core/handler.py @@ -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 diff --git a/backend/src/baserow/core/models.py b/backend/src/baserow/core/models.py index 7d0299c20..38f92bc2b 100644 --- a/backend/src/baserow/core/models.py +++ b/backend/src/baserow/core/models.py @@ -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 diff --git a/backend/tests/baserow/contrib/database/api/v0/tables/test_table_views.py b/backend/tests/baserow/contrib/database/api/v0/tables/test_table_views.py new file mode 100644 index 000000000..631dfb1ad --- /dev/null +++ b/backend/tests/baserow/contrib/database/api/v0/tables/test_table_views.py @@ -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 diff --git a/backend/tests/baserow/contrib/database/table/test_handler.py b/backend/tests/baserow/contrib/database/table/test_handler.py new file mode 100644 index 000000000..b8a0c0344 --- /dev/null +++ b/backend/tests/baserow/contrib/database/table/test_handler.py @@ -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 diff --git a/backend/tests/baserow/contrib/database/table/test_models.py b/backend/tests/baserow/contrib/database/table/test_models.py new file mode 100644 index 000000000..18249749b --- /dev/null +++ b/backend/tests/baserow/contrib/database/table/test_models.py @@ -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 diff --git a/backend/tests/fixtures/__init__.py b/backend/tests/fixtures/__init__.py index a336658ad..7a566c582 100644 --- a/backend/tests/fixtures/__init__.py +++ b/backend/tests/fixtures/__init__.py @@ -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() diff --git a/backend/tests/fixtures/application.py b/backend/tests/fixtures/application.py index 142382d62..01cf8160f 100644 --- a/backend/tests/fixtures/application.py +++ b/backend/tests/fixtures/application.py @@ -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() diff --git a/backend/tests/fixtures/table.py b/backend/tests/fixtures/table.py new file mode 100644 index 000000000..ad1502d25 --- /dev/null +++ b/backend/tests/fixtures/table.py @@ -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) diff --git a/web-frontend/components/sidebar/SidebarApplication.vue b/web-frontend/components/sidebar/SidebarApplication.vue index 8a2a63e17..eb8a04871 100644 --- a/web-frontend/components/sidebar/SidebarApplication.vue +++ b/web-frontend/components/sidebar/SidebarApplication.vue @@ -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( diff --git a/web-frontend/core/applications.js b/web-frontend/core/applications.js index 8e7380eff..99b6f2165 100644 --- a/web-frontend/core/applications.js +++ b/web-frontend/core/applications.js @@ -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 + } } diff --git a/web-frontend/mixins/application.js b/web-frontend/mixins/application.js deleted file mode 100644 index d98aa23ba..000000000 --- a/web-frontend/mixins/application.js +++ /dev/null @@ -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' }) - }) - } -} diff --git a/web-frontend/modules/database/application.js b/web-frontend/modules/database/application.js index 16a60c2b9..aa70fd5e9 100644 --- a/web-frontend/modules/database/application.js +++ b/web-frontend/modules/database/application.js @@ -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 + } } diff --git a/web-frontend/modules/database/components/Sidebar.vue b/web-frontend/modules/database/components/Sidebar.vue index 0145d5cf5..e0341fdb5 100644 --- a/web-frontend/modules/database/components/Sidebar.vue +++ b/web-frontend/modules/database/components/Sidebar.vue @@ -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> diff --git a/web-frontend/modules/database/components/table/CreateTableModal.vue b/web-frontend/modules/database/components/table/CreateTableModal.vue new file mode 100644 index 000000000..1de81ec4b --- /dev/null +++ b/web-frontend/modules/database/components/table/CreateTableModal.vue @@ -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> diff --git a/web-frontend/modules/database/components/table/SidebarItem.vue b/web-frontend/modules/database/components/table/SidebarItem.vue new file mode 100644 index 000000000..8b7ebbccb --- /dev/null +++ b/web-frontend/modules/database/components/table/SidebarItem.vue @@ -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> diff --git a/web-frontend/modules/database/components/table/TableForm.vue b/web-frontend/modules/database/components/table/TableForm.vue new file mode 100644 index 000000000..6daeb553c --- /dev/null +++ b/web-frontend/modules/database/components/table/TableForm.vue @@ -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> diff --git a/web-frontend/modules/database/module.js b/web-frontend/modules/database/module.js index ab867ce18..4bc5deb47 100644 --- a/web-frontend/modules/database/module.js +++ b/web-frontend/modules/database/module.js @@ -9,6 +9,7 @@ export default function DatabaseModule(options) { filename: 'plugin.js' }) + // Add all the related routes. this.extendRoutes(routes => { routes.push(...databaseRoutes) }) diff --git a/web-frontend/modules/database/pages/Database.vue b/web-frontend/modules/database/pages/Database.vue deleted file mode 100644 index a1017bf21..000000000 --- a/web-frontend/modules/database/pages/Database.vue +++ /dev/null @@ -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> diff --git a/web-frontend/modules/database/pages/Table.vue b/web-frontend/modules/database/pages/Table.vue new file mode 100644 index 000000000..9555a3c45 --- /dev/null +++ b/web-frontend/modules/database/pages/Table.vue @@ -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> diff --git a/web-frontend/modules/database/plugin.js b/web-frontend/modules/database/plugin.js index 784b13c82..fdbd108ab 100644 --- a/web-frontend/modules/database/plugin.js +++ b/web-frontend/modules/database/plugin.js @@ -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()) } diff --git a/web-frontend/modules/database/routes.js b/web-frontend/modules/database/routes.js index cc110db65..7a66831b9 100644 --- a/web-frontend/modules/database/routes.js +++ b/web-frontend/modules/database/routes.js @@ -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 } } ] diff --git a/web-frontend/modules/database/services/table.js b/web-frontend/modules/database/services/table.js new file mode 100644 index 000000000..892e0c47d --- /dev/null +++ b/web-frontend/modules/database/services/table.js @@ -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}/`) + } +} diff --git a/web-frontend/modules/database/store/table.js b/web-frontend/modules/database/store/table.js new file mode 100644 index 000000000..be54751c2 --- /dev/null +++ b/web-frontend/modules/database/store/table.js @@ -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 +} diff --git a/web-frontend/pages/app/index.vue b/web-frontend/pages/app/index.vue index 01d71e138..658b62921 100644 --- a/web-frontend/pages/app/index.vue +++ b/web-frontend/pages/app/index.vue @@ -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> diff --git a/web-frontend/store/application.js b/web-frontend/store/application.js index 0dfeb8552..0edaaeeb3 100644 --- a/web-frontend/store/application.js +++ b/web-frontend/store/application.js @@ -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) } }) } diff --git a/web-frontend/store/group.js b/web-frontend/store/group.js index d19eab34c..4d7b70093 100644 --- a/web-frontend/store/group.js +++ b/web-frontend/store/group.js @@ -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 }) }, /**