1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-17 18:32:35 +00:00

Adding docker config and entrypoint.sh

(cherry picked from commit 014a3584a04e59f204a116bb625e2df5bd70d9bc)
This commit is contained in:
Keith Axline 2020-10-18 11:34:02 +00:00 committed by Bram Wiepjes
parent 30249a8634
commit 3c7a14fc9a
16 changed files with 446 additions and 17 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database
changelog.md
web-frontend/modules

View file

@ -45,11 +45,12 @@ class DatabaseConfig(AppConfig):
from .fields.field_types import (
TextFieldType, LongTextFieldType, URLFieldType, NumberFieldType,
BooleanFieldType, DateFieldType, LinkRowFieldType
BooleanFieldType, DateFieldType, LinkRowFieldType, EmailFieldType
)
field_type_registry.register(TextFieldType())
field_type_registry.register(LongTextFieldType())
field_type_registry.register(URLFieldType())
field_type_registry.register(EmailFieldType())
field_type_registry.register(NumberFieldType())
field_type_registry.register(BooleanFieldType())
field_type_registry.register(DateFieldType())

View file

@ -6,7 +6,7 @@ from dateutil.parser import ParserError
from datetime import datetime, date
from django.db import models
from django.core.validators import URLValidator
from django.core.validators import URLValidator, EmailValidator
from django.core.exceptions import ValidationError
from django.utils.timezone import make_aware
@ -21,7 +21,7 @@ from .handler import FieldHandler
from .registries import FieldType
from .models import (
NUMBER_TYPE_INTEGER, NUMBER_TYPE_DECIMAL, TextField, LongTextField, URLField,
NumberField, BooleanField, DateField, LinkRowField
NumberField, BooleanField, DateField, LinkRowField, EmailField
)
from .exceptions import LinkRowTableNotInSameDatabase, LinkRowTableNotProvided
@ -548,3 +548,42 @@ class LinkRowFieldType(FieldType):
values.append(instance.id)
return values
class EmailFieldType(FieldType):
type = 'email'
model_class = EmailField
def prepare_value_for_db(self, instance, value):
if value == '' or value is None:
return ''
validator = EmailValidator()
validator(value)
return value
def get_serializer_field(self, instance, **kwargs):
return serializers.EmailField(
required=False,
allow_null=True,
allow_blank=True,
**kwargs
)
def get_model_field(self, instance, **kwargs):
return models.EmailField(default='', blank=True, null=True, **kwargs)
def random_value(self, instance, fake, cache):
return fake.email()
def get_alter_column_type_function(self, connection, instance):
if connection.vendor == 'postgresql':
return r"""(
case
when p_in::text ~* '[A-Z0-9._+-]+@[A-Z0-9.-]+\.[A-Z]{2,}'
then p_in::text
else ''
end
)"""
return super().get_alter_column_type_function(connection, instance)

View file

@ -235,3 +235,7 @@ class LinkRowField(Field):
raise ValueError('The link row field does not yet have a relation id.')
return f'database_relation_{self.link_row_relation_id}'
class EmailField(Field):
pass

View file

@ -0,0 +1,25 @@
# Generated by Django 2.2.11 on 2020-10-09 13:22
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('database', '0014_viewsort'),
]
operations = [
migrations.CreateModel(
name='EmailField',
fields=[
('field_ptr', models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True, primary_key=True, serialize=False,
to='database.Field'
)),
],
bases=('database.field',),
),
]

View file

@ -3,7 +3,8 @@ from baserow.core.models import Application
from .table.models import Table
from .views.models import View, GridView, GridViewFieldOptions, ViewFilter
from .fields.models import (
Field, TextField, NumberField, LongTextField, BooleanField, DateField, LinkRowField
Field, TextField, NumberField, LongTextField, BooleanField, DateField, LinkRowField,
URLField, EmailField
)
__all__ = [
@ -11,7 +12,7 @@ __all__ = [
'Table',
'View', 'GridView', 'GridViewFieldOptions', 'ViewFilter',
'Field', 'TextField', 'NumberField', 'LongTextField', 'BooleanField', 'DateField',
'LinkRowField'
'LinkRowField', 'URLField', 'EmailField'
]

View file

@ -10,7 +10,7 @@ from django.db.models.fields.related import ManyToManyField
from baserow.contrib.database.fields.field_types import (
TextFieldType, LongTextFieldType, URLFieldType, NumberFieldType, DateFieldType,
LinkRowFieldType, BooleanFieldType
LinkRowFieldType, BooleanFieldType, EmailFieldType
)
from .registries import ViewFilterType
@ -35,7 +35,8 @@ class EqualViewFilterType(ViewFilterType):
LongTextFieldType.type,
URLFieldType.type,
NumberFieldType.type,
BooleanFieldType.type
BooleanFieldType.type,
EmailFieldType.type
]
def get_filter(self, field_name, value, model_field):
@ -70,6 +71,7 @@ class ContainsViewFilterType(ViewFilterType):
TextFieldType.type,
LongTextFieldType.type,
URLFieldType.type,
EmailFieldType.type
]
def get_filter(self, field_name, value, model_field):
@ -248,7 +250,8 @@ class EmptyViewFilterType(ViewFilterType):
NumberFieldType.type,
BooleanFieldType.type,
DateFieldType.type,
LinkRowFieldType.type
LinkRowFieldType.type,
EmailFieldType.type
]
def get_filter(self, field_name, value, model_field):

