mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-10 23:50:12 +00:00
Merge branch 'develop'
This commit is contained in:
commit
ab62f98ecb
14 changed files with 217 additions and 21 deletions
README.md
backend
changelog.mdweb-frontend
|
@ -109,7 +109,7 @@ Created by Bram Wiepjes (Baserow) - bram@baserow.io.
|
|||
|
||||
Distributes under the MIT license. See `LICENSE` for more information.
|
||||
|
||||
Version: 0.7.0
|
||||
Version: 0.7.1
|
||||
|
||||
The official repository can be found at https://gitlab.com/bramw/baserow.
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from setuptools import find_packages, setup
|
|||
|
||||
PROJECT_DIR = os.path.dirname(__file__)
|
||||
REQUIREMENTS_DIR = os.path.join(PROJECT_DIR, 'requirements')
|
||||
VERSION = '0.7.0'
|
||||
VERSION = '0.7.1'
|
||||
|
||||
|
||||
def get_requirements(env):
|
||||
|
|
|
@ -153,7 +153,7 @@ SPECTACULAR_SETTINGS = {
|
|||
'name': 'MIT',
|
||||
'url': 'https://gitlab.com/bramw/baserow/-/blob/master/LICENSE'
|
||||
},
|
||||
'VERSION': '0.7.0',
|
||||
'VERSION': '0.7.1',
|
||||
'SERVE_INCLUDE_SCHEMA': False,
|
||||
'TAGS': [
|
||||
{'name': 'User'},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.utils.functional import lazy
|
||||
from django.db import models
|
||||
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
@ -66,6 +67,21 @@ class UpdateFieldSerializer(serializers.ModelSerializer):
|
|||
}
|
||||
|
||||
|
||||
class LinkRowListSerializer(serializers.ListSerializer):
|
||||
def to_representation(self, data):
|
||||
"""
|
||||
Data that is fetched is always from another Table model and when fetching
|
||||
that data we always need to respect the field enhancements. Otherwise it
|
||||
could for example fail when we want to fetch the related select options that
|
||||
could be in another database and table.
|
||||
"""
|
||||
|
||||
if isinstance(data, models.Manager):
|
||||
data = data.all().enhance_by_fields()
|
||||
|
||||
return super().to_representation(data)
|
||||
|
||||
|
||||
class LinkRowValueSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField(help_text='The unique identifier of the row in the '
|
||||
'related table.')
|
||||
|
|
|
@ -118,7 +118,10 @@ def lenient_schema_editor(connection, alter_column_prepare_value=None,
|
|||
if alert_column_type_function:
|
||||
kwargs['alert_column_type_function'] = alert_column_type_function
|
||||
|
||||
with connection.schema_editor(**kwargs) as schema_editor:
|
||||
yield schema_editor
|
||||
|
||||
connection.SchemaEditorClass = regular_schema_editor
|
||||
try:
|
||||
with connection.schema_editor(**kwargs) as schema_editor:
|
||||
yield schema_editor
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
connection.SchemaEditorClass = regular_schema_editor
|
||||
|
|
|
@ -17,8 +17,8 @@ from rest_framework import serializers
|
|||
from baserow.core.models import UserFile
|
||||
from baserow.core.user_files.exceptions import UserFileDoesNotExist
|
||||
from baserow.contrib.database.api.fields.serializers import (
|
||||
LinkRowValueSerializer, FileFieldRequestSerializer, FileFieldResponseSerializer,
|
||||
SelectOptionSerializer
|
||||
LinkRowListSerializer, LinkRowValueSerializer, FileFieldRequestSerializer,
|
||||
FileFieldResponseSerializer, SelectOptionSerializer
|
||||
)
|
||||
from baserow.contrib.database.api.fields.errors import (
|
||||
ERROR_LINK_ROW_TABLE_NOT_IN_SAME_DATABASE, ERROR_LINK_ROW_TABLE_NOT_PROVIDED,
|
||||
|
@ -348,8 +348,9 @@ class LinkRowFieldType(FieldType):
|
|||
if primary_field:
|
||||
primary_field_name = primary_field['name']
|
||||
|
||||
return LinkRowValueSerializer(many=True, value_field_name=primary_field_name,
|
||||
required=False, **kwargs)
|
||||
return LinkRowListSerializer(child=LinkRowValueSerializer(
|
||||
value_field_name=primary_field_name, required=False, **kwargs
|
||||
))
|
||||
|
||||
def get_serializer_help_text(self, instance):
|
||||
return 'This field accepts an `array` containing the ids of the related rows.' \
|
||||
|
@ -828,6 +829,11 @@ class SingleSelectFieldType(FieldType):
|
|||
f"(lower(%({variable_name})s), '{int(option.id)}')"
|
||||
)
|
||||
|
||||
# If there is no values we don't need to convert the value since all
|
||||
# values should be converted to null.
|
||||
if len(values_mapping) == 0:
|
||||
return None
|
||||
|
||||
return f"""(
|
||||
SELECT value FROM (
|
||||
VALUES {','.join(values_mapping)}
|
||||
|
|
|
@ -236,14 +236,14 @@ class FieldHandler:
|
|||
try:
|
||||
schema_editor.alter_field(from_model, from_model_field,
|
||||
to_model_field)
|
||||
except (ProgrammingError, DataError):
|
||||
except (ProgrammingError, DataError) as e:
|
||||
# If something is going wrong while changing the schema we will
|
||||
# just raise a specific exception. In the future we want to have
|
||||
# some sort of converter abstraction where the values of certain
|
||||
# types can be converted to another value.
|
||||
logger.error(str(e))
|
||||
message = f'Could not alter field when changing field type ' \
|
||||
f'{from_field_type} to {new_type_name}.'
|
||||
logger.error(message)
|
||||
raise CannotChangeFieldType(message)
|
||||
|
||||
from_model_field_type = from_model_field.db_parameters(connection)['type']
|
||||
|
|
|
@ -118,6 +118,9 @@ class SelectOption(models.Model):
|
|||
class Meta:
|
||||
ordering = ('order', 'id',)
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class TextField(Field):
|
||||
text_default = models.CharField(
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import pytest
|
||||
from decimal import Decimal
|
||||
from unittest.mock import patch
|
||||
|
||||
from baserow.core.exceptions import UserNotInGroupError
|
||||
from baserow.contrib.database.fields.handler import FieldHandler
|
||||
from baserow.contrib.database.fields.models import (
|
||||
Field, TextField, NumberField, BooleanField, SelectOption
|
||||
)
|
||||
from baserow.contrib.database.fields.field_types import TextFieldType
|
||||
from baserow.contrib.database.fields.registries import field_type_registry
|
||||
from baserow.contrib.database.fields.exceptions import (
|
||||
FieldTypeDoesNotExist, PrimaryFieldAlreadyExists, CannotDeletePrimaryField,
|
||||
FieldDoesNotExist, IncompatiblePrimaryFieldTypeError
|
||||
FieldDoesNotExist, IncompatiblePrimaryFieldTypeError, CannotChangeFieldType
|
||||
)
|
||||
|
||||
|
||||
|
@ -144,10 +147,6 @@ def test_create_primary_field(data_fixture):
|
|||
|
||||
@pytest.mark.django_db
|
||||
def test_update_field(data_fixture):
|
||||
"""
|
||||
@TODO somehow trigger the CannotChangeFieldType and test if it is raised.
|
||||
"""
|
||||
|
||||
user = data_fixture.create_user()
|
||||
user_2 = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
|
@ -237,6 +236,32 @@ def test_update_field(data_fixture):
|
|||
assert getattr(rows[2], f'field_{field.id}') is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_field_failing(data_fixture):
|
||||
# This failing field type triggers the CannotChangeFieldType error if a field is
|
||||
# changed into this type.
|
||||
class FailingFieldType(TextFieldType):
|
||||
def get_alter_column_type_function(self, connection, from_field, to_field):
|
||||
return 'p_in::NOT_VALID_SQL_SO_IT_WILL_FAIL('
|
||||
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
field = data_fixture.create_number_field(table=table, order=1)
|
||||
|
||||
handler = FieldHandler()
|
||||
|
||||
with patch.dict(
|
||||
field_type_registry.registry,
|
||||
{'text': FailingFieldType()}
|
||||
):
|
||||
with pytest.raises(CannotChangeFieldType):
|
||||
handler.update_field(user=user, field=field, new_type_name='text')
|
||||
|
||||
handler.update_field(user, field=field, new_type_name='text')
|
||||
assert Field.objects.all().count() == 1
|
||||
assert TextField.objects.all().count() == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_field(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
|
|
|
@ -174,6 +174,25 @@ def test_single_select_field_type_rows(data_fixture, django_assert_num_queries):
|
|||
assert getattr(row_4, f'field_{field.id}') is None
|
||||
assert getattr(row_4, f'field_{field.id}_id') is None
|
||||
|
||||
field = field_handler.update_field(user=user, field=field, new_type_name='text')
|
||||
assert field.select_options.all().count() == 0
|
||||
model = table.get_model()
|
||||
rows = model.objects.all().enhance_by_fields()
|
||||
assert getattr(rows[0], f'field_{field.id}') is None
|
||||
assert getattr(rows[1], f'field_{field.id}') == 'option 3'
|
||||
assert getattr(rows[2], f'field_{field.id}') is None
|
||||
assert getattr(rows[3], f'field_{field.id}') is None
|
||||
|
||||
field = field_handler.update_field(user=user, field=field,
|
||||
new_type_name='single_select')
|
||||
assert field.select_options.all().count() == 0
|
||||
model = table.get_model()
|
||||
rows = model.objects.all().enhance_by_fields()
|
||||
assert getattr(rows[0], f'field_{field.id}') is None
|
||||
assert getattr(rows[1], f'field_{field.id}') is None
|
||||
assert getattr(rows[2], f'field_{field.id}') is None
|
||||
assert getattr(rows[3], f'field_{field.id}') is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_single_select_field_type_api_views(api_client, data_fixture):
|
||||
|
@ -473,3 +492,80 @@ def test_single_select_field_type_get_order(data_fixture):
|
|||
rows = view_handler.apply_sorting(grid_view, model.objects.all())
|
||||
row_ids = [row.id for row in rows]
|
||||
assert row_ids == [row_5.id, row_1.id, row_4.id, row_3.id, row_2.id]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_primary_single_select_field_with_link_row_field(api_client, data_fixture):
|
||||
"""
|
||||
We expect the relation to a table that has a single select field to work.
|
||||
"""
|
||||
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
database = data_fixture.create_database_application(user=user, name='Placeholder')
|
||||
example_table = data_fixture.create_database_table(name='Example',
|
||||
database=database)
|
||||
customers_table = data_fixture.create_database_table(name='Customers',
|
||||
database=database)
|
||||
|
||||
field_handler = FieldHandler()
|
||||
row_handler = RowHandler()
|
||||
|
||||
data_fixture.create_text_field(
|
||||
name='Name',
|
||||
table=example_table,
|
||||
primary=True
|
||||
)
|
||||
customers_primary = field_handler.create_field(
|
||||
user=user,
|
||||
table=customers_table,
|
||||
type_name='single_select',
|
||||
select_options=[
|
||||
{'value': 'Option 1', 'color': 'red'},
|
||||
{'value': 'Option 2', 'color': 'blue'}
|
||||
],
|
||||
primary=True
|
||||
)
|
||||
link_row_field = field_handler.create_field(
|
||||
user=user,
|
||||
table=example_table,
|
||||
type_name='link_row',
|
||||
link_row_table=customers_table
|
||||
)
|
||||
select_options = customers_primary.select_options.all()
|
||||
|
||||
customers_row_1 = row_handler.create_row(
|
||||
user=user, table=customers_table,
|
||||
values={f'field_{customers_primary.id}': select_options[0].id}
|
||||
)
|
||||
customers_row_2 = row_handler.create_row(
|
||||
user=user, table=customers_table,
|
||||
values={f'field_{customers_primary.id}': select_options[1].id}
|
||||
)
|
||||
row_handler.create_row(
|
||||
user, table=example_table,
|
||||
values={f'field_{link_row_field.id}': [customers_row_1.id, customers_row_2.id]}
|
||||
)
|
||||
row_handler.create_row(
|
||||
user, table=example_table,
|
||||
values={f'field_{link_row_field.id}': [customers_row_1.id]}
|
||||
)
|
||||
|
||||
response = api_client.get(
|
||||
reverse('api:database:rows:list', kwargs={'table_id': example_table.id}),
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
response_json = response.json()
|
||||
|
||||
assert (
|
||||
response_json['results'][0][f'field_{link_row_field.id}'][0]['value'] ==
|
||||
'Option 1'
|
||||
)
|
||||
assert (
|
||||
response_json['results'][0][f'field_{link_row_field.id}'][1]['value'] ==
|
||||
'Option 2'
|
||||
)
|
||||
assert (
|
||||
response_json['results'][1][f'field_{link_row_field.id}'][0]['value'] ==
|
||||
'Option 1'
|
||||
)
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
* Fixed bug where you could not convert an existing field to a single select field
|
||||
without select options.
|
||||
* Fixed bug where is was not possible to create a relation to a table that has a single
|
||||
select as primary field.
|
||||
|
||||
## Released (2021-01-06)
|
||||
|
||||
* Allow larger values for the number field and improved the validation.
|
||||
|
|
|
@ -33,6 +33,10 @@ import RowEditFieldFile from '@baserow/modules/database/components/row/RowEditFi
|
|||
import RowEditFieldSingleSelect from '@baserow/modules/database/components/row/RowEditFieldSingleSelect'
|
||||
|
||||
import { trueString } from '@baserow/modules/database/utils/constants'
|
||||
import {
|
||||
getDateMomentFormat,
|
||||
getTimeMomentFormat,
|
||||
} from '@baserow/modules/database/utils/date'
|
||||
|
||||
export class FieldType extends Registerable {
|
||||
/**
|
||||
|
@ -151,7 +155,11 @@ export class FieldType extends Registerable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Should return a for humans readable representation of the value.
|
||||
* Should return a for humans readable representation of the value. This is for
|
||||
* example used by the link row field and row modal. This is not a problem with most
|
||||
* fields like text or number, but some store a more complex object object like
|
||||
* the single select or file field. In this case, the object might needs to be
|
||||
* converted to string.
|
||||
*/
|
||||
toHumanReadableString(field, value) {
|
||||
return value
|
||||
|
@ -661,6 +669,24 @@ export class DateFieldType extends FieldType {
|
|||
}
|
||||
}
|
||||
|
||||
toHumanReadableString(field, value) {
|
||||
const date = moment.utc(value)
|
||||
|
||||
if (date.isValid()) {
|
||||
const dateFormat = getDateMomentFormat(field.date_format)
|
||||
let dateString = date.format(dateFormat)
|
||||
|
||||
if (field.date_include_time) {
|
||||
const timeFormat = getTimeMomentFormat(field.date_time_format)
|
||||
dateString = `${dateString} ${date.format(timeFormat)}`
|
||||
}
|
||||
|
||||
return dateString
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to parse the clipboard text value with moment and returns the date in the
|
||||
* correct format for the field. If it can't be parsed null is returned.
|
||||
|
@ -812,6 +838,10 @@ export class FileFieldType extends FieldType {
|
|||
return RowEditFieldFile
|
||||
}
|
||||
|
||||
toHumanReadableString(field, value) {
|
||||
return value.map((file) => file.visible_name).join(', ')
|
||||
}
|
||||
|
||||
prepareValueForCopy(field, value) {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
@ -955,6 +985,13 @@ export class SingleSelectFieldType extends FieldType {
|
|||
}
|
||||
}
|
||||
|
||||
toHumanReadableString(field, value) {
|
||||
if (value === undefined || value === null) {
|
||||
return ''
|
||||
}
|
||||
return value.value
|
||||
}
|
||||
|
||||
getDocsDataType() {
|
||||
return 'integer'
|
||||
}
|
||||
|
|
|
@ -27,9 +27,12 @@ export default {
|
|||
// Prepare the new value with all the relations and emit that value to the
|
||||
// parent.
|
||||
const newValue = JSON.parse(JSON.stringify(value))
|
||||
const rowValue = this.$registry
|
||||
.get('field', primary.type)
|
||||
.toHumanReadableString(primary, row[`field_${primary.id}`])
|
||||
newValue.push({
|
||||
id: row.id,
|
||||
value: row[`field_${primary.id}`].toString(),
|
||||
value: rowValue.toString(),
|
||||
})
|
||||
this.$emit('update', newValue, value)
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "baserow",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"private": true,
|
||||
"description": "Baserow: open source online database web frontend.",
|
||||
"author": "Bram Wiepjes (Baserow)",
|
||||
|
|
Loading…
Add table
Reference in a new issue