View file

@ -7,7 +7,9 @@ from rest_framework.status import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_400_BAD
from django.shortcuts import reverse
from baserow.contrib.database.fields.models import LongTextField, URLField, DateField
from baserow.contrib.database.fields.models import (
LongTextField, URLField, DateField, EmailField
)
@pytest.mark.django_db
@ -288,3 +290,94 @@ def test_date_field_type(api_client, data_fixture):
response = api_client.delete(url, HTTP_AUTHORIZATION=f'JWT {token}')
assert response.status_code == HTTP_204_NO_CONTENT
assert DateField.objects.all().count() == 1
@pytest.mark.django_db
def test_email_field_type(api_client, data_fixture):
user, token = data_fixture.create_user_and_token(
email='test@test.nl', password='password', first_name='Test1')
table = data_fixture.create_database_table(user=user)
response = api_client.post(
reverse('api:database:fields:list', kwargs={'table_id': table.id}),
{'name': 'Email', 'type': 'email'},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json['type'] == 'email'
assert EmailField.objects.all().count() == 1
field_id = response_json['id']
response = api_client.patch(
reverse('api:database:fields:item', kwargs={'field_id': field_id}),
{'name': 'Email2'},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
assert response.status_code == HTTP_200_OK
response = api_client.post(
reverse('api:database:rows:list', kwargs={'table_id': table.id}),
{
f'field_{field_id}': 'test@test.nl'
},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json[f'field_{field_id}'] == 'test@test.nl'
model = table.get_model(attribute_names=True)
row = model.objects.all().last()
assert row.email2 == 'test@test.nl'
response = api_client.post(
reverse('api:database:rows:list', kwargs={'table_id': table.id}),
{
f'field_{field_id}': ''
},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json[f'field_{field_id}'] == ''
row = model.objects.all().last()
assert row.email2 == ''
response = api_client.post(
reverse('api:database:rows:list', kwargs={'table_id': table.id}),
{
f'field_{field_id}': None
},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json[f'field_{field_id}'] == ''
row = model.objects.all().last()
assert row.email2 == ''
response = api_client.post(
reverse('api:database:rows:list', kwargs={'table_id': table.id}),
{},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json[f'field_{field_id}'] == ''
row = model.objects.all().last()
assert row.email2 == ''
email = reverse('api:database:fields:item', kwargs={'field_id': field_id})
response = api_client.delete(email, HTTP_AUTHORIZATION=f'JWT {token}')
assert response.status_code == HTTP_204_NO_CONTENT
assert EmailField.objects.all().count() == 0

View file

@ -8,7 +8,9 @@ from django.core.exceptions import ValidationError
from django.utils.timezone import make_aware, datetime
from baserow.contrib.database.fields.field_types import DateFieldType
from baserow.contrib.database.fields.models import LongTextField, URLField, DateField
from baserow.contrib.database.fields.models import (
LongTextField, URLField, DateField, EmailField
)
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.rows.handler import RowHandler
@ -384,3 +386,98 @@ def test_date_field_type(data_fixture):
field_handler.delete_field(user=user, field=date_field_2)
assert len(DateField.objects.all()) == 0
@pytest.mark.django_db
def test_email_field_type(data_fixture):
user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
table_2 = data_fixture.create_database_table(user=user, database=table.database)
field = data_fixture.create_text_field(table=table, order=1, name='name')
field_handler = FieldHandler()
row_handler = RowHandler()
field_2 = field_handler.create_field(user=user, table=table, type_name='email',
name='email')
number = field_handler.create_field(user=user, table=table, type_name='number',
name='number')
assert len(EmailField.objects.all()) == 1
model = table.get_model(attribute_names=True)
with pytest.raises(ValidationError):
row_handler.create_row(user=user, table=table, values={
'email': 'invalid_email'
}, model=model)
with pytest.raises(ValidationError):
row_handler.create_row(user=user, table=table, values={
'email': 'invalid@email'
}, model=model)
row_0 = row_handler.create_row(user=user, table=table, values={
'name': 'a.very.STRANGE@email.address.coM',
'email': 'test@test.nl',
'number': 5
}, model=model)
row_1 = row_handler.create_row(user=user, table=table, values={
'name': 'someuser',
'email': 'some@user.com',
'number': 10
}, model=model)
row_2 = row_handler.create_row(user=user, table=table, values={
'name': 'http://www.baserow.io',
'email': 'bram@test.nl'
}, model=model)
row_3 = row_handler.create_row(user=user, table=table, values={
'name': 'NOT AN EMAIL',
'email': 'something@example.com'
}, model=model)
row_4 = row_handler.create_row(user=user, table=table, values={
'name': 'testing@nowhere.org',
'email': ''
}, model=model)
row_5 = row_handler.create_row(user=user, table=table, values={
'email': None,
}, model=model)
row_6 = row_handler.create_row(user=user, table=table, values={}, model=model)
# Convert the text field to a url field so we can check how the conversion of
# values went.
field_handler.update_field(user=user, field=field, new_type_name='email')
field_handler.update_field(user=user, field=number, new_type_name='email')
model = table.get_model(attribute_names=True)
rows = model.objects.all()
assert rows[0].name == 'a.very.STRANGE@email.address.coM'
assert rows[0].email == 'test@test.nl'
assert rows[0].number == ''
assert rows[1].name == ''
assert rows[1].email == 'some@user.com'
assert rows[1].number == ''
assert rows[2].name == ''
assert rows[2].email == 'bram@test.nl'
assert rows[2].number == ''
assert rows[3].name == ''
assert rows[3].email == 'something@example.com'
assert rows[3].number == ''
assert rows[4].name == 'testing@nowhere.org'
assert rows[4].email == ''
assert rows[4].number == ''
assert rows[5].name == ''
assert rows[5].email == ''
assert rows[5].number == ''
assert rows[6].name == ''
assert rows[6].email == ''
assert rows[6].number == ''
field_handler.delete_field(user=user, field=field_2)
assert len(EmailField.objects.all()) == 2

View file

@ -4,6 +4,7 @@
* Fixed error when there is no view.
* Added Ubuntu installation guide documentation.
* Added Email field.
## Released (2020-10-06)

View file

@ -47,3 +47,8 @@ export const isValidURL = (str) => {
) // fragment locator
return !!pattern.test(str)
}
export const isValidEmail = (str) => {
const pattern = new RegExp('[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}', 'i') // check email format
return !!pattern.test(str)
}

View file

@ -0,0 +1,27 @@
<template>
<div class="control__elements">
<input
ref="input"
v-model="copy"
type="text"
class="input input--large"
:class="{ 'input--error': !isValid() }"
@keyup.enter="$refs.input.blur()"
@focus="select()"
@blur="unselect()"
/>
<div v-show="!isValid()" class="error">
{{ getError() }}
</div>
</div>
</template>
<script>
import rowEditField from '@baserow/modules/database/mixins/rowEditField'
import rowEditFieldInput from '@baserow/modules/database/mixins/rowEditFieldInput'
import EmailField from '@baserow/modules/database/mixins/EmailField'
export default {
mixins: [rowEditField, rowEditFieldInput, EmailField],
}
</script>

View file

@ -0,0 +1,47 @@
<template>
<div
class="grid-view__cell"
:class="{
active: selected,
editing: editing,
invalid: editing && !isValid(),
}"
@contextmenu="stopContextIfEditing($event)"
>
<div v-show="!editing" class="grid-field-text">
<template v-if="!selected">{{ value }}</template>
<a v-if="selected" :href="'mailto:' + value" target="_blank">
{{ value }}
</a>
</div>
<template v-if="editing">
<input
ref="input"
v-model="copy"
type="text"
class="grid-field-text__input"
/>
<div v-show="!isValid()" class="grid-view__cell--error align-right">
{{ getError() }}
</div>
</template>
</div>
</template>
<script>
import gridField from '@baserow/modules/database/mixins/gridField'
import gridFieldInput from '@baserow/modules/database/mixins/gridFieldInput'
import EmailField from '@baserow/modules/database/mixins/EmailField'
export default {
mixins: [gridField, gridFieldInput, EmailField],
methods: {
afterEdit() {
this.$nextTick(() => {
this.$refs.input.focus()
this.$refs.input.selectionStart = this.$refs.input.selectionEnd = 100000
})
},
},
}
</script>

View file

@ -1,6 +1,6 @@
import moment from 'moment'
import { isValidURL } from '@baserow/modules/core/utils/string'
import { isValidURL, isValidEmail } from '@baserow/modules/core/utils/string'
import { Registerable } from '@baserow/modules/core/registry'
import FieldNumberSubForm from '@baserow/modules/database/components/field/FieldNumberSubForm'
@ -11,6 +11,7 @@ import FieldLinkRowSubForm from '@baserow/modules/database/components/field/Fiel
import GridViewFieldText from '@baserow/modules/database/components/view/grid/GridViewFieldText'
import GridViewFieldLongText from '@baserow/modules/database/components/view/grid/GridViewFieldLongText'
import GridViewFieldURL from '@baserow/modules/database/components/view/grid/GridViewFieldURL'
import GridViewFieldEmail from '@baserow/modules/database/components/view/grid/GridViewFieldEmail'
import GridViewFieldLinkRow from '@baserow/modules/database/components/view/grid/GridViewFieldLinkRow'
import GridViewFieldNumber from '@baserow/modules/database/components/view/grid/GridViewFieldNumber'
import GridViewFieldBoolean from '@baserow/modules/database/components/view/grid/GridViewFieldBoolean'
@ -19,6 +20,7 @@ import GridViewFieldDate from '@baserow/modules/database/components/view/grid/Gr
import RowEditFieldText from '@baserow/modules/database/components/row/RowEditFieldText'
import RowEditFieldLongText from '@baserow/modules/database/components/row/RowEditFieldLongText'
import RowEditFieldURL from '@baserow/modules/database/components/row/RowEditFieldURL'
import RowEditFieldEmail from '@baserow/modules/database/components/row/RowEditFieldEmail'
import RowEditFieldLinkRow from '@baserow/modules/database/components/row/RowEditFieldLinkRow'
import RowEditFieldNumber from '@baserow/modules/database/components/row/RowEditFieldNumber'
import RowEditFieldBoolean from '@baserow/modules/database/components/row/RowEditFieldBoolean'
@ -553,3 +555,41 @@ export class URLFieldType extends FieldType {
}
}
}
export class EmailFieldType extends FieldType {
static getType() {
return 'email'
}
getIconClass() {
return 'at'
}
getName() {
return 'Email'
}
getGridViewFieldComponent() {
return GridViewFieldEmail
}
getRowEditFieldComponent() {
return RowEditFieldEmail
}
prepareValueForPaste(field, clipboardData) {
const value = clipboardData.getData('text')
return isValidEmail(value) ? value : ''
}
getSort(name, order) {
return (a, b) => {
const stringA = a[name] === null ? '' : '' + a[name]
const stringB = b[name] === null ? '' : '' + b[name]
return order === 'ASC'
? stringA.localeCompare(stringB)
: stringB.localeCompare(stringA)
}
}
}

View file

@ -0,0 +1,26 @@
import { isValidEmail } from '@baserow/modules/core/utils/string'
/**
* This mixin contains some method overrides for validating and formatting the
* email field. This mixin is used in both the GridViewFieldEmail and
* RowEditFieldEmail components.
*/
export default {
methods: {
/**
* Generates a human readable error for the user if something is wrong.
*/
getError() {
if (this.copy === null || this.copy === '') {
return null
}
if (!isValidEmail(this.copy)) {
return 'Invalid email'
}
return null
},
isValid() {
return this.getError() === null
},
},
}

View file

@ -4,6 +4,7 @@ import {
TextFieldType,
LongTextFieldType,
URLFieldType,
EmailFieldType,
LinkRowFieldType,
NumberFieldType,
BooleanFieldType,
@ -54,4 +55,5 @@ export default ({ store, app }) => {
app.$registry.register('field', new BooleanFieldType())
app.$registry.register('field', new DateFieldType())
app.$registry.register('field', new URLFieldType())
app.$registry.register('field', new EmailFieldType())
}

View file

@ -92,7 +92,7 @@ export class EqualViewFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['text', 'long_text', 'url', 'number']
return ['text', 'long_text', 'url', 'email', 'number']
}
matches(rowValue, filterValue) {
@ -120,7 +120,7 @@ export class NotEqualViewFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['text', 'long_text', 'url', 'number']
return ['text', 'long_text', 'url', 'email', 'number']
}
matches(rowValue, filterValue) {
@ -148,7 +148,7 @@ export class ContainsViewFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['text', 'long_text', 'url']
return ['text', 'long_text', 'url', 'email']
}
matches(rowValue, filterValue) {
@ -172,7 +172,7 @@ export class ContainsNotViewFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['text', 'long_text', 'url']
return ['text', 'long_text', 'url', 'email']
}
matches(rowValue, filterValue) {
@ -336,7 +336,16 @@ export class EmptyViewFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['text', 'long_text', 'url', 'number', 'date', 'boolean', 'link_row']
return [
'text',
'long_text',
'url',
'email',
'number',
'date',
'boolean',
'link_row',
]
}
matches(rowValue, filterValue) {
@ -363,7 +372,16 @@ export class NotEmptyViewFilterType extends ViewFilterType {
}
getCompatibleFieldTypes() {
return ['text', 'long_text', 'url', 'number', 'date', 'boolean', 'link_row']
return [
'text',
'long_text',
'url',
'email',
'number',
'date',
'boolean',
'link_row',
]
}
matches(rowValue, filterValue) {